@mqarty/plugin-dev-phone 1.0.0-beta.10
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 +56 -0
- package/dist/commands/dev-phone.js +696 -0
- package/dist/serverless/functions/incoming-call-handler.js +8 -0
- package/dist/serverless/functions/incoming-message-handler.js +41 -0
- package/dist/serverless/functions/outbound-call-handler.js +17 -0
- package/dist/serverless/functions/sync-call-history.js +62 -0
- package/dist/utils/create-serverless-util.js +86 -0
- package/dist/utils/helpers.js +28 -0
- package/dist/utils/phone-number-utils.js +66 -0
- package/oclif.manifest.json +229 -0
- package/package.json +79 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const open_1 = __importDefault(require("open"));
|
|
18
|
+
const express_1 = __importDefault(require("express"));
|
|
19
|
+
const confirm_1 = __importDefault(require("@inquirer/confirm"));
|
|
20
|
+
const core_1 = require("@oclif/core");
|
|
21
|
+
const create_serverless_util_1 = require("../utils/create-serverless-util");
|
|
22
|
+
const helpers_1 = require("../utils/helpers");
|
|
23
|
+
const phone_number_utils_1 = require("../utils/phone-number-utils");
|
|
24
|
+
const { TwilioClientCommand } = require('@twilio/cli-core').baseCommands;
|
|
25
|
+
const { TwilioCliError } = require('@twilio/cli-core').services.error;
|
|
26
|
+
const WebClientPath = path_1.default.resolve(require.resolve('@mqarty/dev-phone-ui'), '..');
|
|
27
|
+
const { version } = require('../../package.json');
|
|
28
|
+
const AccessToken = require('twilio').jwt.AccessToken;
|
|
29
|
+
const ChatGrant = AccessToken.ChatGrant;
|
|
30
|
+
const VoiceGrant = AccessToken.VoiceGrant;
|
|
31
|
+
const SyncGrant = AccessToken.SyncGrant;
|
|
32
|
+
const CALL_LOG_MAP_NAME = 'CallLog';
|
|
33
|
+
// removes unecessary properties to standardize the twilio phone number
|
|
34
|
+
const reformatTwilioPns = (twilioResponse) => {
|
|
35
|
+
return {
|
|
36
|
+
"phone-numbers": twilioResponse.map(({ phoneNumber, friendlyName, smsUrl, voiceUrl, sid }) => ({ phoneNumber, friendlyName, smsUrl, voiceUrl, sid }))
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
const generateRandomPhoneName = () => {
|
|
40
|
+
let rand = Math.random().toString().substring(2, 6);
|
|
41
|
+
return `dev-phone-${rand}`;
|
|
42
|
+
};
|
|
43
|
+
class DevPhoneServer extends TwilioClientCommand {
|
|
44
|
+
constructor(argv, config, secureStorage) {
|
|
45
|
+
super(argv, config, secureStorage);
|
|
46
|
+
this.cliSettings = {};
|
|
47
|
+
this.pns = [];
|
|
48
|
+
this.port = 1337;
|
|
49
|
+
this.jwt = null;
|
|
50
|
+
this.apikey = {};
|
|
51
|
+
this.twimlApp = {};
|
|
52
|
+
this.devPhoneName = generateRandomPhoneName();
|
|
53
|
+
this.voiceUrl = null;
|
|
54
|
+
this.smsUrl = null;
|
|
55
|
+
this.voiceOutboundUrl = null;
|
|
56
|
+
}
|
|
57
|
+
run() {
|
|
58
|
+
const _super = Object.create(null, {
|
|
59
|
+
run: { get: () => super.run }
|
|
60
|
+
});
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
yield _super.run.call(this);
|
|
63
|
+
const props = this.parseProperties() || {};
|
|
64
|
+
yield this.validatePropsAndFlags(props, this.flags);
|
|
65
|
+
console.log(`Hello š I'm your dev-phone and my name is ${this.devPhoneName}\n`);
|
|
66
|
+
// set user agent header on twilio client
|
|
67
|
+
this.twilioClient.userAgentExtensions = [
|
|
68
|
+
`@mqarty/plugin-dev-phone/${version}`,
|
|
69
|
+
`@mqarty/plugin-dev-phone/helper-library`,
|
|
70
|
+
'serverless-functions'
|
|
71
|
+
];
|
|
72
|
+
// create API KEY and API SECRET to be generate JWT AccessToken for ChatGrant, VoiceGrant and SyncGrant
|
|
73
|
+
this.apikey = yield this.reuseOrCreateApiKey();
|
|
74
|
+
const isDeletingAll = () => !!this.flags.clear;
|
|
75
|
+
const deleteAll = () => __awaiter(this, void 0, void 0, function* () {
|
|
76
|
+
yield this.destroyAllConversations();
|
|
77
|
+
yield this.destroyAllTwimlApps();
|
|
78
|
+
yield this.destroyAllApiKeys();
|
|
79
|
+
yield this.destroyAllSyncs();
|
|
80
|
+
yield this.destroyAllFunctions();
|
|
81
|
+
yield this.removeAllPhoneWebhooks();
|
|
82
|
+
});
|
|
83
|
+
if (isDeletingAll()) {
|
|
84
|
+
const deleteAllConfirmation = yield (0, confirm_1.default)({
|
|
85
|
+
message: "Do you want to delete all of the dev phone resources on your Twilio account? This may interfere with other instances of the Dev Phone.",
|
|
86
|
+
default: false
|
|
87
|
+
});
|
|
88
|
+
if (deleteAllConfirmation) {
|
|
89
|
+
console.log(`š Deleting all dev-phone resources from your account before starting...`);
|
|
90
|
+
yield deleteAll().finally(() => console.log(`ā
All resources have been deleted.`));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// create conversation for SMS/web interface
|
|
94
|
+
this.conversation = yield this.createConversation();
|
|
95
|
+
// create Sync for Call History interface
|
|
96
|
+
this.sync = yield this.createSync();
|
|
97
|
+
// create Function to handle inbound-voice, inbound-sms and outbound-voice (voip)
|
|
98
|
+
this.serverless = yield this.createFunction();
|
|
99
|
+
// create TwiML App
|
|
100
|
+
this.twimlApp = yield this.createTwimlApp();
|
|
101
|
+
// create JWT Access Token with ChatGrant, VoiceGrant and SyncGrant
|
|
102
|
+
this.jwt = yield this.createJwt();
|
|
103
|
+
// add webhook config to the phone number, if there is one passed by CLI flag
|
|
104
|
+
// TO-DO return updated phone number and set this.phoneNumber
|
|
105
|
+
const phoneNumberProps = { voiceUrl: this.voiceUrl, smsUrl: this.smsUrl, statusCallback: this.statusCallback };
|
|
106
|
+
this.cliSettings.phoneNumber = yield (0, phone_number_utils_1.updatePhoneWebhooks)(this.cliSettings.phoneNumber, this.twilioClient.incomingPhoneNumbers, phoneNumberProps);
|
|
107
|
+
const onShutdown = () => __awaiter(this, void 0, void 0, function* () {
|
|
108
|
+
yield this.destroyConversations();
|
|
109
|
+
yield this.destroyTwimlApps();
|
|
110
|
+
yield this.destroyApiKeys();
|
|
111
|
+
yield this.destroySyncs();
|
|
112
|
+
yield this.destroyFunction();
|
|
113
|
+
yield (0, phone_number_utils_1.removePhoneWebhooks)(this.cliSettings.phoneNumber, this.twilioClient.incomingPhoneNumbers);
|
|
114
|
+
});
|
|
115
|
+
process.on("SIGTERM", function () {
|
|
116
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
console.log("\nš Shutting down");
|
|
118
|
+
yield onShutdown().finally(() => process.exit(0));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
process.on('SIGINT', function () {
|
|
122
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
123
|
+
console.log("\nš Shutting down");
|
|
124
|
+
yield onShutdown().finally(() => process.exit(0));
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
const app = (0, express_1.default)();
|
|
128
|
+
// serve assets from the "public" directory
|
|
129
|
+
// __dirname is the path to _this_ file, so ../../public to find index.html
|
|
130
|
+
app.use(express_1.default.static(WebClientPath));
|
|
131
|
+
app.use(express_1.default.json()); // response body writer
|
|
132
|
+
app.get("/ping", (req, res) => {
|
|
133
|
+
res.json({ pong: true });
|
|
134
|
+
console.log('TWILIO', this.twilioClient);
|
|
135
|
+
});
|
|
136
|
+
app.get("/plugin-settings", (req, res) => {
|
|
137
|
+
res.json(Object.assign(Object.assign({}, this.cliSettings), { devPhoneName: this.devPhoneName, conversation: this.conversation }));
|
|
138
|
+
});
|
|
139
|
+
app.get("/phone-numbers", (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
140
|
+
if (this.pns.length === 0) {
|
|
141
|
+
try {
|
|
142
|
+
const pns = yield this.twilioClient.incomingPhoneNumbers.list();
|
|
143
|
+
this.pns = pns;
|
|
144
|
+
res.json(reformatTwilioPns(pns));
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
console.error('Phone number API threw an error', err);
|
|
148
|
+
res.status(err.status ? err.status : 400).send({ error: err });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
res.json(reformatTwilioPns(this.pns));
|
|
153
|
+
}
|
|
154
|
+
}));
|
|
155
|
+
app.post("/send-sms", (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
156
|
+
const { body, from, to } = req.body;
|
|
157
|
+
try {
|
|
158
|
+
const message = yield this.twilioClient.messages
|
|
159
|
+
.create({
|
|
160
|
+
body,
|
|
161
|
+
from,
|
|
162
|
+
to
|
|
163
|
+
});
|
|
164
|
+
res.json({ result: message });
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
console.error('SMS API threw an error', err);
|
|
168
|
+
res.status(err.status ? err.status : 400).send({ error: err });
|
|
169
|
+
}
|
|
170
|
+
;
|
|
171
|
+
}));
|
|
172
|
+
app.all("/choose-phone-number", (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
173
|
+
try {
|
|
174
|
+
const rawNumbers = yield this.twilioClient.incomingPhoneNumbers
|
|
175
|
+
.list({ phoneNumber: req.body.phoneNumber, limit: 20 });
|
|
176
|
+
const selectedNumber = reformatTwilioPns(rawNumbers)["phone-numbers"];
|
|
177
|
+
// Should only have a single number
|
|
178
|
+
if (selectedNumber.length === 1) {
|
|
179
|
+
yield (0, phone_number_utils_1.removePhoneWebhooks)(this.cliSettings.phoneNumber, this.twilioClient.incomingPhoneNumbers);
|
|
180
|
+
this.cliSettings.phoneNumber = selectedNumber[0];
|
|
181
|
+
this.cliSettings.phoneNumber = yield (0, phone_number_utils_1.updatePhoneWebhooks)(this.cliSettings.phoneNumber, this.twilioClient.incomingPhoneNumbers, { voiceUrl: this.voiceUrl, smsUrl: this.smsUrl, statusCallback: this.statusCallback });
|
|
182
|
+
res.json({
|
|
183
|
+
phoneNumber: this.cliSettings.phoneNumber,
|
|
184
|
+
message: 'Phone number updated!'
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
console.error('Phone number not found!');
|
|
189
|
+
res.status(400).send({
|
|
190
|
+
message: 'Phone number not found!'
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
console.error(err);
|
|
196
|
+
res.status(400).send(err);
|
|
197
|
+
}
|
|
198
|
+
}));
|
|
199
|
+
app.get("/client-token", (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
200
|
+
try {
|
|
201
|
+
if (!this.jwt) {
|
|
202
|
+
this.jwt = yield this.createJwt();
|
|
203
|
+
}
|
|
204
|
+
res.json({ token: this.jwt });
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
res.status(400).send(err);
|
|
208
|
+
}
|
|
209
|
+
}));
|
|
210
|
+
const isHeadless = () => !!this.flags.headless;
|
|
211
|
+
app.listen(this.port, () => {
|
|
212
|
+
console.log(`š Your local webserver is listening on port ${this.port}`);
|
|
213
|
+
if (fs_1.default.existsSync(path_1.default.join(WebClientPath, 'index.html'))) {
|
|
214
|
+
const uiUrl = `http://localhost:${this.port}/`;
|
|
215
|
+
if (isHeadless()) {
|
|
216
|
+
console.log(`š UI is available at ${uiUrl}`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
console.log(`š Opening ${uiUrl} your browser`);
|
|
220
|
+
(0, open_1.default)(uiUrl);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
console.log('Hello friend! Front end files are missing, ie you are developing this pluign.');
|
|
225
|
+
console.log('Run: `cd plugin-dev-phone-client` then `npm start` to run dev front-end');
|
|
226
|
+
console.log('To build the front-end so that the local backend will serve it: ./build-for-release.sh');
|
|
227
|
+
}
|
|
228
|
+
console.log('ā¶ļø Use ctrl-c to stop your dev-phone\n');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
createFunction() {
|
|
233
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
234
|
+
console.log('š» Deploying a Functions Service to handle incoming calls and SMS...');
|
|
235
|
+
const deployedFunctions = yield (0, create_serverless_util_1.deployServerless)({
|
|
236
|
+
username: this.twilioClient.username,
|
|
237
|
+
password: this.twilioClient.password,
|
|
238
|
+
env: {
|
|
239
|
+
SYNC_SERVICE_SID: this.sync.sid,
|
|
240
|
+
CONVERSATION_SID: this.conversation.sid,
|
|
241
|
+
CONVERSATION_SERVICE_SID: this.conversation.serviceSid,
|
|
242
|
+
DEV_PHONE_NAME: this.devPhoneName,
|
|
243
|
+
DEV_PHONE_VERSION: version,
|
|
244
|
+
CALL_LOG_MAP_NAME
|
|
245
|
+
},
|
|
246
|
+
onUpdate: (event) => {
|
|
247
|
+
const isBuildStatusPing = event.message.indexOf('Current status: building') > -1;
|
|
248
|
+
const settingEnvVars = event.message.indexOf('environment variables') > -1;
|
|
249
|
+
if (isBuildStatusPing || event.status === 'building') {
|
|
250
|
+
isBuildStatusPing ? process.stdout.write('.') : process.stdout.write(`š ${event.message}`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
console.log(`${settingEnvVars ? '\n' : ''}š§āš» ${event.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
console.log(`ā
I'm using the Serverless Service ${deployedFunctions.serviceSid}\n`);
|
|
258
|
+
this.voiceUrl = `https://${deployedFunctions.domain}/${create_serverless_util_1.constants.INCOMING_CALL_HANDLER}`;
|
|
259
|
+
this.voiceOutboundUrl = `https://${deployedFunctions.domain}/${create_serverless_util_1.constants.OUTBOUND_CALL_HANDLER}`;
|
|
260
|
+
this.smsUrl = `https://${deployedFunctions.domain}/${create_serverless_util_1.constants.INCOMING_MESSAGE_HANDLER}`;
|
|
261
|
+
this.statusCallback = `https://${deployedFunctions.domain}/${create_serverless_util_1.constants.SYNC_CALL_HISTORY}`;
|
|
262
|
+
return deployedFunctions;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
destroyFunction() {
|
|
266
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
267
|
+
try {
|
|
268
|
+
const functionServices = yield this.twilioClient.serverless.v1.services.list();
|
|
269
|
+
const devPhoneFunctionServices = functionServices.filter((functionServices) => {
|
|
270
|
+
return functionServices.friendlyName !== null && functionServices.friendlyName.startsWith(this.devPhoneName);
|
|
271
|
+
});
|
|
272
|
+
if (devPhoneFunctionServices.length > 0) {
|
|
273
|
+
console.log(`š® Removing Serverless Functions for ${this.devPhoneName}`);
|
|
274
|
+
for (const functionService of devPhoneFunctionServices) {
|
|
275
|
+
yield this.twilioClient.serverless.v1.services(functionService.sid)
|
|
276
|
+
.remove();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
console.error(err);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
destroyAllFunctions() {
|
|
286
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
287
|
+
try {
|
|
288
|
+
const functionServices = yield this.twilioClient.serverless.v1.services.list();
|
|
289
|
+
const devPhoneFunctionServices = functionServices.filter((functionServices) => {
|
|
290
|
+
return functionServices.friendlyName !== null && functionServices.friendlyName.startsWith('dev-phone');
|
|
291
|
+
});
|
|
292
|
+
if (devPhoneFunctionServices.length > 0) {
|
|
293
|
+
console.log(`š® Removing All Serverless Functions for existing dev phone`);
|
|
294
|
+
for (const functionService of devPhoneFunctionServices) {
|
|
295
|
+
yield this.twilioClient.serverless.v1.services(functionService.sid)
|
|
296
|
+
.remove();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
console.error(err);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
validatePropsAndFlags(props, flags) {
|
|
306
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
307
|
+
// Flags defined below can be validated and used here. Example:
|
|
308
|
+
// https://github.com/twilio/plugin-debugger/blob/main/src/commands/debugger/logs/list.js#L46-L56
|
|
309
|
+
this.cliSettings.forceMode = flags['force'];
|
|
310
|
+
this.port = process.env.TWILIO_DEV_PHONE_PORT || (yield (0, helpers_1.getAvailablePort)());
|
|
311
|
+
if (flags['phone-number']) {
|
|
312
|
+
const phoneNumber = yield flags['phone-number'];
|
|
313
|
+
this.pns = yield this.twilioClient.incomingPhoneNumbers
|
|
314
|
+
.list({ phoneNumber: phoneNumber });
|
|
315
|
+
if (this.pns.length < 1) {
|
|
316
|
+
throw new TwilioCliError(`The phone number ${phoneNumber} is not associated with your Twilio account`);
|
|
317
|
+
}
|
|
318
|
+
const pnConfigAlreadySet = [
|
|
319
|
+
((0, phone_number_utils_1.isSmsUrlSet)(this.pns[0].smsUrl) ? "SMS webhook URL" : null),
|
|
320
|
+
((0, phone_number_utils_1.isVoiceUrlSet)(this.pns[0].voiceUrl) ? "Voice webhook URL" : null),
|
|
321
|
+
].filter(x => x);
|
|
322
|
+
if (pnConfigAlreadySet.length > 0 && !this.cliSettings.forceMode) {
|
|
323
|
+
throw new TwilioCliError(`Cannot use ${phoneNumber} because the following config for that phone number would be overwritten: ` + pnConfigAlreadySet.join(", "));
|
|
324
|
+
}
|
|
325
|
+
this.cliSettings.phoneNumber = reformatTwilioPns(this.pns)["phone-numbers"][0];
|
|
326
|
+
}
|
|
327
|
+
if (flags['port']) {
|
|
328
|
+
const port = yield flags['port'];
|
|
329
|
+
try {
|
|
330
|
+
if ((0, helpers_1.isValidPort)(port)) {
|
|
331
|
+
this.port = parseInt(port);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
throw new TwilioCliError(`āļø '${port}' is not a valid port. š³ I'll try to get set up with ${this.port} instead.`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
console.error(err.message);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
twilioCliIsConfiguredWithApiKey() {
|
|
344
|
+
return this.currentProfile.apiKey.startsWith("SK");
|
|
345
|
+
}
|
|
346
|
+
reuseOrCreateApiKey() {
|
|
347
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
348
|
+
// We need an API KEY and SECRET to create the Access Token
|
|
349
|
+
// Depending on how the user has provided the CLI with creds
|
|
350
|
+
// we may have one already in this.currentProfile, or we may
|
|
351
|
+
// need to create a new one
|
|
352
|
+
if (this.twilioCliIsConfiguredWithApiKey()) {
|
|
353
|
+
// This case is if the user has _not_ used env vars for
|
|
354
|
+
// their creds. Here we can reuse the api keys and secret
|
|
355
|
+
// that the CLI created when it was installed
|
|
356
|
+
console.log("ā
I'm using your profile API key.\n");
|
|
357
|
+
return {
|
|
358
|
+
sid: this.currentProfile.apiKey,
|
|
359
|
+
secret: this.currentProfile.apiSecret
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
// This case is if the user has started the CLI with
|
|
364
|
+
// $TWILIO_ACCOUNT_SID and $TWILIO_AUTH_TOKEN set in
|
|
365
|
+
// their environment, using their account creds but
|
|
366
|
+
// their API_KEY and SECRET are not properly set.
|
|
367
|
+
// the CLI uses the ACCOUNT_SID into currentProfile.apiKey
|
|
368
|
+
// and we need to generate another key
|
|
369
|
+
console.log("š» I'm creating a new API Key...");
|
|
370
|
+
yield this.destroyApiKeys();
|
|
371
|
+
try {
|
|
372
|
+
const key = yield this.twilioClient.newKeys.create({ friendlyName: this.devPhoneName });
|
|
373
|
+
const mask = (value) => {
|
|
374
|
+
if (!value)
|
|
375
|
+
return "";
|
|
376
|
+
const last4 = value.slice(-4);
|
|
377
|
+
return `${"*".repeat(value.length - 4)}${last4}`;
|
|
378
|
+
};
|
|
379
|
+
console.log(`ā
I'm using the API Key ${mask(key.sid)}\n`);
|
|
380
|
+
this.currentProfile.apiKey = key.sid;
|
|
381
|
+
this.currentProfile.apiSecret = key.secret;
|
|
382
|
+
return {
|
|
383
|
+
sid: this.currentProfile.apiKey,
|
|
384
|
+
secret: this.currentProfile.apiSecret
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
console.error(err);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
destroyApiKeys() {
|
|
394
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
395
|
+
if (this.twilioCliIsConfiguredWithApiKey()) {
|
|
396
|
+
// we never created one
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
try {
|
|
401
|
+
const keys = yield this.twilioClient.keys.list();
|
|
402
|
+
const devPhoneKeys = keys.filter((key) => {
|
|
403
|
+
return key.friendlyName !== null && key.friendlyName.startsWith(this.devPhoneName);
|
|
404
|
+
});
|
|
405
|
+
if (devPhoneKeys.length > 0) {
|
|
406
|
+
console.log(`š® Removing API Keys for ${this.devPhoneName}`);
|
|
407
|
+
for (const key of devPhoneKeys) {
|
|
408
|
+
yield this.twilioClient.keys(key.sid).remove();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
console.error(err);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
destroyAllApiKeys() {
|
|
419
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
420
|
+
if (this.twilioCliIsConfiguredWithApiKey()) {
|
|
421
|
+
// we never created one
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
try {
|
|
426
|
+
const keys = yield this.twilioClient.keys.list();
|
|
427
|
+
const devPhoneKeys = keys.filter((key) => {
|
|
428
|
+
return key.friendlyName !== null && key.friendlyName.startsWith('dev-phone');
|
|
429
|
+
});
|
|
430
|
+
if (devPhoneKeys.length > 0) {
|
|
431
|
+
console.log(`š® Removing All API Keys for existing dev phone`);
|
|
432
|
+
for (const key of devPhoneKeys) {
|
|
433
|
+
yield this.twilioClient.keys(key.sid).remove();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
console.error(err);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
createTwimlApp() {
|
|
444
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
445
|
+
console.log('š» Creating a new TwiMl App to allow voice calls from your browser...');
|
|
446
|
+
yield this.destroyTwimlApps();
|
|
447
|
+
try {
|
|
448
|
+
const app = yield this.twilioClient.applications
|
|
449
|
+
.create({
|
|
450
|
+
voiceUrl: this.voiceOutboundUrl,
|
|
451
|
+
friendlyName: this.devPhoneName
|
|
452
|
+
});
|
|
453
|
+
console.log(`ā
I'm using the TwiMl App ${app.sid}\n`);
|
|
454
|
+
return app;
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
console.error(err);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
destroyTwimlApps() {
|
|
462
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
463
|
+
try {
|
|
464
|
+
const applications = yield this.twilioClient.applications.list();
|
|
465
|
+
const devPhoneApps = applications.filter((twimlApp) => {
|
|
466
|
+
return twimlApp.friendlyName !== null && twimlApp.friendlyName.startsWith(this.devPhoneName);
|
|
467
|
+
});
|
|
468
|
+
if (devPhoneApps.length > 0) {
|
|
469
|
+
console.log(`š® Removing TwiML app for ${this.devPhoneName}`);
|
|
470
|
+
for (const twimlApp of devPhoneApps) {
|
|
471
|
+
yield this.twilioClient.applications(twimlApp.sid)
|
|
472
|
+
.remove();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
console.error(err);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
destroyAllTwimlApps() {
|
|
482
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
483
|
+
try {
|
|
484
|
+
const applications = yield this.twilioClient.applications.list();
|
|
485
|
+
const devPhoneApps = applications.filter((twimlApp) => {
|
|
486
|
+
return twimlApp.friendlyName !== null && twimlApp.friendlyName.startsWith('dev-phone');
|
|
487
|
+
});
|
|
488
|
+
if (devPhoneApps.length > 0) {
|
|
489
|
+
console.log(`š® Removing All TwiML app for existing dev phone`);
|
|
490
|
+
for (const twimlApp of devPhoneApps) {
|
|
491
|
+
yield this.twilioClient.applications(twimlApp.sid)
|
|
492
|
+
.remove();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
console.error(err);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
createJwt() {
|
|
502
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
503
|
+
const chatGrant = new ChatGrant({
|
|
504
|
+
serviceSid: this.conversation.serviceSid
|
|
505
|
+
});
|
|
506
|
+
const voiceGrant = new VoiceGrant({
|
|
507
|
+
incomingAllow: true,
|
|
508
|
+
outgoingApplicationSid: this.twimlApp.sid
|
|
509
|
+
});
|
|
510
|
+
const syncGrant = new SyncGrant({
|
|
511
|
+
serviceSid: this.sync.sid,
|
|
512
|
+
});
|
|
513
|
+
const token = new AccessToken(this.twilioClient.accountSid, this.apikey.sid, this.apikey.secret, {
|
|
514
|
+
identity: this.devPhoneName,
|
|
515
|
+
ttl: 24 * 60 * 60
|
|
516
|
+
});
|
|
517
|
+
token.addGrant(chatGrant);
|
|
518
|
+
token.addGrant(voiceGrant);
|
|
519
|
+
token.addGrant(syncGrant);
|
|
520
|
+
return token.toJwt();
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
createSync() {
|
|
524
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
525
|
+
console.log('š» Creating a new sync list for call history...');
|
|
526
|
+
yield this.destroySyncs();
|
|
527
|
+
try {
|
|
528
|
+
const syncService = yield this.twilioClient.sync.v1.services
|
|
529
|
+
.create({ friendlyName: this.devPhoneName });
|
|
530
|
+
console.log(`ā
I'm using the sync service ${syncService.sid}\n`);
|
|
531
|
+
// create 'CallLog' syncMap
|
|
532
|
+
yield this.twilioClient.sync.v1.services(syncService.sid).syncMaps.create({
|
|
533
|
+
uniqueName: CALL_LOG_MAP_NAME,
|
|
534
|
+
});
|
|
535
|
+
return syncService;
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
console.error(err);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
destroySyncs() {
|
|
543
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
544
|
+
try {
|
|
545
|
+
const syncServices = yield this.twilioClient.sync.v1.services.list();
|
|
546
|
+
const devPhoneSyncServices = syncServices.filter((syncService) => {
|
|
547
|
+
return syncService.friendlyName !== null && syncService.friendlyName.startsWith(this.devPhoneName);
|
|
548
|
+
});
|
|
549
|
+
if (devPhoneSyncServices.length > 0) {
|
|
550
|
+
console.log(`š® Removing Sync Service for ${this.devPhoneName}`);
|
|
551
|
+
for (const syncService of devPhoneSyncServices) {
|
|
552
|
+
yield this.twilioClient.sync.v1.services(syncService.sid)
|
|
553
|
+
.remove();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch (err) {
|
|
558
|
+
console.error(err);
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
destroyAllSyncs() {
|
|
563
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
564
|
+
try {
|
|
565
|
+
const syncServices = yield this.twilioClient.sync.v1.services.list();
|
|
566
|
+
const devPhoneSyncServices = syncServices.filter((syncService) => {
|
|
567
|
+
return syncService.friendlyName !== null && syncService.friendlyName.startsWith('dev-phone');
|
|
568
|
+
});
|
|
569
|
+
if (devPhoneSyncServices.length > 0) {
|
|
570
|
+
console.log(`š® Removing All Sync Service for existing dev phone`);
|
|
571
|
+
for (const syncService of devPhoneSyncServices) {
|
|
572
|
+
yield this.twilioClient.sync.v1.services(syncService.sid)
|
|
573
|
+
.remove();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
console.error(err);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
// Creates a new conversation service, a conversation, and makes the dev phone a participant
|
|
583
|
+
createConversation() {
|
|
584
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
585
|
+
yield this.destroyConversations();
|
|
586
|
+
console.log('š» Creating a new conversation...');
|
|
587
|
+
try {
|
|
588
|
+
const service = yield this.twilioClient.conversations.v1.services
|
|
589
|
+
.create({ friendlyName: this.devPhoneName });
|
|
590
|
+
const conversationService = this.twilioClient.conversations.v1.services(service.sid);
|
|
591
|
+
const newConversation = yield conversationService.conversations.create({ friendlyName: this.devPhoneName });
|
|
592
|
+
yield conversationService.conversations(newConversation.sid)
|
|
593
|
+
.participants.create({ identity: this.devPhoneName });
|
|
594
|
+
console.log(`ā
I'm using the conversation ${newConversation.sid} from service ${service.sid}\n`);
|
|
595
|
+
return {
|
|
596
|
+
serviceSid: service.sid,
|
|
597
|
+
sid: newConversation.sid
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
console.error(err);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
destroyConversations() {
|
|
606
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
607
|
+
try {
|
|
608
|
+
const convoServices = yield this.twilioClient.conversations.v1.services.list();
|
|
609
|
+
const devPhoneConvoServices = convoServices.filter((convoService) => {
|
|
610
|
+
return convoService.friendlyName !== null && convoService.friendlyName.startsWith(this.devPhoneName);
|
|
611
|
+
});
|
|
612
|
+
if (devPhoneConvoServices.length > 0) {
|
|
613
|
+
console.log(`š® Removing Conversation Service for ${this.devPhoneName}`);
|
|
614
|
+
for (const convoService of devPhoneConvoServices) {
|
|
615
|
+
yield this.twilioClient.conversations.v1.services(convoService.sid)
|
|
616
|
+
.remove();
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
console.error(err);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
destroyAllConversations() {
|
|
626
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
627
|
+
try {
|
|
628
|
+
const convoServices = yield this.twilioClient.conversations.v1.services.list();
|
|
629
|
+
const devPhoneConvoServices = convoServices.filter((convoService) => {
|
|
630
|
+
return convoService.friendlyName !== null && convoService.friendlyName.startsWith('dev-phone');
|
|
631
|
+
});
|
|
632
|
+
if (devPhoneConvoServices.length > 0) {
|
|
633
|
+
console.log(`š® Removing All Conversation Service for existing dev phone`);
|
|
634
|
+
for (const convoService of devPhoneConvoServices) {
|
|
635
|
+
yield this.twilioClient.conversations.v1.services(convoService.sid)
|
|
636
|
+
.remove();
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
console.error(err);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
removeAllPhoneWebhooks() {
|
|
646
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
647
|
+
try {
|
|
648
|
+
const pns = yield this.twilioClient.incomingPhoneNumbers.list();
|
|
649
|
+
const numbersDevPhone = pns.filter((pn) => {
|
|
650
|
+
return pn.smsUrl.startsWith('https://dev-phone') && pn.voiceUrl.startsWith('https://dev-phone');
|
|
651
|
+
});
|
|
652
|
+
if (numbersDevPhone.length > 0) {
|
|
653
|
+
console.log(`š® Removing All number webhooks for dev phone`);
|
|
654
|
+
for (const pn of numbersDevPhone) {
|
|
655
|
+
yield (0, phone_number_utils_1.removePhoneWebhooks)({
|
|
656
|
+
voiceUrl: '',
|
|
657
|
+
smsUrl: '',
|
|
658
|
+
statusCallback: '',
|
|
659
|
+
phoneNumber: pn.phoneNumber,
|
|
660
|
+
sid: pn.sid,
|
|
661
|
+
}, this.twilioClient.incomingPhoneNumbers);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
console.error(err);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
DevPhoneServer.description = `Dev Phone local express server`;
|
|
672
|
+
// Example of how to define flags and properties:
|
|
673
|
+
// https://github.com/twilio/plugin-debugger/blob/main/src/commands/debugger/logs/list.js#L99-L126
|
|
674
|
+
DevPhoneServer.PropertyFlags = {
|
|
675
|
+
"phone-number": core_1.Flags.string({
|
|
676
|
+
description: 'Optional. Associates the Dev Phone with a phone number. Takes a number from the active profile on the Twilio CLI as the parameter.'
|
|
677
|
+
}),
|
|
678
|
+
force: core_1.Flags.boolean({
|
|
679
|
+
char: 'f',
|
|
680
|
+
description: 'Optional. Forces an overwrite of the phone number configuration.',
|
|
681
|
+
dependsOn: ['phone-number']
|
|
682
|
+
}),
|
|
683
|
+
headless: core_1.Flags.boolean({
|
|
684
|
+
description: 'Optional. Prevents the UI from automatically opening in the browser.',
|
|
685
|
+
default: false,
|
|
686
|
+
}),
|
|
687
|
+
clear: core_1.Flags.boolean({
|
|
688
|
+
description: 'Optional. Remove all dev-phone resources from your account before starting the dev-phone.',
|
|
689
|
+
default: false,
|
|
690
|
+
}),
|
|
691
|
+
port: core_1.Flags.string({
|
|
692
|
+
description: 'Optional. Configures the port of the Dev Phone UI. Takes a valid port as a parameter.',
|
|
693
|
+
})
|
|
694
|
+
};
|
|
695
|
+
DevPhoneServer.flags = Object.assign(DevPhoneServer.PropertyFlags, TwilioClientCommand.flags);
|
|
696
|
+
module.exports = DevPhoneServer;
|