@memberjunction/telephony-adapters 0.0.0 → 6.1.0-edge.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +183 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts +20 -0
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/RingCentralTelephonyResolver.js +83 -0
- package/dist/resolvers/RingCentralTelephonyResolver.js.map +1 -0
- package/dist/resolvers/TeamsMeetingsResolver.d.ts +21 -0
- package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +1 -0
- package/dist/resolvers/TeamsMeetingsResolver.js +87 -0
- package/dist/resolvers/TeamsMeetingsResolver.js.map +1 -0
- package/dist/resolvers/TelephonyResolver.d.ts +20 -0
- package/dist/resolvers/TelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/TelephonyResolver.js +83 -0
- package/dist/resolvers/TelephonyResolver.js.map +1 -0
- package/dist/resolvers/VonageTelephonyResolver.d.ts +20 -0
- package/dist/resolvers/VonageTelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/VonageTelephonyResolver.js +83 -0
- package/dist/resolvers/VonageTelephonyResolver.js.map +1 -0
- package/dist/resolvers/index.d.ts +10 -0
- package/dist/resolvers/index.d.ts.map +1 -0
- package/dist/resolvers/index.js +10 -0
- package/dist/resolvers/index.js.map +1 -0
- package/dist/server-extensions/RingCentralTelephonyExtension.d.ts +14 -0
- package/dist/server-extensions/RingCentralTelephonyExtension.d.ts.map +1 -0
- package/dist/server-extensions/RingCentralTelephonyExtension.js +81 -0
- package/dist/server-extensions/RingCentralTelephonyExtension.js.map +1 -0
- package/dist/server-extensions/TeamsMeetingsExtension.d.ts +17 -0
- package/dist/server-extensions/TeamsMeetingsExtension.d.ts.map +1 -0
- package/dist/server-extensions/TeamsMeetingsExtension.js +109 -0
- package/dist/server-extensions/TeamsMeetingsExtension.js.map +1 -0
- package/dist/server-extensions/TwilioTelephonyExtension.d.ts +14 -0
- package/dist/server-extensions/TwilioTelephonyExtension.d.ts.map +1 -0
- package/dist/server-extensions/TwilioTelephonyExtension.js +88 -0
- package/dist/server-extensions/TwilioTelephonyExtension.js.map +1 -0
- package/dist/server-extensions/VonageTelephonyExtension.d.ts +14 -0
- package/dist/server-extensions/VonageTelephonyExtension.d.ts.map +1 -0
- package/dist/server-extensions/VonageTelephonyExtension.js +89 -0
- package/dist/server-extensions/VonageTelephonyExtension.js.map +1 -0
- package/dist/sessionManager.d.ts +26 -0
- package/dist/sessionManager.d.ts.map +1 -0
- package/dist/sessionManager.js +38 -0
- package/dist/sessionManager.js.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.d.ts +79 -0
- package/dist/telephony/RingCentralTelephonyService.d.ts.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.js +225 -0
- package/dist/telephony/RingCentralTelephonyService.js.map +1 -0
- package/dist/telephony/TeamsMeetingsRouter.d.ts +21 -0
- package/dist/telephony/TeamsMeetingsRouter.d.ts.map +1 -0
- package/dist/telephony/TeamsMeetingsRouter.js +84 -0
- package/dist/telephony/TeamsMeetingsRouter.js.map +1 -0
- package/dist/telephony/TeamsMeetingsService.d.ts +76 -0
- package/dist/telephony/TeamsMeetingsService.d.ts.map +1 -0
- package/dist/telephony/TeamsMeetingsService.js +157 -0
- package/dist/telephony/TeamsMeetingsService.js.map +1 -0
- package/dist/telephony/TwilioTelephonyRouter.d.ts +30 -0
- package/dist/telephony/TwilioTelephonyRouter.d.ts.map +1 -0
- package/dist/telephony/TwilioTelephonyRouter.js +137 -0
- package/dist/telephony/TwilioTelephonyRouter.js.map +1 -0
- package/dist/telephony/TwilioTelephonyService.d.ts +91 -0
- package/dist/telephony/TwilioTelephonyService.d.ts.map +1 -0
- package/dist/telephony/TwilioTelephonyService.js +191 -0
- package/dist/telephony/TwilioTelephonyService.js.map +1 -0
- package/dist/telephony/VonageTelephonyRouter.d.ts +24 -0
- package/dist/telephony/VonageTelephonyRouter.d.ts.map +1 -0
- package/dist/telephony/VonageTelephonyRouter.js +175 -0
- package/dist/telephony/VonageTelephonyRouter.js.map +1 -0
- package/dist/telephony/VonageTelephonyService.d.ts +62 -0
- package/dist/telephony/VonageTelephonyService.d.ts.map +1 -0
- package/dist/telephony/VonageTelephonyService.js +162 -0
- package/dist/telephony/VonageTelephonyService.js.map +1 -0
- package/dist/telephony/calendar-scheduler.d.ts +41 -0
- package/dist/telephony/calendar-scheduler.d.ts.map +1 -0
- package/dist/telephony/calendar-scheduler.js +98 -0
- package/dist/telephony/calendar-scheduler.js.map +1 -0
- package/dist/telephony/index.d.ts +21 -0
- package/dist/telephony/index.d.ts.map +1 -0
- package/dist/telephony/index.js +26 -0
- package/dist/telephony/index.js.map +1 -0
- package/dist/telephony/ringcentral-runtime.d.ts +11 -0
- package/dist/telephony/ringcentral-runtime.d.ts.map +1 -0
- package/dist/telephony/ringcentral-runtime.js +20 -0
- package/dist/telephony/ringcentral-runtime.js.map +1 -0
- package/dist/telephony/teams-meetings-runtime.d.ts +11 -0
- package/dist/telephony/teams-meetings-runtime.d.ts.map +1 -0
- package/dist/telephony/teams-meetings-runtime.js +20 -0
- package/dist/telephony/teams-meetings-runtime.js.map +1 -0
- package/dist/telephony/teamsAcsMediaRegistry.d.ts +35 -0
- package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/teamsAcsMediaRegistry.js +86 -0
- package/dist/telephony/teamsAcsMediaRegistry.js.map +1 -0
- package/dist/telephony/telephony-runtime.d.ts +14 -0
- package/dist/telephony/telephony-runtime.d.ts.map +1 -0
- package/dist/telephony/telephony-runtime.js +23 -0
- package/dist/telephony/telephony-runtime.js.map +1 -0
- package/dist/telephony/twilioMediaRegistry.d.ts +60 -0
- package/dist/telephony/twilioMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/twilioMediaRegistry.js +109 -0
- package/dist/telephony/twilioMediaRegistry.js.map +1 -0
- package/dist/telephony/vonage-runtime.d.ts +11 -0
- package/dist/telephony/vonage-runtime.d.ts.map +1 -0
- package/dist/telephony/vonage-runtime.js +20 -0
- package/dist/telephony/vonage-runtime.js.map +1 -0
- package/dist/telephony/vonageMediaRegistry.d.ts +38 -0
- package/dist/telephony/vonageMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/vonageMediaRegistry.js +113 -0
- package/dist/telephony/vonageMediaRegistry.js.map +1 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +27 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -8
- package/README.md +0 -45
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Orchestrates Teams meeting bridge sessions (join-by-URL) for MJAPI.
|
|
3
|
+
*
|
|
4
|
+
* @module @memberjunction/telephony-adapters
|
|
5
|
+
*/
|
|
6
|
+
import { LogError } from '@memberjunction/core';
|
|
7
|
+
import { AIBridgeEngine } from '@memberjunction/ai-bridge-server';
|
|
8
|
+
import { CreateBridgeRealtimeSession } from '@memberjunction/ai-agents';
|
|
9
|
+
import { RealTeamsBindings, RealGraphCallsClient, PumpBackedAcsMedia } from '@memberjunction/ai-bridge-teams';
|
|
10
|
+
import { DefaultAgentSessionManager } from '../sessionManager.js';
|
|
11
|
+
const TEAMS_PROVIDER_DRIVER = 'TeamsBridge';
|
|
12
|
+
const AGENT_IDENTITY_ENTITY = 'MJ: AI Bridge Agent Identities';
|
|
13
|
+
/**
|
|
14
|
+
* Starts Teams meeting bridge sessions. One instance per server, constructed alongside the meetings router and
|
|
15
|
+
* sharing the {@link TeamsAcsMediaRegistry} with the server's ACS media adapter.
|
|
16
|
+
*/
|
|
17
|
+
export class TeamsMeetingsService {
|
|
18
|
+
constructor(config, registry, deps = {}) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.registry = registry;
|
|
21
|
+
/** Live Graph clients keyed by call id, so the webhook ingress can drive their roster/ended handlers. */
|
|
22
|
+
this.graphClientsByCall = new Map();
|
|
23
|
+
this.engine = deps.engine ?? AIBridgeEngine.Instance;
|
|
24
|
+
this.sessionFactory = deps.sessionFactory ?? CreateBridgeRealtimeSession;
|
|
25
|
+
this.sessionManager = deps.sessionManager ?? new DefaultAgentSessionManager();
|
|
26
|
+
this.graphClientFactory =
|
|
27
|
+
deps.graphClientFactory ?? ((accessToken, tenantId) => new RealGraphCallsClient({ AccessToken: accessToken, TenantId: tenantId }));
|
|
28
|
+
}
|
|
29
|
+
get Config() {
|
|
30
|
+
return this.config;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Joins a Teams meeting by URL for a given agent identity and starts the bridge session.
|
|
34
|
+
*/
|
|
35
|
+
async JoinMeetingByUrl(agentIdentityId, joinUrl, contextUser, provider) {
|
|
36
|
+
try {
|
|
37
|
+
const identity = await this.loadAgentIdentity(agentIdentityId, contextUser, provider);
|
|
38
|
+
if (!identity) {
|
|
39
|
+
return { accepted: false, reason: `Agent identity '${agentIdentityId}' not found or inactive.` };
|
|
40
|
+
}
|
|
41
|
+
const session = await this.startBridge({ agentID: identity.AgentID, joinUrl, contextUser, provider });
|
|
42
|
+
return { accepted: true, callId: session.CallId };
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
LogError(`[Meetings][Teams] join meeting failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
46
|
+
return { accepted: false, reason: e instanceof Error ? e.message : 'Internal error starting the agent.' };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Drives the retained Graph roster handler for a call from a Graph change notification.
|
|
51
|
+
*/
|
|
52
|
+
DriveParticipantsUpdated(callId, participants) {
|
|
53
|
+
this.graphClientsByCall.get(callId)?.DriveParticipantsUpdated(callId, participants);
|
|
54
|
+
}
|
|
55
|
+
/** Drives the call-ended handler for a call from a Graph change notification, then tears the call down. */
|
|
56
|
+
DriveCallEnded(callId) {
|
|
57
|
+
this.graphClientsByCall.get(callId)?.DriveCallEnded(callId);
|
|
58
|
+
this.graphClientsByCall.delete(callId);
|
|
59
|
+
this.registry.EndCall(callId);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Builds the {@link StartBridgeSessionParams} for a SCHEDULED Teams join.
|
|
63
|
+
*/
|
|
64
|
+
async BuildScheduledStartParams(args) {
|
|
65
|
+
await this.engine.Config(false, args.contextUser, args.provider);
|
|
66
|
+
const teamsProvider = this.resolveProvider();
|
|
67
|
+
const realtimeSession = await this.sessionFactory({
|
|
68
|
+
AgentID: args.agentID,
|
|
69
|
+
TargetAgentID: args.agentID,
|
|
70
|
+
ContextUser: args.contextUser,
|
|
71
|
+
MetadataProvider: args.provider,
|
|
72
|
+
AgentSessionID: args.agentSessionID,
|
|
73
|
+
RoomName: args.joinUrl,
|
|
74
|
+
});
|
|
75
|
+
const graphClient = this.graphClientFactory(this.config.botAccessToken ?? '', this.config.tenantId);
|
|
76
|
+
return {
|
|
77
|
+
AgentSessionID: args.agentSessionID,
|
|
78
|
+
AgentID: args.agentID,
|
|
79
|
+
TargetAgentID: args.agentID,
|
|
80
|
+
Provider: teamsProvider,
|
|
81
|
+
RealtimeSession: realtimeSession,
|
|
82
|
+
Address: args.joinUrl,
|
|
83
|
+
Direction: 'Outbound',
|
|
84
|
+
Configuration: this.buildSessionConfiguration(),
|
|
85
|
+
BindSdk: this.buildBindSdk(graphClient),
|
|
86
|
+
ContextUser: args.contextUser,
|
|
87
|
+
MetadataProvider: args.provider,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// ── internals ────────────────────────────────────────────────────────────────
|
|
91
|
+
async startBridge(args) {
|
|
92
|
+
await this.engine.Config(false, args.contextUser, args.provider);
|
|
93
|
+
const teamsProvider = this.resolveProvider();
|
|
94
|
+
const agentSession = await this.sessionManager.CreateSession({ agentID: args.agentID, userID: args.contextUser.ID }, args.contextUser, args.provider);
|
|
95
|
+
const realtimeSession = await this.sessionFactory({
|
|
96
|
+
AgentID: args.agentID,
|
|
97
|
+
TargetAgentID: args.agentID,
|
|
98
|
+
ContextUser: args.contextUser,
|
|
99
|
+
MetadataProvider: args.provider,
|
|
100
|
+
AgentSessionID: agentSession.ID,
|
|
101
|
+
RoomName: args.joinUrl,
|
|
102
|
+
});
|
|
103
|
+
const graphClient = this.graphClientFactory(this.config.botAccessToken ?? '', this.config.tenantId);
|
|
104
|
+
const active = await this.engine.StartBridgeSession({
|
|
105
|
+
AgentSessionID: agentSession.ID,
|
|
106
|
+
AgentID: args.agentID,
|
|
107
|
+
TargetAgentID: args.agentID,
|
|
108
|
+
Provider: teamsProvider,
|
|
109
|
+
RealtimeSession: realtimeSession,
|
|
110
|
+
Address: args.joinUrl,
|
|
111
|
+
Direction: 'Outbound',
|
|
112
|
+
Configuration: this.buildSessionConfiguration(),
|
|
113
|
+
BindSdk: this.buildBindSdk(graphClient),
|
|
114
|
+
ContextUser: args.contextUser,
|
|
115
|
+
MetadataProvider: args.provider,
|
|
116
|
+
});
|
|
117
|
+
const callId = active.RoomKey;
|
|
118
|
+
if (callId) {
|
|
119
|
+
this.registry.RegisterCall(callId);
|
|
120
|
+
this.graphClientsByCall.set(callId, graphClient);
|
|
121
|
+
}
|
|
122
|
+
return { CallId: callId };
|
|
123
|
+
}
|
|
124
|
+
resolveProvider() {
|
|
125
|
+
const provider = this.engine.ProviderByDriverClass(TEAMS_PROVIDER_DRIVER) ?? this.engine.ProviderByName('Microsoft Teams');
|
|
126
|
+
if (!provider) {
|
|
127
|
+
throw new Error(`No active 'MJ: AI Bridge Providers' row for the Teams bridge (DriverClass '${TEAMS_PROVIDER_DRIVER}').`);
|
|
128
|
+
}
|
|
129
|
+
return provider;
|
|
130
|
+
}
|
|
131
|
+
buildBindSdk(graphClient) {
|
|
132
|
+
return (driver) => {
|
|
133
|
+
const teams = driver;
|
|
134
|
+
teams.SetSdkFactory(() => new RealTeamsBindings({
|
|
135
|
+
Graph: graphClient,
|
|
136
|
+
Media: new PumpBackedAcsMedia(this.registry),
|
|
137
|
+
ModelSampleRate: this.config.modelSampleRate,
|
|
138
|
+
}));
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
buildSessionConfiguration() {
|
|
142
|
+
const config = {};
|
|
143
|
+
if (this.config.tenantId) {
|
|
144
|
+
config.TenantId = this.config.tenantId;
|
|
145
|
+
}
|
|
146
|
+
return config;
|
|
147
|
+
}
|
|
148
|
+
async loadAgentIdentity(agentIdentityId, contextUser, provider) {
|
|
149
|
+
const entity = await provider.GetEntityObject(AGENT_IDENTITY_ENTITY, contextUser);
|
|
150
|
+
const loaded = await entity.Load(agentIdentityId);
|
|
151
|
+
if (!loaded || !entity.IsActive) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return entity;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=TeamsMeetingsService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TeamsMeetingsService.js","sourceRoot":"","sources":["../../src/telephony/TeamsMeetingsService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Express + WSS wiring for the Twilio telephony ingress.
|
|
3
|
+
*
|
|
4
|
+
* Two public surfaces (carriers cannot present an MJ JWT — the X-Twilio-Signature HMAC is the gate):
|
|
5
|
+
* - `POST /telephony/twilio/voice` — inbound voice webhook. Verifies the signature, resolves the
|
|
6
|
+
* dialed DID to a pinned agent, starts an inbound bridge session, and answers with the
|
|
7
|
+
* `<Connect><Stream>` TwiML that opens the Media-Streams socket.
|
|
8
|
+
* - `WSS /telephony/twilio/media` — the bidirectional Media-Streams socket. Frames are routed to the
|
|
9
|
+
* active call's `RealTwilioBindings` via the shared {@link TwilioCallMediaRegistry}.
|
|
10
|
+
*
|
|
11
|
+
* @module @memberjunction/telephony-adapters
|
|
12
|
+
*/
|
|
13
|
+
import { Router } from 'express';
|
|
14
|
+
import type { TwilioTelephonyConfig } from '../types.js';
|
|
15
|
+
import { TwilioCallMediaRegistry } from './twilioMediaRegistry.js';
|
|
16
|
+
import { TwilioTelephonyService } from './TwilioTelephonyService.js';
|
|
17
|
+
/** The mount path for the Twilio telephony public router. */
|
|
18
|
+
export declare const TWILIO_TELEPHONY_MOUNT_PATH = "/telephony/twilio";
|
|
19
|
+
/** The Media-Streams websocket path Twilio's `<Connect><Stream>` connects to. */
|
|
20
|
+
export declare const TWILIO_MEDIA_WSS_PATH = "/telephony/twilio/media";
|
|
21
|
+
/**
|
|
22
|
+
* Creates the public router + the Media-Streams WSS attachment function for Twilio telephony.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createTwilioTelephonyHandler(publicUrl: string, config: TwilioTelephonyConfig): {
|
|
25
|
+
publicRouter: Router;
|
|
26
|
+
attachMediaStreamServer: () => void;
|
|
27
|
+
registry: TwilioCallMediaRegistry;
|
|
28
|
+
service: TwilioTelephonyService;
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=TwilioTelephonyRouter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TwilioTelephonyRouter.d.ts","sourceRoot":"","sources":["../../src/telephony/TwilioTelephonyRouter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,MAAM,EAA2C,MAAM,SAAS,CAAC;AAW1E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE,6DAA6D;AAC7D,eAAO,MAAM,2BAA2B,sBAAsB,CAAC;AAE/D,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,4BAA4B,CAAC;AAc/D;;GAEG;AACH,wBAAgB,4BAA4B,CACxC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,qBAAqB,GAC9B;IACC,YAAY,EAAE,MAAM,CAAC;IACrB,uBAAuB,EAAE,MAAM,IAAI,CAAC;IACpC,QAAQ,EAAE,uBAAuB,CAAC;IAClC,OAAO,EAAE,sBAAsB,CAAC;CACnC,CAgBA"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Express + WSS wiring for the Twilio telephony ingress.
|
|
3
|
+
*
|
|
4
|
+
* Two public surfaces (carriers cannot present an MJ JWT — the X-Twilio-Signature HMAC is the gate):
|
|
5
|
+
* - `POST /telephony/twilio/voice` — inbound voice webhook. Verifies the signature, resolves the
|
|
6
|
+
* dialed DID to a pinned agent, starts an inbound bridge session, and answers with the
|
|
7
|
+
* `<Connect><Stream>` TwiML that opens the Media-Streams socket.
|
|
8
|
+
* - `WSS /telephony/twilio/media` — the bidirectional Media-Streams socket. Frames are routed to the
|
|
9
|
+
* active call's `RealTwilioBindings` via the shared {@link TwilioCallMediaRegistry}.
|
|
10
|
+
*
|
|
11
|
+
* @module @memberjunction/telephony-adapters
|
|
12
|
+
*/
|
|
13
|
+
import { Router, urlencoded } from 'express';
|
|
14
|
+
import { WebSocketServer } from 'ws';
|
|
15
|
+
import { RegisterMediaUpgradeRoute } from '@memberjunction/server-extensions-core';
|
|
16
|
+
import { LogError, LogStatus, Metadata } from '@memberjunction/core';
|
|
17
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
18
|
+
import { verifyTwilioSignature, resolveInboundCall, buildInboundVoiceTwiML, } from '@memberjunction/ai-bridge-twilio';
|
|
19
|
+
import { TwilioCallMediaRegistry } from './twilioMediaRegistry.js';
|
|
20
|
+
import { TwilioTelephonyService } from './TwilioTelephonyService.js';
|
|
21
|
+
/** The mount path for the Twilio telephony public router. */
|
|
22
|
+
export const TWILIO_TELEPHONY_MOUNT_PATH = '/telephony/twilio';
|
|
23
|
+
/** The Media-Streams websocket path Twilio's `<Connect><Stream>` connects to. */
|
|
24
|
+
export const TWILIO_MEDIA_WSS_PATH = '/telephony/twilio/media';
|
|
25
|
+
/** A polite TwiML response played when no agent is available for the dialed number. */
|
|
26
|
+
const NO_AGENT_TWIML = '<?xml version="1.0" encoding="UTF-8"?><Response><Say>Sorry, no agent is available to take this call.</Say><Hangup/></Response>';
|
|
27
|
+
/**
|
|
28
|
+
* Creates the public router + the Media-Streams WSS attachment function for Twilio telephony.
|
|
29
|
+
*/
|
|
30
|
+
export function createTwilioTelephonyHandler(publicUrl, config) {
|
|
31
|
+
const registry = new TwilioCallMediaRegistry();
|
|
32
|
+
const service = new TwilioTelephonyService(config, registry);
|
|
33
|
+
const publicRouter = Router();
|
|
34
|
+
// Twilio signs the form-urlencoded webhook body; parse it so we can both verify + resolve.
|
|
35
|
+
publicRouter.post('/voice', urlencoded({ extended: false }), async (req, res) => {
|
|
36
|
+
await handleInboundVoice(service, config, publicUrl, req, res);
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
publicRouter,
|
|
40
|
+
attachMediaStreamServer: () => attachMediaStreamServer(registry),
|
|
41
|
+
registry,
|
|
42
|
+
service,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Handles the inbound voice webhook: verify signature → resolve → start bridge → answer with stream TwiML. */
|
|
46
|
+
async function handleInboundVoice(service, config, publicUrl, req, res) {
|
|
47
|
+
const params = coerceParams(req.body);
|
|
48
|
+
const fullUrl = `${publicUrl.replace(/\/+$/, '')}${req.originalUrl}`;
|
|
49
|
+
if (!config.authToken || !verifyTwilioSignature(config.authToken, req.get('X-Twilio-Signature'), fullUrl, params)) {
|
|
50
|
+
res.status(403).type('text/plain').send('Invalid Twilio signature.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const { callSid, from, to } = resolveInboundCall(params);
|
|
54
|
+
const context = resolveServerContext();
|
|
55
|
+
if (!context) {
|
|
56
|
+
LogError('[Telephony][Twilio] no server context user available; cannot start inbound call.');
|
|
57
|
+
res.status(200).type('text/xml').send(NO_AGENT_TWIML);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const result = await service.HandleInboundCall({ callSid, from, to }, context.user, context.provider);
|
|
61
|
+
if (!result.accepted) {
|
|
62
|
+
LogStatus(`[Telephony][Twilio] inbound ${callSid} not accepted: ${result.reason ?? 'unknown'}`);
|
|
63
|
+
res.status(200).type('text/xml').send(NO_AGENT_TWIML);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
res.status(200).type('text/xml').send(buildInboundVoiceTwiML(config.streamPublicUrl));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Attaches the Media-Streams WSS to the shared HTTP server on {@link TWILIO_MEDIA_WSS_PATH}.
|
|
70
|
+
*/
|
|
71
|
+
function attachMediaStreamServer(registry) {
|
|
72
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
73
|
+
wss.on('connection', (socket) => wireMediaSocket(socket, registry));
|
|
74
|
+
RegisterMediaUpgradeRoute(TWILIO_MEDIA_WSS_PATH, wss);
|
|
75
|
+
LogStatus(`[Telephony][Twilio] Media-Streams WSS route registered at ${TWILIO_MEDIA_WSS_PATH}`);
|
|
76
|
+
}
|
|
77
|
+
/** Wires one Media-Streams socket: bind on `start`, dispatch inbound frames, tear down on close. */
|
|
78
|
+
function wireMediaSocket(socket, registry) {
|
|
79
|
+
let callSid = null;
|
|
80
|
+
const adapter = { send: (data) => socket.send(data), close: () => socket.close() };
|
|
81
|
+
socket.on('message', (raw) => {
|
|
82
|
+
const message = parseWsMessage(raw);
|
|
83
|
+
if (!message) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (message.event === 'start' && message.start?.callSid) {
|
|
87
|
+
callSid = message.start.callSid;
|
|
88
|
+
registry.AttachSocket(callSid, adapter, message.start.streamSid ?? '');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (callSid) {
|
|
92
|
+
registry.DispatchInbound(callSid, message);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
socket.on('close', () => {
|
|
96
|
+
if (callSid) {
|
|
97
|
+
registry.EndCall(callSid);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
socket.on('error', (err) => {
|
|
101
|
+
LogError(`[Telephony][Twilio] media socket error for call ${callSid ?? 'unknown'}: ${err.message}`);
|
|
102
|
+
if (callSid) {
|
|
103
|
+
registry.EndCall(callSid);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function parseWsMessage(raw) {
|
|
108
|
+
try {
|
|
109
|
+
const text = typeof raw === 'string' ? raw : raw instanceof Buffer ? raw.toString('utf8') : String(raw);
|
|
110
|
+
return JSON.parse(text);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Coerces an Express urlencoded body to the `Record<string,string>` the signature verifier expects. */
|
|
117
|
+
function coerceParams(body) {
|
|
118
|
+
const out = {};
|
|
119
|
+
if (body && typeof body === 'object') {
|
|
120
|
+
for (const [key, value] of Object.entries(body)) {
|
|
121
|
+
if (typeof value === 'string') {
|
|
122
|
+
out[key] = value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
/** Resolves the server-side principal + provider for the public webhook. */
|
|
129
|
+
function resolveServerContext() {
|
|
130
|
+
const user = UserCache.Instance.GetSystemUser() ?? UserCache.Users.find((u) => u.IsActive && u.Type?.trim().toLowerCase() === 'owner') ?? null;
|
|
131
|
+
const provider = Metadata.Provider; // global-provider-ok: inbound telephony webhook runs in server-global provider context
|
|
132
|
+
if (!user || !provider) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return { user, provider };
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=TwilioTelephonyRouter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TwilioTelephonyRouter.js","sourceRoot":"","sources":["../../src/telephony/TwilioTelephonyRouter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Orchestrates Twilio telephony bridge sessions (inbound + outbound) for MJAPI.
|
|
3
|
+
*
|
|
4
|
+
* This is the composition layer between the carrier ingress (the public webhook + the Media-Streams
|
|
5
|
+
* WSS) and the ONE unified realtime-agent pathway. It does NOT re-implement any bridge logic: it
|
|
6
|
+
* resolves the Twilio provider + the agent identity, opens a server-side realtime session via the
|
|
7
|
+
* agent layer's `CreateBridgeRealtimeSession` factory, binds `RealTwilioBindings` (real REST client +
|
|
8
|
+
* the per-call media registry) onto the driver, and hands everything to
|
|
9
|
+
* `AIBridgeEngine.StartBridgeSession` — exactly the seam the LiveKit room coordinator uses.
|
|
10
|
+
*
|
|
11
|
+
* Collaborators are injected (engine, session factory, session manager) so the resolution/branching
|
|
12
|
+
* logic is unit-testable with fakes; the actual call placement + media flow are live-only (real Twilio
|
|
13
|
+
* credentials + a publicly reachable stream URL).
|
|
14
|
+
*
|
|
15
|
+
* @module @memberjunction/telephony-adapters
|
|
16
|
+
*/
|
|
17
|
+
import { UserInfo, IMetadataProvider } from '@memberjunction/core';
|
|
18
|
+
import { AIBridgeEngine } from '@memberjunction/ai-bridge-server';
|
|
19
|
+
import { CreateBridgeRealtimeSession } from '@memberjunction/ai-agents';
|
|
20
|
+
import { type BridgeNativeSdkBinding } from '@memberjunction/ai-bridge-base';
|
|
21
|
+
import type { TwilioTelephonyConfig } from '../types.js';
|
|
22
|
+
import { IAgentSessionManager } from '../sessionManager.js';
|
|
23
|
+
import { TwilioCallMediaRegistry } from './twilioMediaRegistry.js';
|
|
24
|
+
/** A resolved inbound call's identifying fields (parsed from the Twilio webhook). */
|
|
25
|
+
export interface InboundCallInput {
|
|
26
|
+
/** The Twilio Call SID. */
|
|
27
|
+
callSid: string;
|
|
28
|
+
/** The caller's number (Twilio `From`). */
|
|
29
|
+
from: string;
|
|
30
|
+
/** The dialed DID (Twilio `To`) — resolved to an agent identity. */
|
|
31
|
+
to: string;
|
|
32
|
+
}
|
|
33
|
+
/** Result of starting an inbound bridge session. */
|
|
34
|
+
export interface InboundCallResult {
|
|
35
|
+
/** Whether a pinned agent was resolved + a bridge session started. */
|
|
36
|
+
accepted: boolean;
|
|
37
|
+
/** Why it was rejected (no agent identity for the DID, provider missing, etc.). */
|
|
38
|
+
reason?: string;
|
|
39
|
+
}
|
|
40
|
+
/** Injectable collaborators (production defaults wired in the constructor; fakes in tests). */
|
|
41
|
+
export interface TwilioTelephonyServiceDeps {
|
|
42
|
+
engine?: Pick<AIBridgeEngine, 'ProviderByName' | 'ProviderByDriverClass' | 'StartBridgeSession' | 'Config'>;
|
|
43
|
+
sessionFactory?: typeof CreateBridgeRealtimeSession;
|
|
44
|
+
sessionManager?: IAgentSessionManager;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Starts Twilio bridge sessions. One instance per server, constructed alongside the telephony router and
|
|
48
|
+
* sharing the {@link TwilioCallMediaRegistry} with the Media-Streams WSS server.
|
|
49
|
+
*/
|
|
50
|
+
export declare class TwilioTelephonyService {
|
|
51
|
+
private readonly config;
|
|
52
|
+
private readonly registry;
|
|
53
|
+
private readonly rest;
|
|
54
|
+
private readonly engine;
|
|
55
|
+
private readonly sessionFactory;
|
|
56
|
+
private readonly sessionManager;
|
|
57
|
+
constructor(config: TwilioTelephonyConfig, registry: TwilioCallMediaRegistry, deps?: TwilioTelephonyServiceDeps);
|
|
58
|
+
/**
|
|
59
|
+
* Resolves the dialed DID to a pinned agent and starts an INBOUND bridge session. The bound
|
|
60
|
+
* `RealTwilioBindings` will accept the Media-Streams socket (opened by the webhook's TwiML) for the
|
|
61
|
+
* Call SID. Returns `{ accepted:false }` (never throws) when no agent identity matches the DID, so
|
|
62
|
+
* the webhook can answer with a polite "no agent" message instead of a 500.
|
|
63
|
+
*/
|
|
64
|
+
HandleInboundCall(input: InboundCallInput, contextUser: UserInfo, provider: IMetadataProvider): Promise<InboundCallResult>;
|
|
65
|
+
/**
|
|
66
|
+
* Places an OUTBOUND call from a given agent identity to a destination number. The bound
|
|
67
|
+
* `RealTwilioBindings.createCall` issues the Twilio REST `POST /Calls` with the `<Connect><Stream>`
|
|
68
|
+
* TwiML; the returned Call SID is the bridge's external connection id.
|
|
69
|
+
*
|
|
70
|
+
* @returns The placed Call SID.
|
|
71
|
+
*/
|
|
72
|
+
PlaceOutboundCall(agentIdentityId: string, toNumber: string, contextUser: UserInfo, provider: IMetadataProvider): Promise<string>;
|
|
73
|
+
/** Shared inbound/outbound bridge-start: resolve provider, open the realtime session, bind the SDK, start. */
|
|
74
|
+
private startBridge;
|
|
75
|
+
/** Resolves the seeded Twilio provider row (by driver class, falling back to display name). */
|
|
76
|
+
private resolveProvider;
|
|
77
|
+
/**
|
|
78
|
+
* Builds the per-session SDK binding that wires the REAL Twilio bindings (REST client + the per-call
|
|
79
|
+
* media registry) onto the telephony driver — overriding the package's default unbound SDK.
|
|
80
|
+
*/
|
|
81
|
+
buildBindSdk(): BridgeNativeSdkBinding;
|
|
82
|
+
/**
|
|
83
|
+
* Assembles the per-session bridge Configuration the telephony driver reads.
|
|
84
|
+
*/
|
|
85
|
+
private buildSessionConfiguration;
|
|
86
|
+
/** Finds the active agent identity whose phone number matches the dialed DID and Twilio provider. */
|
|
87
|
+
private resolveAgentIdentityByPhone;
|
|
88
|
+
/** Loads a specific agent identity by id (used by outbound PlaceCall), requiring it be active. */
|
|
89
|
+
private loadAgentIdentity;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=TwilioTelephonyService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TwilioTelephonyService.d.ts","sourceRoot":"","sources":["../../src/telephony/TwilioTelephonyService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAW,QAAQ,EAAE,iBAAiB,EAAY,MAAM,sBAAsB,CAAC;AAGtF,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAEH,KAAK,sBAAsB,EAI9B,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAA8B,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAMnE,qFAAqF;AACrF,MAAM,WAAW,gBAAgB;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,EAAE,EAAE,MAAM,CAAC;CACd;AAED,oDAAoD;AACpD,MAAM,WAAW,iBAAiB;IAC9B,sEAAsE;IACtE,QAAQ,EAAE,OAAO,CAAC;IAClB,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,+FAA+F;AAC/F,MAAM,WAAW,0BAA0B;IACvC,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,gBAAgB,GAAG,uBAAuB,GAAG,oBAAoB,GAAG,QAAQ,CAAC,CAAC;IAC5G,cAAc,CAAC,EAAE,OAAO,2BAA2B,CAAC;IACpD,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACzC;AAED;;;GAGG;AACH,qBAAa,sBAAsB;IAO3B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAP7B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqG;IAC5H,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqC;IACpE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAuB;gBAGjC,MAAM,EAAE,qBAAqB,EAC7B,QAAQ,EAAE,uBAAuB,EAClD,IAAI,GAAE,0BAA+B;IAazC;;;;;OAKG;IACU,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAwBvI;;;;;;OAMG;IACU,iBAAiB,CAAC,eAAe,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;IAkB9I,8GAA8G;YAChG,WAAW;IAsCzB,+FAA+F;IAC/F,OAAO,CAAC,eAAe;IAQvB;;;OAGG;IACI,YAAY,IAAI,sBAAsB;IAgB7C;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAWjC,qGAAqG;YACvF,2BAA2B;IAsBzC,kGAAkG;YACpF,iBAAiB;CAQlC"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Orchestrates Twilio telephony bridge sessions (inbound + outbound) for MJAPI.
|
|
3
|
+
*
|
|
4
|
+
* This is the composition layer between the carrier ingress (the public webhook + the Media-Streams
|
|
5
|
+
* WSS) and the ONE unified realtime-agent pathway. It does NOT re-implement any bridge logic: it
|
|
6
|
+
* resolves the Twilio provider + the agent identity, opens a server-side realtime session via the
|
|
7
|
+
* agent layer's `CreateBridgeRealtimeSession` factory, binds `RealTwilioBindings` (real REST client +
|
|
8
|
+
* the per-call media registry) onto the driver, and hands everything to
|
|
9
|
+
* `AIBridgeEngine.StartBridgeSession` — exactly the seam the LiveKit room coordinator uses.
|
|
10
|
+
*
|
|
11
|
+
* Collaborators are injected (engine, session factory, session manager) so the resolution/branching
|
|
12
|
+
* logic is unit-testable with fakes; the actual call placement + media flow are live-only (real Twilio
|
|
13
|
+
* credentials + a publicly reachable stream URL).
|
|
14
|
+
*
|
|
15
|
+
* @module @memberjunction/telephony-adapters
|
|
16
|
+
*/
|
|
17
|
+
import { RunView, LogError } from '@memberjunction/core';
|
|
18
|
+
import { EscapeSQLString } from '@memberjunction/global';
|
|
19
|
+
import { AIBridgeEngine } from '@memberjunction/ai-bridge-server';
|
|
20
|
+
import { CreateBridgeRealtimeSession } from '@memberjunction/ai-agents';
|
|
21
|
+
import { DIRECTION_CONFIG_KEY, FROM_NUMBER_CONFIG_KEY, INBOUND_CALL_ID_CONFIG_KEY, } from '@memberjunction/ai-bridge-base';
|
|
22
|
+
import { TwilioCallSdk, RealTwilioBindings, RealTwilioRestClient } from '@memberjunction/ai-bridge-twilio';
|
|
23
|
+
import { DefaultAgentSessionManager } from '../sessionManager.js';
|
|
24
|
+
const TWILIO_PROVIDER_DRIVER = 'TwilioBridge';
|
|
25
|
+
const AGENT_IDENTITY_ENTITY = 'MJ: AI Bridge Agent Identities';
|
|
26
|
+
const PHONE_IDENTITY_TYPE = 'PhoneNumber';
|
|
27
|
+
/**
|
|
28
|
+
* Starts Twilio bridge sessions. One instance per server, constructed alongside the telephony router and
|
|
29
|
+
* sharing the {@link TwilioCallMediaRegistry} with the Media-Streams WSS server.
|
|
30
|
+
*/
|
|
31
|
+
export class TwilioTelephonyService {
|
|
32
|
+
constructor(config, registry, deps = {}) {
|
|
33
|
+
this.config = config;
|
|
34
|
+
this.registry = registry;
|
|
35
|
+
this.rest = new RealTwilioRestClient({
|
|
36
|
+
AccountSid: config.accountSid,
|
|
37
|
+
AuthToken: config.authToken,
|
|
38
|
+
ApiKeySid: config.apiKeySid,
|
|
39
|
+
ApiKeySecret: config.apiKeySecret,
|
|
40
|
+
});
|
|
41
|
+
this.engine = deps.engine ?? AIBridgeEngine.Instance;
|
|
42
|
+
this.sessionFactory = deps.sessionFactory ?? CreateBridgeRealtimeSession;
|
|
43
|
+
this.sessionManager = deps.sessionManager ?? new DefaultAgentSessionManager();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolves the dialed DID to a pinned agent and starts an INBOUND bridge session. The bound
|
|
47
|
+
* `RealTwilioBindings` will accept the Media-Streams socket (opened by the webhook's TwiML) for the
|
|
48
|
+
* Call SID. Returns `{ accepted:false }` (never throws) when no agent identity matches the DID, so
|
|
49
|
+
* the webhook can answer with a polite "no agent" message instead of a 500.
|
|
50
|
+
*/
|
|
51
|
+
async HandleInboundCall(input, contextUser, provider) {
|
|
52
|
+
try {
|
|
53
|
+
await this.engine.Config(false, contextUser, provider);
|
|
54
|
+
const twilioProvider = this.resolveProvider();
|
|
55
|
+
const identity = await this.resolveAgentIdentityByPhone(input.to, twilioProvider.ID, contextUser);
|
|
56
|
+
if (!identity) {
|
|
57
|
+
return { accepted: false, reason: `No active agent identity for dialed number '${input.to}'.` };
|
|
58
|
+
}
|
|
59
|
+
this.registry.RegisterCall(input.callSid);
|
|
60
|
+
await this.startBridge({
|
|
61
|
+
agentID: identity.AgentID,
|
|
62
|
+
direction: 'Inbound',
|
|
63
|
+
address: input.from,
|
|
64
|
+
inboundCallId: input.callSid,
|
|
65
|
+
contextUser,
|
|
66
|
+
provider,
|
|
67
|
+
});
|
|
68
|
+
return { accepted: true };
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
LogError(`[Telephony][Twilio] inbound call ${input.callSid} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
72
|
+
return { accepted: false, reason: 'Internal error starting the agent.' };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Places an OUTBOUND call from a given agent identity to a destination number. The bound
|
|
77
|
+
* `RealTwilioBindings.createCall` issues the Twilio REST `POST /Calls` with the `<Connect><Stream>`
|
|
78
|
+
* TwiML; the returned Call SID is the bridge's external connection id.
|
|
79
|
+
*
|
|
80
|
+
* @returns The placed Call SID.
|
|
81
|
+
*/
|
|
82
|
+
async PlaceOutboundCall(agentIdentityId, toNumber, contextUser, provider) {
|
|
83
|
+
const identity = await this.loadAgentIdentity(agentIdentityId, contextUser, provider);
|
|
84
|
+
if (!identity) {
|
|
85
|
+
throw new Error(`Agent identity '${agentIdentityId}' not found or inactive.`);
|
|
86
|
+
}
|
|
87
|
+
const session = await this.startBridge({
|
|
88
|
+
agentID: identity.AgentID,
|
|
89
|
+
direction: 'Outbound',
|
|
90
|
+
address: toNumber,
|
|
91
|
+
fromNumber: identity.IdentityValue,
|
|
92
|
+
contextUser,
|
|
93
|
+
provider,
|
|
94
|
+
});
|
|
95
|
+
return session.RoomKey ?? '';
|
|
96
|
+
}
|
|
97
|
+
// ── internals ────────────────────────────────────────────────────────────────
|
|
98
|
+
/** Shared inbound/outbound bridge-start: resolve provider, open the realtime session, bind the SDK, start. */
|
|
99
|
+
async startBridge(args) {
|
|
100
|
+
await this.engine.Config(false, args.contextUser, args.provider);
|
|
101
|
+
const twilioProvider = this.resolveProvider();
|
|
102
|
+
const agentSession = await this.sessionManager.CreateSession({ agentID: args.agentID, userID: args.contextUser.ID }, args.contextUser, args.provider);
|
|
103
|
+
const realtimeSession = await this.sessionFactory({
|
|
104
|
+
AgentID: args.agentID,
|
|
105
|
+
TargetAgentID: args.agentID,
|
|
106
|
+
ContextUser: args.contextUser,
|
|
107
|
+
MetadataProvider: args.provider,
|
|
108
|
+
AgentSessionID: agentSession.ID,
|
|
109
|
+
RoomName: args.address,
|
|
110
|
+
});
|
|
111
|
+
const active = await this.engine.StartBridgeSession({
|
|
112
|
+
AgentSessionID: agentSession.ID,
|
|
113
|
+
AgentID: args.agentID,
|
|
114
|
+
TargetAgentID: args.agentID,
|
|
115
|
+
Provider: twilioProvider,
|
|
116
|
+
RealtimeSession: realtimeSession,
|
|
117
|
+
Address: args.address,
|
|
118
|
+
Direction: args.direction,
|
|
119
|
+
Configuration: this.buildSessionConfiguration(args.direction, args.fromNumber, args.inboundCallId),
|
|
120
|
+
BindSdk: this.buildBindSdk(),
|
|
121
|
+
ContextUser: args.contextUser,
|
|
122
|
+
MetadataProvider: args.provider,
|
|
123
|
+
});
|
|
124
|
+
return { RoomKey: active.RoomKey };
|
|
125
|
+
}
|
|
126
|
+
/** Resolves the seeded Twilio provider row (by driver class, falling back to display name). */
|
|
127
|
+
resolveProvider() {
|
|
128
|
+
const provider = this.engine.ProviderByDriverClass(TWILIO_PROVIDER_DRIVER) ?? this.engine.ProviderByName('Twilio');
|
|
129
|
+
if (!provider) {
|
|
130
|
+
throw new Error(`No active 'MJ: AI Bridge Providers' row for the Twilio bridge (DriverClass '${TWILIO_PROVIDER_DRIVER}').`);
|
|
131
|
+
}
|
|
132
|
+
return provider;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Builds the per-session SDK binding that wires the REAL Twilio bindings (REST client + the per-call
|
|
136
|
+
* media registry) onto the telephony driver — overriding the package's default unbound SDK.
|
|
137
|
+
*/
|
|
138
|
+
buildBindSdk() {
|
|
139
|
+
return (driver) => {
|
|
140
|
+
const telephony = driver;
|
|
141
|
+
telephony.SetSdkFactory(() => new TwilioCallSdk(new RealTwilioBindings({
|
|
142
|
+
Rest: this.rest,
|
|
143
|
+
MediaPump: this.registry,
|
|
144
|
+
StreamUrl: this.config.streamPublicUrl,
|
|
145
|
+
StatusCallbackUrl: this.config.statusCallbackUrl,
|
|
146
|
+
})));
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Assembles the per-session bridge Configuration the telephony driver reads.
|
|
151
|
+
*/
|
|
152
|
+
buildSessionConfiguration(direction, fromNumber, inboundCallId) {
|
|
153
|
+
const config = { [DIRECTION_CONFIG_KEY]: direction };
|
|
154
|
+
if (fromNumber) {
|
|
155
|
+
config[FROM_NUMBER_CONFIG_KEY] = fromNumber;
|
|
156
|
+
}
|
|
157
|
+
if (inboundCallId) {
|
|
158
|
+
config[INBOUND_CALL_ID_CONFIG_KEY] = inboundCallId;
|
|
159
|
+
}
|
|
160
|
+
return config;
|
|
161
|
+
}
|
|
162
|
+
/** Finds the active agent identity whose phone number matches the dialed DID and Twilio provider. */
|
|
163
|
+
async resolveAgentIdentityByPhone(dialedNumber, providerId, contextUser) {
|
|
164
|
+
const normalized = (dialedNumber ?? '').trim();
|
|
165
|
+
if (!normalized) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const rv = new RunView();
|
|
169
|
+
const result = await rv.RunView({
|
|
170
|
+
EntityName: AGENT_IDENTITY_ENTITY,
|
|
171
|
+
ExtraFilter: `IdentityType='${PHONE_IDENTITY_TYPE}' AND IdentityValue='${EscapeSQLString(normalized)}' AND ProviderID='${EscapeSQLString(providerId)}' AND IsActive=1`,
|
|
172
|
+
MaxRows: 1,
|
|
173
|
+
ResultType: 'entity_object',
|
|
174
|
+
}, contextUser);
|
|
175
|
+
if (!result.Success) {
|
|
176
|
+
LogError(`[Telephony][Twilio] agent-identity lookup failed: ${result.ErrorMessage}`);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
return result.Results?.[0] ?? null;
|
|
180
|
+
}
|
|
181
|
+
/** Loads a specific agent identity by id (used by outbound PlaceCall), requiring it be active. */
|
|
182
|
+
async loadAgentIdentity(agentIdentityId, contextUser, provider) {
|
|
183
|
+
const entity = await provider.GetEntityObject(AGENT_IDENTITY_ENTITY, contextUser);
|
|
184
|
+
const loaded = await entity.Load(agentIdentityId);
|
|
185
|
+
if (!loaded || !entity.IsActive) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
return entity;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=TwilioTelephonyService.js.map
|