@stacksjs/ts-cloud 0.1.3 → 0.1.5
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/README.md +98 -13
- package/dist/aws/acm.d.ts +129 -0
- package/dist/aws/application-autoscaling.d.ts +282 -0
- package/dist/aws/bedrock.d.ts +2292 -0
- package/dist/aws/client.d.ts +79 -0
- package/dist/aws/cloudformation.d.ts +105 -0
- package/dist/aws/cloudfront.d.ts +265 -0
- package/dist/aws/cloudwatch-logs.d.ts +48 -0
- package/dist/aws/comprehend.d.ts +505 -0
- package/dist/aws/connect.d.ts +377 -0
- package/dist/aws/deploy-imap.d.ts +14 -0
- package/dist/aws/dynamodb.d.ts +176 -0
- package/dist/aws/ec2.d.ts +272 -0
- package/dist/aws/ecr.d.ts +149 -0
- package/dist/aws/ecs.d.ts +162 -0
- package/dist/aws/elasticache.d.ts +71 -0
- package/dist/aws/elbv2.d.ts +248 -0
- package/dist/aws/email.d.ts +175 -0
- package/dist/aws/eventbridge.d.ts +142 -0
- package/dist/aws/iam.d.ts +638 -0
- package/dist/aws/imap-server.d.ts +119 -0
- package/{src/aws/index.ts → dist/aws/index.d.ts} +62 -83
- package/{src/aws/kendra.ts → dist/aws/kendra.d.ts} +71 -386
- package/dist/aws/lambda.d.ts +232 -0
- package/dist/aws/opensearch.d.ts +87 -0
- package/dist/aws/personalize.d.ts +516 -0
- package/dist/aws/polly.d.ts +214 -0
- package/dist/aws/rds.d.ts +240 -0
- package/dist/aws/rekognition.d.ts +543 -0
- package/dist/aws/route53-domains.d.ts +113 -0
- package/dist/aws/route53.d.ts +215 -0
- package/dist/aws/s3.d.ts +212 -0
- package/dist/aws/scheduler.d.ts +140 -0
- package/dist/aws/secrets-manager.d.ts +170 -0
- package/dist/aws/ses.d.ts +288 -0
- package/dist/aws/setup-phone.d.ts +0 -0
- package/dist/aws/setup-sms.d.ts +115 -0
- package/dist/aws/sms.d.ts +304 -0
- package/dist/aws/smtp-server.d.ts +61 -0
- package/dist/aws/sns.d.ts +117 -0
- package/dist/aws/sqs.d.ts +65 -0
- package/dist/aws/ssm.d.ts +179 -0
- package/dist/aws/sts.d.ts +15 -0
- package/dist/aws/support.d.ts +104 -0
- package/dist/aws/test-imap.d.ts +0 -0
- package/dist/aws/textract.d.ts +403 -0
- package/dist/aws/transcribe.d.ts +60 -0
- package/dist/aws/translate.d.ts +358 -0
- package/dist/aws/voice.d.ts +219 -0
- package/dist/bin/cli.js +1724 -0
- package/dist/config.d.ts +7 -0
- package/dist/deploy/index.d.ts +2 -0
- package/dist/deploy/static-site-external-dns.d.ts +51 -0
- package/dist/deploy/static-site.d.ts +71 -0
- package/dist/dns/cloudflare.d.ts +52 -0
- package/dist/dns/godaddy.d.ts +38 -0
- package/dist/dns/index.d.ts +45 -0
- package/dist/dns/porkbun.d.ts +18 -0
- package/dist/dns/route53-adapter.d.ts +38 -0
- package/{src/dns/types.ts → dist/dns/types.d.ts} +26 -63
- package/dist/dns/validator.d.ts +78 -0
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/infrastructure.d.ts +30 -0
- package/{src/index.ts → dist/index.d.ts} +70 -93
- package/dist/index.js +7881 -0
- package/dist/push/apns.d.ts +60 -0
- package/dist/push/fcm.d.ts +117 -0
- package/dist/push/index.d.ts +14 -0
- package/dist/security/pre-deploy-scanner.d.ts +69 -0
- package/dist/ssl/acme-client.d.ts +67 -0
- package/dist/ssl/index.d.ts +2 -0
- package/dist/ssl/letsencrypt.d.ts +48 -0
- package/dist/types.d.ts +1 -0
- package/dist/utils/cli.d.ts +123 -0
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/template.d.ts +23 -0
- package/package.json +8 -8
- package/bin/cli.ts +0 -133
- package/bin/commands/analytics.ts +0 -328
- package/bin/commands/api.ts +0 -379
- package/bin/commands/assets.ts +0 -221
- package/bin/commands/audit.ts +0 -501
- package/bin/commands/backup.ts +0 -682
- package/bin/commands/cache.ts +0 -294
- package/bin/commands/cdn.ts +0 -281
- package/bin/commands/config.ts +0 -202
- package/bin/commands/container.ts +0 -105
- package/bin/commands/cost.ts +0 -208
- package/bin/commands/database.ts +0 -401
- package/bin/commands/deploy.ts +0 -674
- package/bin/commands/domain.ts +0 -397
- package/bin/commands/email.ts +0 -423
- package/bin/commands/environment.ts +0 -285
- package/bin/commands/events.ts +0 -424
- package/bin/commands/firewall.ts +0 -145
- package/bin/commands/function.ts +0 -116
- package/bin/commands/generate.ts +0 -280
- package/bin/commands/git.ts +0 -139
- package/bin/commands/iam.ts +0 -464
- package/bin/commands/index.ts +0 -48
- package/bin/commands/init.ts +0 -120
- package/bin/commands/logs.ts +0 -148
- package/bin/commands/network.ts +0 -579
- package/bin/commands/notify.ts +0 -489
- package/bin/commands/queue.ts +0 -407
- package/bin/commands/scheduler.ts +0 -370
- package/bin/commands/secrets.ts +0 -54
- package/bin/commands/server.ts +0 -629
- package/bin/commands/shared.ts +0 -97
- package/bin/commands/ssl.ts +0 -138
- package/bin/commands/stack.ts +0 -325
- package/bin/commands/status.ts +0 -385
- package/bin/commands/storage.ts +0 -450
- package/bin/commands/team.ts +0 -96
- package/bin/commands/tunnel.ts +0 -489
- package/bin/commands/utils.ts +0 -202
- package/build.ts +0 -15
- package/cloud +0 -2
- package/src/aws/acm.ts +0 -768
- package/src/aws/application-autoscaling.ts +0 -845
- package/src/aws/bedrock.ts +0 -4074
- package/src/aws/client.ts +0 -878
- package/src/aws/cloudformation.ts +0 -896
- package/src/aws/cloudfront.ts +0 -1531
- package/src/aws/cloudwatch-logs.ts +0 -154
- package/src/aws/comprehend.ts +0 -839
- package/src/aws/connect.ts +0 -1056
- package/src/aws/deploy-imap.ts +0 -384
- package/src/aws/dynamodb.ts +0 -340
- package/src/aws/ec2.ts +0 -1385
- package/src/aws/ecr.ts +0 -621
- package/src/aws/ecs.ts +0 -615
- package/src/aws/elasticache.ts +0 -301
- package/src/aws/elbv2.ts +0 -942
- package/src/aws/email.ts +0 -928
- package/src/aws/eventbridge.ts +0 -248
- package/src/aws/iam.ts +0 -1689
- package/src/aws/imap-server.ts +0 -2100
- package/src/aws/lambda.ts +0 -786
- package/src/aws/opensearch.ts +0 -158
- package/src/aws/personalize.ts +0 -977
- package/src/aws/polly.ts +0 -559
- package/src/aws/rds.ts +0 -888
- package/src/aws/rekognition.ts +0 -846
- package/src/aws/route53-domains.ts +0 -359
- package/src/aws/route53.ts +0 -1046
- package/src/aws/s3.ts +0 -2318
- package/src/aws/scheduler.ts +0 -571
- package/src/aws/secrets-manager.ts +0 -769
- package/src/aws/ses.ts +0 -1081
- package/src/aws/setup-phone.ts +0 -104
- package/src/aws/setup-sms.ts +0 -580
- package/src/aws/sms.ts +0 -1735
- package/src/aws/smtp-server.ts +0 -531
- package/src/aws/sns.ts +0 -758
- package/src/aws/sqs.ts +0 -382
- package/src/aws/ssm.ts +0 -807
- package/src/aws/sts.ts +0 -92
- package/src/aws/support.ts +0 -391
- package/src/aws/test-imap.ts +0 -86
- package/src/aws/textract.ts +0 -780
- package/src/aws/transcribe.ts +0 -108
- package/src/aws/translate.ts +0 -641
- package/src/aws/voice.ts +0 -1379
- package/src/config.ts +0 -35
- package/src/deploy/index.ts +0 -7
- package/src/deploy/static-site-external-dns.ts +0 -906
- package/src/deploy/static-site.ts +0 -1125
- package/src/dns/godaddy.ts +0 -412
- package/src/dns/index.ts +0 -183
- package/src/dns/porkbun.ts +0 -362
- package/src/dns/route53-adapter.ts +0 -414
- package/src/dns/validator.ts +0 -369
- package/src/generators/index.ts +0 -5
- package/src/generators/infrastructure.ts +0 -1660
- package/src/push/apns.ts +0 -452
- package/src/push/fcm.ts +0 -506
- package/src/push/index.ts +0 -58
- package/src/ssl/acme-client.ts +0 -478
- package/src/ssl/index.ts +0 -7
- package/src/ssl/letsencrypt.ts +0 -747
- package/src/types.ts +0 -2
- package/src/utils/cli.ts +0 -398
- package/src/validation/index.ts +0 -5
- package/src/validation/template.ts +0 -405
- package/test/index.test.ts +0 -128
- package/tsconfig.json +0 -18
package/src/aws/voice.ts
DELETED
|
@@ -1,1379 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified Voice Module
|
|
3
|
-
* Provides voice calling and voicemail with S3 storage
|
|
4
|
-
*
|
|
5
|
-
* Similar to the Email and SMS modules, this provides:
|
|
6
|
-
* - Making outbound calls via Connect
|
|
7
|
-
* - Receiving voicemails stored in S3
|
|
8
|
-
* - Voicemail management (list, read, delete)
|
|
9
|
-
* - Call recording storage
|
|
10
|
-
* - Text-to-speech for voice messages
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { ConnectClient } from './connect'
|
|
14
|
-
import { S3Client } from './s3'
|
|
15
|
-
import { TranscribeClient } from './transcribe'
|
|
16
|
-
|
|
17
|
-
export interface VoiceClientConfig {
|
|
18
|
-
region?: string
|
|
19
|
-
// S3 bucket for storing voicemails and recordings
|
|
20
|
-
voicemailBucket?: string
|
|
21
|
-
voicemailPrefix?: string
|
|
22
|
-
recordingsPrefix?: string
|
|
23
|
-
// Connect instance for full call center features
|
|
24
|
-
connectInstanceId?: string
|
|
25
|
-
connectContactFlowId?: string
|
|
26
|
-
// Default caller ID
|
|
27
|
-
defaultCallerId?: string
|
|
28
|
-
// Enable automatic transcription of voicemails
|
|
29
|
-
enableTranscription?: boolean
|
|
30
|
-
// Language for transcription
|
|
31
|
-
transcriptionLanguage?: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface Voicemail {
|
|
35
|
-
key: string
|
|
36
|
-
from: string
|
|
37
|
-
to: string
|
|
38
|
-
duration: number
|
|
39
|
-
timestamp: Date
|
|
40
|
-
transcription?: string
|
|
41
|
-
transcriptionStatus?: 'pending' | 'processing' | 'completed' | 'failed'
|
|
42
|
-
transcriptionJobName?: string
|
|
43
|
-
audioUrl?: string
|
|
44
|
-
// Read/unread status
|
|
45
|
-
read?: boolean
|
|
46
|
-
readAt?: Date
|
|
47
|
-
// Recording file info
|
|
48
|
-
contentType?: string
|
|
49
|
-
size?: number
|
|
50
|
-
// Raw metadata
|
|
51
|
-
raw?: any
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface CallRecording {
|
|
55
|
-
key: string
|
|
56
|
-
contactId: string
|
|
57
|
-
from?: string
|
|
58
|
-
to?: string
|
|
59
|
-
duration: number
|
|
60
|
-
timestamp: Date
|
|
61
|
-
audioUrl?: string
|
|
62
|
-
contentType?: string
|
|
63
|
-
size?: number
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface MakeCallOptions {
|
|
67
|
-
to: string
|
|
68
|
-
from?: string
|
|
69
|
-
// Text message to speak (TTS)
|
|
70
|
-
message?: string
|
|
71
|
-
// Voice for TTS
|
|
72
|
-
voiceId?: string
|
|
73
|
-
// Audio URL to play
|
|
74
|
-
audioUrl?: string
|
|
75
|
-
// Connect-specific options
|
|
76
|
-
contactFlowId?: string
|
|
77
|
-
attributes?: Record<string, string>
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface SendVoiceMessageOptions {
|
|
81
|
-
to: string
|
|
82
|
-
from?: string
|
|
83
|
-
message: string
|
|
84
|
-
voiceId?: string
|
|
85
|
-
languageCode?: string
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface VoicemailGreeting {
|
|
89
|
-
id: string
|
|
90
|
-
name: string
|
|
91
|
-
type: 'default' | 'busy' | 'unavailable' | 'custom'
|
|
92
|
-
// Text for TTS or audio file key
|
|
93
|
-
text?: string
|
|
94
|
-
audioKey?: string
|
|
95
|
-
audioUrl?: string
|
|
96
|
-
voiceId?: string
|
|
97
|
-
languageCode?: string
|
|
98
|
-
isActive: boolean
|
|
99
|
-
createdAt: Date
|
|
100
|
-
updatedAt?: Date
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface CallForwardingRule {
|
|
104
|
-
id: string
|
|
105
|
-
name: string
|
|
106
|
-
enabled: boolean
|
|
107
|
-
// When to forward
|
|
108
|
-
condition: 'always' | 'busy' | 'no_answer' | 'unreachable' | 'after_hours'
|
|
109
|
-
// Where to forward
|
|
110
|
-
forwardTo: string
|
|
111
|
-
// After how many seconds (for no_answer)
|
|
112
|
-
ringTimeout?: number
|
|
113
|
-
// Business hours (for after_hours condition)
|
|
114
|
-
businessHours?: {
|
|
115
|
-
timezone: string
|
|
116
|
-
schedule: Array<{
|
|
117
|
-
day: 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun'
|
|
118
|
-
start: string // HH:MM format
|
|
119
|
-
end: string
|
|
120
|
-
}>
|
|
121
|
-
}
|
|
122
|
-
// Order of priority (lower = higher priority)
|
|
123
|
-
priority: number
|
|
124
|
-
createdAt: Date
|
|
125
|
-
updatedAt?: Date
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Voice Client with S3 voicemail storage
|
|
130
|
-
*/
|
|
131
|
-
export class VoiceClient {
|
|
132
|
-
private config: VoiceClientConfig
|
|
133
|
-
private connect?: ConnectClient
|
|
134
|
-
private s3?: S3Client
|
|
135
|
-
private transcribe?: TranscribeClient
|
|
136
|
-
|
|
137
|
-
constructor(config: VoiceClientConfig = {}) {
|
|
138
|
-
this.config = {
|
|
139
|
-
region: 'us-east-1',
|
|
140
|
-
voicemailPrefix: 'voicemail/',
|
|
141
|
-
recordingsPrefix: 'recordings/',
|
|
142
|
-
transcriptionLanguage: 'en-US',
|
|
143
|
-
...config,
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (this.config.connectInstanceId) {
|
|
147
|
-
this.connect = new ConnectClient(this.config.region!)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (this.config.voicemailBucket) {
|
|
151
|
-
this.s3 = new S3Client(this.config.region!)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (this.config.enableTranscription) {
|
|
155
|
-
this.transcribe = new TranscribeClient(this.config.region!)
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ============================================
|
|
160
|
-
// Making Calls
|
|
161
|
-
// ============================================
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Make an outbound voice call via Connect
|
|
165
|
-
*/
|
|
166
|
-
async call(options: MakeCallOptions): Promise<{ callId: string }> {
|
|
167
|
-
if (!this.connect || !this.config.connectInstanceId) {
|
|
168
|
-
throw new Error('Connect instance ID required for voice calls. Set connectInstanceId in config.')
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const from = options.from || this.config.defaultCallerId
|
|
172
|
-
const contactFlowId = options.contactFlowId || this.config.connectContactFlowId
|
|
173
|
-
|
|
174
|
-
if (!contactFlowId) {
|
|
175
|
-
throw new Error('Contact flow ID required for Connect calls')
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const result = await this.connect.makeCall({
|
|
179
|
-
instanceId: this.config.connectInstanceId,
|
|
180
|
-
contactFlowId,
|
|
181
|
-
to: options.to,
|
|
182
|
-
from,
|
|
183
|
-
attributes: options.attributes,
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
return { callId: result.ContactId || '' }
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Send a voice message (one-way TTS call) via Connect
|
|
191
|
-
*
|
|
192
|
-
* Note: This requires a Contact Flow configured for TTS playback.
|
|
193
|
-
* The message is passed as an attribute that the Contact Flow can use.
|
|
194
|
-
*/
|
|
195
|
-
async sendVoiceMessage(options: SendVoiceMessageOptions): Promise<{ messageId: string }> {
|
|
196
|
-
const result = await this.call({
|
|
197
|
-
to: options.to,
|
|
198
|
-
from: options.from,
|
|
199
|
-
attributes: {
|
|
200
|
-
message: options.message,
|
|
201
|
-
voiceId: options.voiceId || 'Joanna',
|
|
202
|
-
languageCode: options.languageCode || 'en-US',
|
|
203
|
-
},
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
return { messageId: result.callId }
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Send a TTS voice message (alias for sendVoiceMessage)
|
|
211
|
-
*/
|
|
212
|
-
async speak(to: string, message: string, voiceId?: string): Promise<{ messageId: string }> {
|
|
213
|
-
return this.sendVoiceMessage({ to, message, voiceId })
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ============================================
|
|
217
|
-
// Voicemail Management (S3 Storage)
|
|
218
|
-
// ============================================
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Get voicemails from S3
|
|
222
|
-
*/
|
|
223
|
-
async getVoicemails(options: {
|
|
224
|
-
prefix?: string
|
|
225
|
-
maxResults?: number
|
|
226
|
-
} = {}): Promise<Voicemail[]> {
|
|
227
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
228
|
-
throw new Error('Voicemail bucket not configured')
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const prefix = options.prefix || this.config.voicemailPrefix || 'voicemail/'
|
|
232
|
-
const objects = await this.s3.list({
|
|
233
|
-
bucket: this.config.voicemailBucket,
|
|
234
|
-
prefix,
|
|
235
|
-
maxKeys: options.maxResults || 100,
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
const voicemails: Voicemail[] = []
|
|
239
|
-
|
|
240
|
-
for (const obj of objects || []) {
|
|
241
|
-
if (!obj.Key) continue
|
|
242
|
-
|
|
243
|
-
// Skip non-audio files (metadata files, etc.)
|
|
244
|
-
if (obj.Key.endsWith('.json')) {
|
|
245
|
-
continue
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
const voicemail = await this.getVoicemailMetadata(obj.Key)
|
|
250
|
-
if (voicemail) {
|
|
251
|
-
voicemails.push({
|
|
252
|
-
...voicemail,
|
|
253
|
-
size: obj.Size,
|
|
254
|
-
})
|
|
255
|
-
}
|
|
256
|
-
} catch (err) {
|
|
257
|
-
console.error(`Failed to read voicemail ${obj.Key}:`, err)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Sort by timestamp descending (newest first)
|
|
262
|
-
return voicemails.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Get a specific voicemail
|
|
267
|
-
*/
|
|
268
|
-
async getVoicemail(key: string): Promise<Voicemail | null> {
|
|
269
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
270
|
-
throw new Error('Voicemail bucket not configured')
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return this.getVoicemailMetadata(key)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Get voicemail audio as a signed URL
|
|
278
|
-
*/
|
|
279
|
-
async getVoicemailAudioUrl(key: string, expiresIn: number = 3600): Promise<string> {
|
|
280
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
281
|
-
throw new Error('Voicemail bucket not configured')
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Generate a presigned URL for the audio file
|
|
285
|
-
return this.s3.getSignedUrl({
|
|
286
|
-
bucket: this.config.voicemailBucket,
|
|
287
|
-
key,
|
|
288
|
-
expiresIn,
|
|
289
|
-
})
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Delete a voicemail
|
|
294
|
-
*/
|
|
295
|
-
async deleteVoicemail(key: string): Promise<void> {
|
|
296
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
297
|
-
throw new Error('Voicemail bucket not configured')
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Delete audio file
|
|
301
|
-
await this.s3.deleteObject(this.config.voicemailBucket, key)
|
|
302
|
-
|
|
303
|
-
// Delete metadata file if exists
|
|
304
|
-
const metadataKey = key.replace(/\.[^/.]+$/, '.json')
|
|
305
|
-
try {
|
|
306
|
-
await this.s3.deleteObject(this.config.voicemailBucket, metadataKey)
|
|
307
|
-
} catch {
|
|
308
|
-
// Metadata might not exist
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Archive a voicemail
|
|
314
|
-
*/
|
|
315
|
-
async archiveVoicemail(key: string): Promise<string> {
|
|
316
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
317
|
-
throw new Error('Voicemail bucket not configured')
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const filename = key.split('/').pop() || `${Date.now()}.wav`
|
|
321
|
-
const newKey = `voicemail/archive/${filename}`
|
|
322
|
-
|
|
323
|
-
// Copy to archive
|
|
324
|
-
await this.s3.copyObject({
|
|
325
|
-
sourceBucket: this.config.voicemailBucket,
|
|
326
|
-
sourceKey: key,
|
|
327
|
-
destinationBucket: this.config.voicemailBucket,
|
|
328
|
-
destinationKey: newKey,
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
// Delete original
|
|
332
|
-
await this.s3.deleteObject(this.config.voicemailBucket, key)
|
|
333
|
-
|
|
334
|
-
return newKey
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Get voicemail count
|
|
339
|
-
*/
|
|
340
|
-
async getVoicemailCount(): Promise<number> {
|
|
341
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
342
|
-
throw new Error('Voicemail bucket not configured')
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const objects = await this.s3.list({
|
|
346
|
-
bucket: this.config.voicemailBucket,
|
|
347
|
-
prefix: this.config.voicemailPrefix || 'voicemail/',
|
|
348
|
-
maxKeys: 1000,
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
// Count only audio files, not metadata
|
|
352
|
-
return (objects || []).filter(obj =>
|
|
353
|
-
obj.Key && !obj.Key.endsWith('.json') && !obj.Key.endsWith('/')
|
|
354
|
-
).length
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Get unread voicemail count
|
|
359
|
-
*/
|
|
360
|
-
async getUnreadCount(): Promise<number> {
|
|
361
|
-
const voicemails = await this.getVoicemails({ maxResults: 1000 })
|
|
362
|
-
return voicemails.filter(v => !v.read).length
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Mark a voicemail as read
|
|
367
|
-
*/
|
|
368
|
-
async markAsRead(key: string): Promise<void> {
|
|
369
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
370
|
-
throw new Error('Voicemail bucket not configured')
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const metadataKey = key.replace(/\.[^/.]+$/, '.json')
|
|
374
|
-
try {
|
|
375
|
-
const content = await this.s3.getObject(this.config.voicemailBucket, metadataKey)
|
|
376
|
-
const data = JSON.parse(content)
|
|
377
|
-
data.read = true
|
|
378
|
-
data.readAt = new Date().toISOString()
|
|
379
|
-
|
|
380
|
-
await this.s3.putObject({
|
|
381
|
-
bucket: this.config.voicemailBucket,
|
|
382
|
-
key: metadataKey,
|
|
383
|
-
body: JSON.stringify(data, null, 2),
|
|
384
|
-
contentType: 'application/json',
|
|
385
|
-
})
|
|
386
|
-
} catch {
|
|
387
|
-
// Create metadata if it doesn't exist
|
|
388
|
-
await this.s3.putObject({
|
|
389
|
-
bucket: this.config.voicemailBucket,
|
|
390
|
-
key: metadataKey,
|
|
391
|
-
body: JSON.stringify({ read: true, readAt: new Date().toISOString() }, null, 2),
|
|
392
|
-
contentType: 'application/json',
|
|
393
|
-
})
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Mark a voicemail as unread
|
|
399
|
-
*/
|
|
400
|
-
async markAsUnread(key: string): Promise<void> {
|
|
401
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
402
|
-
throw new Error('Voicemail bucket not configured')
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const metadataKey = key.replace(/\.[^/.]+$/, '.json')
|
|
406
|
-
try {
|
|
407
|
-
const content = await this.s3.getObject(this.config.voicemailBucket, metadataKey)
|
|
408
|
-
const data = JSON.parse(content)
|
|
409
|
-
data.read = false
|
|
410
|
-
delete data.readAt
|
|
411
|
-
|
|
412
|
-
await this.s3.putObject({
|
|
413
|
-
bucket: this.config.voicemailBucket,
|
|
414
|
-
key: metadataKey,
|
|
415
|
-
body: JSON.stringify(data, null, 2),
|
|
416
|
-
contentType: 'application/json',
|
|
417
|
-
})
|
|
418
|
-
} catch {
|
|
419
|
-
// Ignore if metadata doesn't exist
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Batch mark voicemails as read
|
|
425
|
-
*/
|
|
426
|
-
async markManyAsRead(keys: string[]): Promise<void> {
|
|
427
|
-
await Promise.all(keys.map(key => this.markAsRead(key)))
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Batch delete voicemails
|
|
432
|
-
*/
|
|
433
|
-
async deleteMany(keys: string[]): Promise<void> {
|
|
434
|
-
await Promise.all(keys.map(key => this.deleteVoicemail(key)))
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// ============================================
|
|
438
|
-
// Transcription
|
|
439
|
-
// ============================================
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Start transcription for a voicemail
|
|
443
|
-
*/
|
|
444
|
-
async transcribeVoicemail(key: string): Promise<{ jobName: string }> {
|
|
445
|
-
if (!this.transcribe) {
|
|
446
|
-
throw new Error('Transcription not enabled. Set enableTranscription: true in config')
|
|
447
|
-
}
|
|
448
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
449
|
-
throw new Error('Voicemail bucket not configured')
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const jobName = `voicemail-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
453
|
-
const mediaUri = `s3://${this.config.voicemailBucket}/${key}`
|
|
454
|
-
|
|
455
|
-
// Determine media format from extension
|
|
456
|
-
const ext = key.split('.').pop()?.toLowerCase()
|
|
457
|
-
const mediaFormat = ext === 'mp3' ? 'mp3' : ext === 'wav' ? 'wav' : 'wav'
|
|
458
|
-
|
|
459
|
-
await this.transcribe.startTranscriptionJob({
|
|
460
|
-
TranscriptionJobName: jobName,
|
|
461
|
-
LanguageCode: this.config.transcriptionLanguage || 'en-US',
|
|
462
|
-
Media: { MediaFileUri: mediaUri },
|
|
463
|
-
MediaFormat: mediaFormat,
|
|
464
|
-
OutputBucketName: this.config.voicemailBucket,
|
|
465
|
-
OutputKey: key.replace(/\.[^/.]+$/, '-transcript.json'),
|
|
466
|
-
})
|
|
467
|
-
|
|
468
|
-
// Update metadata with job info
|
|
469
|
-
const metadataKey = key.replace(/\.[^/.]+$/, '.json')
|
|
470
|
-
try {
|
|
471
|
-
const content = await this.s3.getObject(this.config.voicemailBucket, metadataKey)
|
|
472
|
-
const data = JSON.parse(content)
|
|
473
|
-
data.transcriptionJobName = jobName
|
|
474
|
-
data.transcriptionStatus = 'processing'
|
|
475
|
-
|
|
476
|
-
await this.s3.putObject({
|
|
477
|
-
bucket: this.config.voicemailBucket,
|
|
478
|
-
key: metadataKey,
|
|
479
|
-
body: JSON.stringify(data, null, 2),
|
|
480
|
-
contentType: 'application/json',
|
|
481
|
-
})
|
|
482
|
-
} catch {
|
|
483
|
-
// Create metadata if it doesn't exist
|
|
484
|
-
await this.s3.putObject({
|
|
485
|
-
bucket: this.config.voicemailBucket,
|
|
486
|
-
key: metadataKey,
|
|
487
|
-
body: JSON.stringify({
|
|
488
|
-
transcriptionJobName: jobName,
|
|
489
|
-
transcriptionStatus: 'processing',
|
|
490
|
-
}, null, 2),
|
|
491
|
-
contentType: 'application/json',
|
|
492
|
-
})
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
return { jobName }
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Get transcription status for a voicemail
|
|
500
|
-
*/
|
|
501
|
-
async getTranscriptionStatus(jobName: string): Promise<{
|
|
502
|
-
status: 'IN_PROGRESS' | 'COMPLETED' | 'FAILED'
|
|
503
|
-
transcription?: string
|
|
504
|
-
}> {
|
|
505
|
-
if (!this.transcribe) {
|
|
506
|
-
throw new Error('Transcription not enabled')
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const job = await this.transcribe.getTranscriptionJob({ TranscriptionJobName: jobName })
|
|
510
|
-
const status = job.TranscriptionJob?.TranscriptionJobStatus
|
|
511
|
-
|
|
512
|
-
if (status === 'COMPLETED' && job.TranscriptionJob?.Transcript?.TranscriptFileUri) {
|
|
513
|
-
// Fetch the transcript from S3
|
|
514
|
-
const transcriptUri = job.TranscriptionJob.Transcript.TranscriptFileUri
|
|
515
|
-
// The URI is like s3://bucket/key or https://s3.region.amazonaws.com/bucket/key
|
|
516
|
-
// We need to extract the key
|
|
517
|
-
try {
|
|
518
|
-
if (this.s3 && this.config.voicemailBucket && transcriptUri.includes(this.config.voicemailBucket)) {
|
|
519
|
-
const key = transcriptUri.split(this.config.voicemailBucket + '/')[1]
|
|
520
|
-
if (key) {
|
|
521
|
-
const content = await this.s3.getObject(this.config.voicemailBucket, key)
|
|
522
|
-
const transcript = JSON.parse(content)
|
|
523
|
-
return {
|
|
524
|
-
status: 'COMPLETED',
|
|
525
|
-
transcription: transcript.results?.transcripts?.[0]?.transcript || '',
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
} catch {
|
|
530
|
-
// Could not fetch transcript
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
return { status: status as 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' }
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* Update voicemail metadata with completed transcription
|
|
539
|
-
*/
|
|
540
|
-
async updateTranscription(key: string, transcription: string): Promise<void> {
|
|
541
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
542
|
-
throw new Error('Voicemail bucket not configured')
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const metadataKey = key.replace(/\.[^/.]+$/, '.json')
|
|
546
|
-
try {
|
|
547
|
-
const content = await this.s3.getObject(this.config.voicemailBucket, metadataKey)
|
|
548
|
-
const data = JSON.parse(content)
|
|
549
|
-
data.transcription = transcription
|
|
550
|
-
data.transcriptionStatus = 'completed'
|
|
551
|
-
|
|
552
|
-
await this.s3.putObject({
|
|
553
|
-
bucket: this.config.voicemailBucket,
|
|
554
|
-
key: metadataKey,
|
|
555
|
-
body: JSON.stringify(data, null, 2),
|
|
556
|
-
contentType: 'application/json',
|
|
557
|
-
})
|
|
558
|
-
} catch {
|
|
559
|
-
await this.s3.putObject({
|
|
560
|
-
bucket: this.config.voicemailBucket,
|
|
561
|
-
key: metadataKey,
|
|
562
|
-
body: JSON.stringify({
|
|
563
|
-
transcription,
|
|
564
|
-
transcriptionStatus: 'completed',
|
|
565
|
-
}, null, 2),
|
|
566
|
-
contentType: 'application/json',
|
|
567
|
-
})
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// ============================================
|
|
572
|
-
// Voicemail Greetings
|
|
573
|
-
// ============================================
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Create a voicemail greeting
|
|
577
|
-
*/
|
|
578
|
-
async createGreeting(greeting: {
|
|
579
|
-
name: string
|
|
580
|
-
type: 'default' | 'busy' | 'unavailable' | 'custom'
|
|
581
|
-
text?: string
|
|
582
|
-
audioData?: Buffer | string
|
|
583
|
-
voiceId?: string
|
|
584
|
-
languageCode?: string
|
|
585
|
-
setActive?: boolean
|
|
586
|
-
}): Promise<VoicemailGreeting> {
|
|
587
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
588
|
-
throw new Error('Voicemail bucket not configured')
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
592
|
-
let audioKey: string | undefined
|
|
593
|
-
|
|
594
|
-
// If audio data provided, store it
|
|
595
|
-
if (greeting.audioData) {
|
|
596
|
-
audioKey = `greetings/${id}.wav`
|
|
597
|
-
await this.s3.putObject({
|
|
598
|
-
bucket: this.config.voicemailBucket,
|
|
599
|
-
key: audioKey,
|
|
600
|
-
body: greeting.audioData,
|
|
601
|
-
contentType: 'audio/wav',
|
|
602
|
-
})
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const newGreeting: VoicemailGreeting = {
|
|
606
|
-
id,
|
|
607
|
-
name: greeting.name,
|
|
608
|
-
type: greeting.type,
|
|
609
|
-
text: greeting.text,
|
|
610
|
-
audioKey,
|
|
611
|
-
voiceId: greeting.voiceId || 'Joanna',
|
|
612
|
-
languageCode: greeting.languageCode || 'en-US',
|
|
613
|
-
isActive: greeting.setActive || false,
|
|
614
|
-
createdAt: new Date(),
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// If setting as active, deactivate other greetings of same type
|
|
618
|
-
if (greeting.setActive) {
|
|
619
|
-
await this.deactivateGreetingsOfType(greeting.type)
|
|
620
|
-
newGreeting.isActive = true
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// Store metadata
|
|
624
|
-
await this.s3.putObject({
|
|
625
|
-
bucket: this.config.voicemailBucket,
|
|
626
|
-
key: `greetings/${id}.json`,
|
|
627
|
-
body: JSON.stringify(newGreeting, null, 2),
|
|
628
|
-
contentType: 'application/json',
|
|
629
|
-
})
|
|
630
|
-
|
|
631
|
-
return newGreeting
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Get all voicemail greetings
|
|
636
|
-
*/
|
|
637
|
-
async getGreetings(): Promise<VoicemailGreeting[]> {
|
|
638
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
639
|
-
throw new Error('Voicemail bucket not configured')
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const objects = await this.s3.list({
|
|
643
|
-
bucket: this.config.voicemailBucket,
|
|
644
|
-
prefix: 'greetings/',
|
|
645
|
-
maxKeys: 100,
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
const greetings: VoicemailGreeting[] = []
|
|
649
|
-
for (const obj of objects || []) {
|
|
650
|
-
if (!obj.Key || !obj.Key.endsWith('.json')) continue
|
|
651
|
-
try {
|
|
652
|
-
const content = await this.s3.getObject(this.config.voicemailBucket, obj.Key)
|
|
653
|
-
const greeting = JSON.parse(content) as VoicemailGreeting
|
|
654
|
-
greeting.createdAt = new Date(greeting.createdAt)
|
|
655
|
-
if (greeting.updatedAt) greeting.updatedAt = new Date(greeting.updatedAt)
|
|
656
|
-
greetings.push(greeting)
|
|
657
|
-
} catch {
|
|
658
|
-
// Skip invalid
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
return greetings.sort((a, b) => a.name.localeCompare(b.name))
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
/**
|
|
666
|
-
* Get a specific greeting
|
|
667
|
-
*/
|
|
668
|
-
async getGreeting(id: string): Promise<VoicemailGreeting | null> {
|
|
669
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
670
|
-
return null
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
try {
|
|
674
|
-
const content = await this.s3.getObject(this.config.voicemailBucket, `greetings/${id}.json`)
|
|
675
|
-
const greeting = JSON.parse(content) as VoicemailGreeting
|
|
676
|
-
greeting.createdAt = new Date(greeting.createdAt)
|
|
677
|
-
if (greeting.updatedAt) greeting.updatedAt = new Date(greeting.updatedAt)
|
|
678
|
-
return greeting
|
|
679
|
-
} catch {
|
|
680
|
-
return null
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
* Get the active greeting of a specific type
|
|
686
|
-
*/
|
|
687
|
-
async getActiveGreeting(type: 'default' | 'busy' | 'unavailable' | 'custom'): Promise<VoicemailGreeting | null> {
|
|
688
|
-
const greetings = await this.getGreetings()
|
|
689
|
-
return greetings.find(g => g.type === type && g.isActive) || null
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* Set a greeting as active
|
|
694
|
-
*/
|
|
695
|
-
async setActiveGreeting(id: string): Promise<void> {
|
|
696
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
697
|
-
throw new Error('Voicemail bucket not configured')
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const greeting = await this.getGreeting(id)
|
|
701
|
-
if (!greeting) throw new Error(`Greeting ${id} not found`)
|
|
702
|
-
|
|
703
|
-
// Deactivate other greetings of same type
|
|
704
|
-
await this.deactivateGreetingsOfType(greeting.type)
|
|
705
|
-
|
|
706
|
-
// Activate this greeting
|
|
707
|
-
greeting.isActive = true
|
|
708
|
-
greeting.updatedAt = new Date()
|
|
709
|
-
|
|
710
|
-
await this.s3.putObject({
|
|
711
|
-
bucket: this.config.voicemailBucket,
|
|
712
|
-
key: `greetings/${id}.json`,
|
|
713
|
-
body: JSON.stringify(greeting, null, 2),
|
|
714
|
-
contentType: 'application/json',
|
|
715
|
-
})
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* Deactivate all greetings of a type
|
|
720
|
-
*/
|
|
721
|
-
private async deactivateGreetingsOfType(type: string): Promise<void> {
|
|
722
|
-
const greetings = await this.getGreetings()
|
|
723
|
-
for (const g of greetings) {
|
|
724
|
-
if (g.type === type && g.isActive) {
|
|
725
|
-
g.isActive = false
|
|
726
|
-
g.updatedAt = new Date()
|
|
727
|
-
await this.s3!.putObject({
|
|
728
|
-
bucket: this.config.voicemailBucket!,
|
|
729
|
-
key: `greetings/${g.id}.json`,
|
|
730
|
-
body: JSON.stringify(g, null, 2),
|
|
731
|
-
contentType: 'application/json',
|
|
732
|
-
})
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
/**
|
|
738
|
-
* Update a greeting
|
|
739
|
-
*/
|
|
740
|
-
async updateGreeting(
|
|
741
|
-
id: string,
|
|
742
|
-
updates: { name?: string; text?: string; audioData?: Buffer | string },
|
|
743
|
-
): Promise<VoicemailGreeting> {
|
|
744
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
745
|
-
throw new Error('Voicemail bucket not configured')
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const greeting = await this.getGreeting(id)
|
|
749
|
-
if (!greeting) throw new Error(`Greeting ${id} not found`)
|
|
750
|
-
|
|
751
|
-
if (updates.name) greeting.name = updates.name
|
|
752
|
-
if (updates.text) greeting.text = updates.text
|
|
753
|
-
if (updates.audioData) {
|
|
754
|
-
const audioKey = `greetings/${id}.wav`
|
|
755
|
-
await this.s3.putObject({
|
|
756
|
-
bucket: this.config.voicemailBucket,
|
|
757
|
-
key: audioKey,
|
|
758
|
-
body: updates.audioData,
|
|
759
|
-
contentType: 'audio/wav',
|
|
760
|
-
})
|
|
761
|
-
greeting.audioKey = audioKey
|
|
762
|
-
}
|
|
763
|
-
greeting.updatedAt = new Date()
|
|
764
|
-
|
|
765
|
-
await this.s3.putObject({
|
|
766
|
-
bucket: this.config.voicemailBucket,
|
|
767
|
-
key: `greetings/${id}.json`,
|
|
768
|
-
body: JSON.stringify(greeting, null, 2),
|
|
769
|
-
contentType: 'application/json',
|
|
770
|
-
})
|
|
771
|
-
|
|
772
|
-
return greeting
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* Delete a greeting
|
|
777
|
-
*/
|
|
778
|
-
async deleteGreeting(id: string): Promise<void> {
|
|
779
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
780
|
-
throw new Error('Voicemail bucket not configured')
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
const greeting = await this.getGreeting(id)
|
|
784
|
-
if (greeting?.audioKey) {
|
|
785
|
-
try {
|
|
786
|
-
await this.s3.deleteObject(this.config.voicemailBucket, greeting.audioKey)
|
|
787
|
-
} catch {
|
|
788
|
-
// Audio may not exist
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
await this.s3.deleteObject(this.config.voicemailBucket, `greetings/${id}.json`)
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Get a signed URL for a greeting audio
|
|
797
|
-
*/
|
|
798
|
-
async getGreetingAudioUrl(id: string, expiresIn: number = 3600): Promise<string | null> {
|
|
799
|
-
const greeting = await this.getGreeting(id)
|
|
800
|
-
if (!greeting?.audioKey || !this.s3 || !this.config.voicemailBucket) {
|
|
801
|
-
return null
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
return this.s3.getSignedUrl({
|
|
805
|
-
bucket: this.config.voicemailBucket,
|
|
806
|
-
key: greeting.audioKey,
|
|
807
|
-
expiresIn,
|
|
808
|
-
})
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// ============================================
|
|
812
|
-
// Call Forwarding Rules
|
|
813
|
-
// ============================================
|
|
814
|
-
|
|
815
|
-
/**
|
|
816
|
-
* Create a call forwarding rule
|
|
817
|
-
*/
|
|
818
|
-
async createForwardingRule(rule: {
|
|
819
|
-
name: string
|
|
820
|
-
condition: 'always' | 'busy' | 'no_answer' | 'unreachable' | 'after_hours'
|
|
821
|
-
forwardTo: string
|
|
822
|
-
ringTimeout?: number
|
|
823
|
-
businessHours?: CallForwardingRule['businessHours']
|
|
824
|
-
priority?: number
|
|
825
|
-
enabled?: boolean
|
|
826
|
-
}): Promise<CallForwardingRule> {
|
|
827
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
828
|
-
throw new Error('Voicemail bucket not configured')
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
832
|
-
|
|
833
|
-
const newRule: CallForwardingRule = {
|
|
834
|
-
id,
|
|
835
|
-
name: rule.name,
|
|
836
|
-
enabled: rule.enabled !== false,
|
|
837
|
-
condition: rule.condition,
|
|
838
|
-
forwardTo: rule.forwardTo,
|
|
839
|
-
ringTimeout: rule.ringTimeout || 20,
|
|
840
|
-
businessHours: rule.businessHours,
|
|
841
|
-
priority: rule.priority || 100,
|
|
842
|
-
createdAt: new Date(),
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
await this.s3.putObject({
|
|
846
|
-
bucket: this.config.voicemailBucket,
|
|
847
|
-
key: `forwarding/${id}.json`,
|
|
848
|
-
body: JSON.stringify(newRule, null, 2),
|
|
849
|
-
contentType: 'application/json',
|
|
850
|
-
})
|
|
851
|
-
|
|
852
|
-
return newRule
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* Get all forwarding rules
|
|
857
|
-
*/
|
|
858
|
-
async getForwardingRules(): Promise<CallForwardingRule[]> {
|
|
859
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
860
|
-
throw new Error('Voicemail bucket not configured')
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
const objects = await this.s3.list({
|
|
864
|
-
bucket: this.config.voicemailBucket,
|
|
865
|
-
prefix: 'forwarding/',
|
|
866
|
-
maxKeys: 100,
|
|
867
|
-
})
|
|
868
|
-
|
|
869
|
-
const rules: CallForwardingRule[] = []
|
|
870
|
-
for (const obj of objects || []) {
|
|
871
|
-
if (!obj.Key || !obj.Key.endsWith('.json')) continue
|
|
872
|
-
try {
|
|
873
|
-
const content = await this.s3.getObject(this.config.voicemailBucket, obj.Key)
|
|
874
|
-
const rule = JSON.parse(content) as CallForwardingRule
|
|
875
|
-
rule.createdAt = new Date(rule.createdAt)
|
|
876
|
-
if (rule.updatedAt) rule.updatedAt = new Date(rule.updatedAt)
|
|
877
|
-
rules.push(rule)
|
|
878
|
-
} catch {
|
|
879
|
-
// Skip invalid
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
return rules.sort((a, b) => a.priority - b.priority)
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
/**
|
|
887
|
-
* Get a specific forwarding rule
|
|
888
|
-
*/
|
|
889
|
-
async getForwardingRule(id: string): Promise<CallForwardingRule | null> {
|
|
890
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
891
|
-
return null
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
try {
|
|
895
|
-
const content = await this.s3.getObject(this.config.voicemailBucket, `forwarding/${id}.json`)
|
|
896
|
-
const rule = JSON.parse(content) as CallForwardingRule
|
|
897
|
-
rule.createdAt = new Date(rule.createdAt)
|
|
898
|
-
if (rule.updatedAt) rule.updatedAt = new Date(rule.updatedAt)
|
|
899
|
-
return rule
|
|
900
|
-
} catch {
|
|
901
|
-
return null
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* Update a forwarding rule
|
|
907
|
-
*/
|
|
908
|
-
async updateForwardingRule(
|
|
909
|
-
id: string,
|
|
910
|
-
updates: Partial<Omit<CallForwardingRule, 'id' | 'createdAt' | 'updatedAt'>>,
|
|
911
|
-
): Promise<CallForwardingRule> {
|
|
912
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
913
|
-
throw new Error('Voicemail bucket not configured')
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
const rule = await this.getForwardingRule(id)
|
|
917
|
-
if (!rule) throw new Error(`Forwarding rule ${id} not found`)
|
|
918
|
-
|
|
919
|
-
if (updates.name !== undefined) rule.name = updates.name
|
|
920
|
-
if (updates.enabled !== undefined) rule.enabled = updates.enabled
|
|
921
|
-
if (updates.condition !== undefined) rule.condition = updates.condition
|
|
922
|
-
if (updates.forwardTo !== undefined) rule.forwardTo = updates.forwardTo
|
|
923
|
-
if (updates.ringTimeout !== undefined) rule.ringTimeout = updates.ringTimeout
|
|
924
|
-
if (updates.businessHours !== undefined) rule.businessHours = updates.businessHours
|
|
925
|
-
if (updates.priority !== undefined) rule.priority = updates.priority
|
|
926
|
-
rule.updatedAt = new Date()
|
|
927
|
-
|
|
928
|
-
await this.s3.putObject({
|
|
929
|
-
bucket: this.config.voicemailBucket,
|
|
930
|
-
key: `forwarding/${id}.json`,
|
|
931
|
-
body: JSON.stringify(rule, null, 2),
|
|
932
|
-
contentType: 'application/json',
|
|
933
|
-
})
|
|
934
|
-
|
|
935
|
-
return rule
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
/**
|
|
939
|
-
* Enable/disable a forwarding rule
|
|
940
|
-
*/
|
|
941
|
-
async setForwardingRuleEnabled(id: string, enabled: boolean): Promise<void> {
|
|
942
|
-
await this.updateForwardingRule(id, { enabled })
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
/**
|
|
946
|
-
* Delete a forwarding rule
|
|
947
|
-
*/
|
|
948
|
-
async deleteForwardingRule(id: string): Promise<void> {
|
|
949
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
950
|
-
throw new Error('Voicemail bucket not configured')
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
await this.s3.deleteObject(this.config.voicemailBucket, `forwarding/${id}.json`)
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
/**
|
|
957
|
-
* Get the applicable forwarding rule for current conditions
|
|
958
|
-
* Returns the highest priority enabled rule that matches current conditions
|
|
959
|
-
*/
|
|
960
|
-
async getApplicableForwardingRule(
|
|
961
|
-
currentConditions: {
|
|
962
|
-
isBusy?: boolean
|
|
963
|
-
isUnreachable?: boolean
|
|
964
|
-
noAnswer?: boolean
|
|
965
|
-
} = {},
|
|
966
|
-
): Promise<CallForwardingRule | null> {
|
|
967
|
-
const rules = await this.getForwardingRules()
|
|
968
|
-
const now = new Date()
|
|
969
|
-
|
|
970
|
-
for (const rule of rules) {
|
|
971
|
-
if (!rule.enabled) continue
|
|
972
|
-
|
|
973
|
-
switch (rule.condition) {
|
|
974
|
-
case 'always':
|
|
975
|
-
return rule
|
|
976
|
-
case 'busy':
|
|
977
|
-
if (currentConditions.isBusy) return rule
|
|
978
|
-
break
|
|
979
|
-
case 'no_answer':
|
|
980
|
-
if (currentConditions.noAnswer) return rule
|
|
981
|
-
break
|
|
982
|
-
case 'unreachable':
|
|
983
|
-
if (currentConditions.isUnreachable) return rule
|
|
984
|
-
break
|
|
985
|
-
case 'after_hours':
|
|
986
|
-
if (rule.businessHours && !this.isWithinBusinessHours(now, rule.businessHours)) {
|
|
987
|
-
return rule
|
|
988
|
-
}
|
|
989
|
-
break
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
return null
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
/**
|
|
997
|
-
* Check if a time is within business hours
|
|
998
|
-
*/
|
|
999
|
-
private isWithinBusinessHours(
|
|
1000
|
-
date: Date,
|
|
1001
|
-
businessHours: NonNullable<CallForwardingRule['businessHours']>,
|
|
1002
|
-
): boolean {
|
|
1003
|
-
const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] as const
|
|
1004
|
-
|
|
1005
|
-
// Convert to business timezone if needed (simplified - uses local time)
|
|
1006
|
-
const dayName = days[date.getDay()]
|
|
1007
|
-
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
|
1008
|
-
|
|
1009
|
-
for (const schedule of businessHours.schedule) {
|
|
1010
|
-
if (schedule.day === dayName) {
|
|
1011
|
-
if (timeStr >= schedule.start && timeStr <= schedule.end) {
|
|
1012
|
-
return true
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
return false
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// ============================================
|
|
1021
|
-
// Call Recordings (Connect)
|
|
1022
|
-
// ============================================
|
|
1023
|
-
|
|
1024
|
-
/**
|
|
1025
|
-
* Get call recordings from S3
|
|
1026
|
-
*/
|
|
1027
|
-
async getRecordings(options: {
|
|
1028
|
-
prefix?: string
|
|
1029
|
-
maxResults?: number
|
|
1030
|
-
} = {}): Promise<CallRecording[]> {
|
|
1031
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
1032
|
-
throw new Error('Recordings bucket not configured')
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
const prefix = options.prefix || this.config.recordingsPrefix || 'recordings/'
|
|
1036
|
-
const objects = await this.s3.list({
|
|
1037
|
-
bucket: this.config.voicemailBucket,
|
|
1038
|
-
prefix,
|
|
1039
|
-
maxKeys: options.maxResults || 100,
|
|
1040
|
-
})
|
|
1041
|
-
|
|
1042
|
-
const recordings: CallRecording[] = []
|
|
1043
|
-
|
|
1044
|
-
for (const obj of objects || []) {
|
|
1045
|
-
if (!obj.Key || obj.Key.endsWith('.json') || obj.Key.endsWith('/')) continue
|
|
1046
|
-
|
|
1047
|
-
try {
|
|
1048
|
-
// Try to get metadata
|
|
1049
|
-
const metadataKey = obj.Key.replace(/\.[^/.]+$/, '.json')
|
|
1050
|
-
let metadata: any = {}
|
|
1051
|
-
|
|
1052
|
-
try {
|
|
1053
|
-
const metadataContent = await this.s3.getObject(this.config.voicemailBucket!, metadataKey)
|
|
1054
|
-
metadata = JSON.parse(metadataContent)
|
|
1055
|
-
} catch {
|
|
1056
|
-
// No metadata file
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
recordings.push({
|
|
1060
|
-
key: obj.Key,
|
|
1061
|
-
contactId: metadata.contactId || obj.Key.split('/').pop()?.split('.')[0] || '',
|
|
1062
|
-
from: metadata.from,
|
|
1063
|
-
to: metadata.to,
|
|
1064
|
-
duration: metadata.duration || 0,
|
|
1065
|
-
timestamp: new Date(obj.LastModified || Date.now()),
|
|
1066
|
-
contentType: 'audio/wav',
|
|
1067
|
-
size: obj.Size,
|
|
1068
|
-
})
|
|
1069
|
-
} catch (err) {
|
|
1070
|
-
console.error(`Failed to read recording ${obj.Key}:`, err)
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
return recordings.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
/**
|
|
1078
|
-
* Get a recording audio URL
|
|
1079
|
-
*/
|
|
1080
|
-
async getRecordingUrl(key: string, expiresIn: number = 3600): Promise<string> {
|
|
1081
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
1082
|
-
throw new Error('Recordings bucket not configured')
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
return this.s3.getSignedUrl({
|
|
1086
|
-
bucket: this.config.voicemailBucket,
|
|
1087
|
-
key,
|
|
1088
|
-
expiresIn,
|
|
1089
|
-
})
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
// ============================================
|
|
1093
|
-
// Voicemail Ingestion
|
|
1094
|
-
// ============================================
|
|
1095
|
-
|
|
1096
|
-
/**
|
|
1097
|
-
* Store an incoming voicemail to S3
|
|
1098
|
-
* This is typically called from a Lambda handler
|
|
1099
|
-
*/
|
|
1100
|
-
async storeVoicemail(voicemail: {
|
|
1101
|
-
from: string
|
|
1102
|
-
to: string
|
|
1103
|
-
audioData: Buffer | string
|
|
1104
|
-
duration?: number
|
|
1105
|
-
transcription?: string
|
|
1106
|
-
contentType?: string
|
|
1107
|
-
timestamp?: Date
|
|
1108
|
-
}): Promise<string> {
|
|
1109
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
1110
|
-
throw new Error('Voicemail bucket not configured')
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
const timestamp = voicemail.timestamp || new Date()
|
|
1114
|
-
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
1115
|
-
const ext = voicemail.contentType === 'audio/mp3' ? 'mp3' : 'wav'
|
|
1116
|
-
const audioKey = `${this.config.voicemailPrefix}${timestamp.toISOString().split('T')[0]}/${id}.${ext}`
|
|
1117
|
-
const metadataKey = `${this.config.voicemailPrefix}${timestamp.toISOString().split('T')[0]}/${id}.json`
|
|
1118
|
-
|
|
1119
|
-
// Store audio file
|
|
1120
|
-
await this.s3.putObject({
|
|
1121
|
-
bucket: this.config.voicemailBucket,
|
|
1122
|
-
key: audioKey,
|
|
1123
|
-
body: voicemail.audioData,
|
|
1124
|
-
contentType: voicemail.contentType || 'audio/wav',
|
|
1125
|
-
})
|
|
1126
|
-
|
|
1127
|
-
// Store metadata
|
|
1128
|
-
const metadata = {
|
|
1129
|
-
from: voicemail.from,
|
|
1130
|
-
to: voicemail.to,
|
|
1131
|
-
duration: voicemail.duration || 0,
|
|
1132
|
-
transcription: voicemail.transcription,
|
|
1133
|
-
timestamp: timestamp.toISOString(),
|
|
1134
|
-
audioKey,
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
await this.s3.putObject({
|
|
1138
|
-
bucket: this.config.voicemailBucket,
|
|
1139
|
-
key: metadataKey,
|
|
1140
|
-
body: JSON.stringify(metadata, null, 2),
|
|
1141
|
-
contentType: 'application/json',
|
|
1142
|
-
})
|
|
1143
|
-
|
|
1144
|
-
return audioKey
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
/**
|
|
1148
|
-
* Store a call recording to S3
|
|
1149
|
-
*/
|
|
1150
|
-
async storeRecording(recording: {
|
|
1151
|
-
contactId: string
|
|
1152
|
-
from?: string
|
|
1153
|
-
to?: string
|
|
1154
|
-
audioData: Buffer | string
|
|
1155
|
-
duration?: number
|
|
1156
|
-
contentType?: string
|
|
1157
|
-
timestamp?: Date
|
|
1158
|
-
}): Promise<string> {
|
|
1159
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
1160
|
-
throw new Error('Recordings bucket not configured')
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
const timestamp = recording.timestamp || new Date()
|
|
1164
|
-
const ext = recording.contentType === 'audio/mp3' ? 'mp3' : 'wav'
|
|
1165
|
-
const audioKey = `${this.config.recordingsPrefix}${timestamp.toISOString().split('T')[0]}/${recording.contactId}.${ext}`
|
|
1166
|
-
const metadataKey = `${this.config.recordingsPrefix}${timestamp.toISOString().split('T')[0]}/${recording.contactId}.json`
|
|
1167
|
-
|
|
1168
|
-
// Store audio file
|
|
1169
|
-
await this.s3.putObject({
|
|
1170
|
-
bucket: this.config.voicemailBucket,
|
|
1171
|
-
key: audioKey,
|
|
1172
|
-
body: recording.audioData,
|
|
1173
|
-
contentType: recording.contentType || 'audio/wav',
|
|
1174
|
-
})
|
|
1175
|
-
|
|
1176
|
-
// Store metadata
|
|
1177
|
-
const metadata = {
|
|
1178
|
-
contactId: recording.contactId,
|
|
1179
|
-
from: recording.from,
|
|
1180
|
-
to: recording.to,
|
|
1181
|
-
duration: recording.duration || 0,
|
|
1182
|
-
timestamp: timestamp.toISOString(),
|
|
1183
|
-
audioKey,
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
await this.s3.putObject({
|
|
1187
|
-
bucket: this.config.voicemailBucket,
|
|
1188
|
-
key: metadataKey,
|
|
1189
|
-
body: JSON.stringify(metadata, null, 2),
|
|
1190
|
-
contentType: 'application/json',
|
|
1191
|
-
})
|
|
1192
|
-
|
|
1193
|
-
return audioKey
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
// ============================================
|
|
1197
|
-
// Private Helpers
|
|
1198
|
-
// ============================================
|
|
1199
|
-
|
|
1200
|
-
/**
|
|
1201
|
-
* Get voicemail metadata from S3
|
|
1202
|
-
*/
|
|
1203
|
-
private async getVoicemailMetadata(audioKey: string): Promise<Voicemail | null> {
|
|
1204
|
-
if (!this.s3 || !this.config.voicemailBucket) {
|
|
1205
|
-
return null
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
// Try to get metadata file
|
|
1209
|
-
const metadataKey = audioKey.replace(/\.[^/.]+$/, '.json')
|
|
1210
|
-
|
|
1211
|
-
try {
|
|
1212
|
-
const metadataContent = await this.s3.getObject(this.config.voicemailBucket, metadataKey)
|
|
1213
|
-
const metadata = JSON.parse(metadataContent)
|
|
1214
|
-
|
|
1215
|
-
return {
|
|
1216
|
-
key: audioKey,
|
|
1217
|
-
from: metadata.from || 'unknown',
|
|
1218
|
-
to: metadata.to || 'unknown',
|
|
1219
|
-
duration: metadata.duration || 0,
|
|
1220
|
-
timestamp: new Date(metadata.timestamp || Date.now()),
|
|
1221
|
-
transcription: metadata.transcription,
|
|
1222
|
-
contentType: audioKey.endsWith('.mp3') ? 'audio/mp3' : 'audio/wav',
|
|
1223
|
-
raw: metadata,
|
|
1224
|
-
}
|
|
1225
|
-
} catch {
|
|
1226
|
-
// No metadata file, return basic info
|
|
1227
|
-
return {
|
|
1228
|
-
key: audioKey,
|
|
1229
|
-
from: 'unknown',
|
|
1230
|
-
to: 'unknown',
|
|
1231
|
-
duration: 0,
|
|
1232
|
-
timestamp: new Date(),
|
|
1233
|
-
contentType: audioKey.endsWith('.mp3') ? 'audio/mp3' : 'audio/wav',
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
// ============================================
|
|
1240
|
-
// Lambda Handlers
|
|
1241
|
-
// ============================================
|
|
1242
|
-
|
|
1243
|
-
/**
|
|
1244
|
-
* Create a Lambda handler for processing incoming voicemails
|
|
1245
|
-
* Use this with Connect's voicemail feature or custom IVR
|
|
1246
|
-
*
|
|
1247
|
-
* @example
|
|
1248
|
-
* ```typescript
|
|
1249
|
-
* // lambda.ts
|
|
1250
|
-
* import { createVoicemailHandler } from 'ts-cloud/aws/voice'
|
|
1251
|
-
*
|
|
1252
|
-
* export const handler = createVoicemailHandler({
|
|
1253
|
-
* bucket: 'my-voicemail-bucket',
|
|
1254
|
-
* prefix: 'voicemail/',
|
|
1255
|
-
* region: 'us-east-1',
|
|
1256
|
-
* })
|
|
1257
|
-
* ```
|
|
1258
|
-
*/
|
|
1259
|
-
export function createVoicemailHandler(config: {
|
|
1260
|
-
bucket: string
|
|
1261
|
-
prefix?: string
|
|
1262
|
-
region?: string
|
|
1263
|
-
onVoicemail?: (voicemail: Voicemail) => Promise<void>
|
|
1264
|
-
}) {
|
|
1265
|
-
const voiceClient = new VoiceClient({
|
|
1266
|
-
region: config.region || 'us-east-1',
|
|
1267
|
-
voicemailBucket: config.bucket,
|
|
1268
|
-
voicemailPrefix: config.prefix || 'voicemail/',
|
|
1269
|
-
})
|
|
1270
|
-
|
|
1271
|
-
return async (event: any): Promise<any> => {
|
|
1272
|
-
console.log('Incoming voicemail event:', JSON.stringify(event))
|
|
1273
|
-
|
|
1274
|
-
// Handle S3 event (audio file uploaded)
|
|
1275
|
-
if (event.Records) {
|
|
1276
|
-
for (const record of event.Records) {
|
|
1277
|
-
if (record.s3) {
|
|
1278
|
-
const bucket = record.s3.bucket.name
|
|
1279
|
-
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '))
|
|
1280
|
-
|
|
1281
|
-
// Skip metadata files
|
|
1282
|
-
if (key.endsWith('.json')) continue
|
|
1283
|
-
|
|
1284
|
-
console.log(`Processing voicemail: ${bucket}/${key}`)
|
|
1285
|
-
|
|
1286
|
-
if (config.onVoicemail) {
|
|
1287
|
-
const voicemail = await voiceClient.getVoicemail(key)
|
|
1288
|
-
if (voicemail) {
|
|
1289
|
-
await config.onVoicemail(voicemail)
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
// Handle Connect event (from contact flow)
|
|
1297
|
-
if (event.Details?.ContactData) {
|
|
1298
|
-
const contactData = event.Details.ContactData
|
|
1299
|
-
console.log(`Connect voicemail from: ${contactData.CustomerEndpoint?.Address}`)
|
|
1300
|
-
|
|
1301
|
-
// The actual audio would need to be fetched from Connect's recording API
|
|
1302
|
-
// This is triggered after the voicemail is recorded
|
|
1303
|
-
|
|
1304
|
-
if (config.onVoicemail && event.audioData) {
|
|
1305
|
-
const key = await voiceClient.storeVoicemail({
|
|
1306
|
-
from: contactData.CustomerEndpoint?.Address || 'unknown',
|
|
1307
|
-
to: contactData.SystemEndpoint?.Address || 'unknown',
|
|
1308
|
-
audioData: event.audioData,
|
|
1309
|
-
duration: event.duration,
|
|
1310
|
-
transcription: event.transcription,
|
|
1311
|
-
})
|
|
1312
|
-
|
|
1313
|
-
const voicemail = await voiceClient.getVoicemail(key)
|
|
1314
|
-
if (voicemail) {
|
|
1315
|
-
await config.onVoicemail(voicemail)
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
return {
|
|
1321
|
-
statusCode: 200,
|
|
1322
|
-
body: JSON.stringify({ message: 'Voicemail processed' }),
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
/**
|
|
1328
|
-
* Create a Lambda handler for Connect recording events
|
|
1329
|
-
* This processes call recordings uploaded to S3 by Connect
|
|
1330
|
-
*/
|
|
1331
|
-
export function createRecordingHandler(config: {
|
|
1332
|
-
bucket: string
|
|
1333
|
-
prefix?: string
|
|
1334
|
-
region?: string
|
|
1335
|
-
onRecording?: (recording: CallRecording) => Promise<void>
|
|
1336
|
-
}) {
|
|
1337
|
-
const voiceClient = new VoiceClient({
|
|
1338
|
-
region: config.region || 'us-east-1',
|
|
1339
|
-
voicemailBucket: config.bucket,
|
|
1340
|
-
recordingsPrefix: config.prefix || 'recordings/',
|
|
1341
|
-
})
|
|
1342
|
-
|
|
1343
|
-
return async (event: any): Promise<any> => {
|
|
1344
|
-
console.log('Recording event:', JSON.stringify(event))
|
|
1345
|
-
|
|
1346
|
-
// Handle S3 event
|
|
1347
|
-
if (event.Records) {
|
|
1348
|
-
for (const record of event.Records) {
|
|
1349
|
-
if (record.s3) {
|
|
1350
|
-
const bucket = record.s3.bucket.name
|
|
1351
|
-
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '))
|
|
1352
|
-
|
|
1353
|
-
if (key.endsWith('.json')) continue
|
|
1354
|
-
|
|
1355
|
-
console.log(`Processing recording: ${bucket}/${key}`)
|
|
1356
|
-
|
|
1357
|
-
if (config.onRecording) {
|
|
1358
|
-
const recordings = await voiceClient.getRecordings({ prefix: key })
|
|
1359
|
-
if (recordings.length > 0) {
|
|
1360
|
-
await config.onRecording(recordings[0])
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
return {
|
|
1368
|
-
statusCode: 200,
|
|
1369
|
-
body: JSON.stringify({ message: 'Recording processed' }),
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
/**
|
|
1375
|
-
* Convenience function to create a voice client
|
|
1376
|
-
*/
|
|
1377
|
-
export function createVoiceClient(config?: VoiceClientConfig): VoiceClient {
|
|
1378
|
-
return new VoiceClient(config)
|
|
1379
|
-
}
|