Forwarding an incoming call after task reservation timeout - twilio

I am using Twilio Flex to support a call center. I have a TaskRouter workflow set up where Task Reservation Timeout is set to 120 seconds. In its filter, I've created two routing steps. The first one finds matching workers in the main queue and has a timeout of 120 seconds. After 120 seconds, it should move to Call Forward Queue. In the call forward queue, no workers exist (target worker expression: 1==2). I'm catching all these events with a "trEventListener" function. Once a task is moved into the Call Forward queue, I call the "callForward" function which uses twiml.dial() to connect the call to an external number. I also change this task's status to "canceled" with a custom reason so I can track it in flex insights. I am using the guide in this link to form my logic: https://support.twilio.com/hc/en-us/articles/360021082934-Implementing-Voicemail-with-Twilio-Flex-TaskRouter-and-WFO.
Call forwarding is working fine but according to Flex insights, there are some calls that get handled after 120 seconds (between 120 - 300 seconds). Ideally, these should be forwarded as well. There is also no error logged for me to track down why this is happening to only a handful of calls.
Furthermore, in some cases, when I try to change the task status to cancel with my custom reason, it spits out the following error: Cannot cancel task because it is not pending or reserved. In other cases, it works fine. It's again hard to figure out why it's selectively working and not consistent in its behavior.
Here is the function code.
trEventListener.js:
exports.handler = function(context, event, callback) {
const client = context.getTwilioClient();
let task = '';
let workspace = '';
console.log(`__[trEventStream]__: Event recieved of type: ${event.EventType}`);
// setup an empty success response
let response = new Twilio.Response();
response.setStatusCode(204);
// switch on the event type
switch(event.EventType) {
case 'task-queue.entered':
// ignore events that are not entering the 'Call Forward' TaskQueue
if (event.TaskQueueName !== 'Call Forward') {
console.log(`__[trEventStream]__: Entered ${event.TaskQueueName} queue - no forwarding required!`);
return callback(null, response);
}
console.log(`__[trEventStream]__: entered ${event.TaskQueueName} queue - forwarding call!`);
task = event.TaskSid;
workspace = event.WorkspaceSid;
const ta = JSON.parse(event.TaskAttributes);
const callSid = ta.call_sid;
let url = `https://${context.DOMAIN_NAME}/forwardCall`;
// redirect call to forwardCall function
client.calls(callSid).update({
method: 'POST',
url: encodeURI(url),
}).then(() => {
console.log(`__[trEventStream]__: [SUCCESS] ~> Task with id ${task} forwarded to external DID`);
// change task status to canceled so it doesn't appear in flex or show up as a pending task
client.taskrouter.workspaces(workspace)
.tasks(task)
.update({
assignmentStatus: 'canceled',
reason: 'Call forwarded'
})
.then(task => {
console.log(`__[trEventStream]__: [SUCCESS] ~> Task canceled`);
return callback(null, response);
}).catch(err => {
console.log(`__[trEventStream]__: [ERROR] ~> Task not marked complete: `, err);
// doesn't warrant reponse 500 since call still forwarded :)
return callback(null, response);
});
}).catch(err => {
console.log(`__[trEventStream]__: [ERROR] ~> Task failed to forward to external DID: `, err);
response.setStatusCode(500);
return callback(err, response);
});
break;
default:
return callback(null, response);
}
};
callForward.js:
exports.handler = function(context, event, callback) {
console.log(`forwarding call`);
// set-up the variables that this Function will use to forward a phone call using TwiML
// REQUIRED - you must set this
let phoneNumber = event.PhoneNumber || context.NUMBER;
// OPTIONAL
let callerId = event.CallerId || null;
// OPTIONAL
let timeout = event.Timeout || null;
// OPTIONAL
let allowedCallers = event.allowedCallers || [];
let allowedThrough = true;
if (allowedCallers.length > 0) {
if (allowedCallers.indexOf(event.From) === -1) {
allowedThrough = false;
}
}
// generate the TwiML to tell Twilio how to forward this call
let twiml = new Twilio.twiml.VoiceResponse();
let dialParams = {};
if (callerId) {
dialParams.callerId = callerId;
}
if (timeout) {
dialParams.timeout = timeout;
}
if (allowedThrough) {
twiml.dial(dialParams, phoneNumber); // making call :)
}
else {
twiml.say('Sorry, you are calling from a restricted number. Good bye.');
}
// return the TwiML
callback(null, twiml);
};
Any kind of help and/or guidance will be appreciated.

Twilio developer evangelist here.
When you redirect a call from a task, its task is cancelled with the reason "redirected" so you don't need to cancel it yourself.
Your code was failing to update the task occasionally because of a race condition between your code and the task getting cancelled by Twilio.

Related

How do I keep my script streaming so, it doesn't disconnecting?

So, I'm using filtered stream. Unfortunately, every time I turn it on, after 5 minutes its turns off but during that time, it catches tweets.
What I'm trying to do is keep it on 24/7 so, it doesn't turn off after 5 minutes. Also, if it disconnects I want it so, it tries connecting again.
This is the code sample I'm using to help adjust my code:
https://github.com/twitterdev/Twitter-API-v2-sample-code/blob/master/Filtered-Stream/filtered_stream.js
function streamTweets(retryAttempt) {
const stream = needle.get(streamURL, {
headers: {
Authorization: `Bearer ${TOKEN}`
},
retryAttempt: 20000
});
stream.on('data', (data) => {
try {
const json = JSON.parse(data)
console.log(json.data.text)
retryAttempt = 0;
} catch (e) {
if (data.detail === "This stream is currently at the maximum allowed connection limit.") {
console.log(data.detail)
process.exit(1)
} else {
// Keep alive signal received. Do nothing.
}
}
}).on('err', error => {
if (error.code !== 'ECONNRESET') {
console.log(error.code);
process.exit(1);
} else {
// This reconnection logic will attempt to reconnect when a disconnection is detected.
// To avoid rate limits, this logic implements exponential backoff, so the wait time
// will increase if the client cannot reconnect to the stream.
setTimeout(() => {
console.warn("A connection error occurred. Reconnecting...")
streamTweets(++retryAttempt);
}, 2 ** retryAttempt)
}
});
return stream;
}
(async() => {
let currentRules;
try {
//get all stream rules
currentRules = await getRules();
//delete all stream rules
await deleteRules(currentRules);
//Set rules based on array above
await setRules();
} catch (e) {
console.error(e);
process.exit(1);
}
streamTweets(0);
})();
Have you tried to send the keepalive in your header?
Also, I changed the authorization header to match the syntax that of your linked GitHub code source.
function streamTweets(retryAttempt) {
const stream = needle.get(streamURL, {
headers: {
"authorization": `Bearer ${token}`,
"Connection": "keep-alive"
},
retryAttempt: 20000
});
Also, according to the Twitter docs: " If you would like to close your connection, you can press Control-C in your command line tool on either Mac or Windows systems to break the connection, or you can also close the window. "
Are you sure it's not disconnecting because you closed the terminal session, or was closed by something like an ssh timeout?

How do I get a Twilio function to continue a call and return gather information

I have a studio flow that I am attempting to handle multiple different languages. I have a widget that starts the call and then passes it over to my function. However, after making the call and moving to the function, the call instantly ends. Am I doing something incorrect? From what I understand, I can send the call to a function to continue it. Is something wrong with my function? See my function code below.
exports.handler = function(context, event, callback) {
let twiml = new Twilio.twiml.VoiceResponse();
const gatherOptions = { Numdigit:"1", Timeout:"5"};
let sayOptions = { Voice:"Alice", Language: event.Language };
if(!event.Retries){
event.Retries = 0;
}
console.log(event.Language);
console.log(event.Body);
if (event.Digits) {
if(event.Digits === '9' && event.Retries < 3) {
event.Retries += 1;
twiml.gather(gatherOptions).say(sayOptions, event.Body);
} else if(event.Digits === '3' || event.Digits === '5'){
return callback(null, twiml);
}else {
twiml.say("sorry, I didnt get that.");
event.Digits = '9';
}
} else {
twiml.gather(gatherOptions).say(sayOptions, event.Body);
}
callback(null, twiml);
};
Outside of your Twilio Function code, anytime you jump out of Studio and return TwiML and then want to return to the Studio flow, you must use the TwiML Redirect Widget (which you can use to call the Twilio Function).
Your gatherOptions keys should be camelCase (i.e. numDigits, timeout).
This may be useful to carry state across Functions.
How to Share Information Between Your Applications

Twilio issue with task sitting in "Wrapping Up"

I'm testing out a new phone system design using Twilio TaskRouter, Studio, and Functions. I've gotten to the point that I can finish the call, but the task sits in "Wrapping Up" and won't allow a new call from the queue to go to the worker associated with that task until I physically delete that task. I've looked everywhere on how to close the task (get out of Wrapping Up), but can't find any good documentation anywhere.
I have a URL for the "Event Callbacks" of the TaskRouter and can capture exactly when the call moves to this EventType "task.wrapup", but don't know what to do at this point to move it past this step so it releases the task and worker.
So, with a little more digging I found the solution. For anyone coming here and having the problem I was having, here is the answer.
exports.handler = function(context, event, callback) {
let twiml = new Twilio.twiml.VoiceResponse();
let client = context.getTwilioClient();
switch(event.EventType) {
case 'task.wrapup':
let workspaceId = 'WSxxxxxxxxxxxxxxxxxxxxxxxxx';
console.log(event.TaskSid);
client.taskrouter.workspaces(workspaceId)
.tasks(event.TaskSid)
.update({
assignmentStatus: 'completed',
reason: 'Call completed'
})
.then(task => {
callback(null, twiml);
})
.catch(err => {
console.log(err);
callback(null, twiml);
});
break;
default:
callback(null, twiml);
break;
}
};
Hope this helps someone else :D
I ran into the same issues and came up with this solution. It's client-side Javascript based and using Twilio's taskrouter.js:
function registerTaskRouterCallbacks() {
worker.on("reservation.wrapup", function(reservation) {
worker.completeTask(reservation.task.sid,
function(error,completedTask) {
// Do stuff here if needed
});
});
}
window.onload = function() {
// Initialize TaskRouter.js on page load using window.workerToken -
// a Twilio Capability token that was set in a <script> in associated web page (PHP, etc.) script
window.worker = new Twilio.TaskRouter.Worker(workerToken);
registerTaskRouterCallbacks();
};

Twilio Functions Error 20429 - Too many requests multiple sms messages

I am using Twilio functions and Programable SMS to send SMS Messages to a list of numbers form my iOS App. There are just over 100 mobile numbers (113 on the time of this error) in the list. Most of these messages send but then the function says that it timed out after 502ms.
I am using the example code from Twilio to send to group messages (that I have copied below) and making a URLSession request from my iOS app.
Is there a way that I can fix this issue so that I can send to this fairly large list of phone numbers or make the function run for longer?
Thank you very much for your help.
Tom
Request:
let messagesPhoneNumberString = [+447987654321abc,+447123789456def,+447123456789ghi]
"https://myfunction.twil.io/send-messages?phoneAndID=\(messagesPhoneNumberString)&globalID=\(current globalID)"
My Twilio Function Code:
exports.handler = function(context, event, callback) {
let phoneAndIDString = event['phoneAndID'];
let globalID String = event['globalID'];
let numbersArray = phoneAndIDString.split(",");
Promise.all(
numbersArray(number => {
let phoneNumberSplit = "+" + number.slice(1, 13);
let idSplit = number.slice(13);
console.log('Send to number: ' + phoneNumberSplit + ' - with ID: ' + idSplit);
return context.getTwilioClient().messages.create({
to: phoneNumberSplit,
from: 'Example',
body: 'Hello World: ' + idSplit
});
})
)
.then(messages => {
console.log('Messages sent!');
callback(null, response);
})
.catch(err => console.error(err));
};
Twilio developer evangelist here.
Twilio Functions has a timeout of 5 seconds, so it is likely not the best idea to use a Twilio Function to send that many messages in one go.
You have some options though.
If you are sending all those numbers the same message then you could use the Twilio Notify passthrough API. Check out the details in this blog post about sending mass messages with Node.js.
Otherwise, if you have to send different messages then you could split up the numbers into batches and use the same function multiple times.
Finally, you could use a different platform to send the messages that doesn't have a 5 second limit.
In addition to the options provided in Phil's answer you could use recursion.
You could trigger the process from your app and pass all numbers in the initial function call just like you do now.
Then, the idea is to send just one message per function call and let the Twilio function call itself after it receives the response from .create(). This means no concurent calls to send messages, messages are sent one after another though the order in which they are received is not necessary the order in which the numbers are passed in the query string.
You'll need to add axios in the function dependencies configuration (https://www.twilio.com/console/runtime/functions/configure).
Axios is used to make the HTTP request to the function from within the function.
Each function run, tests for the stop condition which happens when the phone numbers query string length is zero. Then, uses .shift() to remove the first element from the numbers array to work with it. The remaining array is passed to the next function call.
This is the code I've tried, and it worked for me, but you'll have to change (the 11 length on .slice() method) for +44 because I've tested with US numbers +1 which are shorter in length.
exports.handler = function(context, event, callback) {
const axios = require("axios");
let phoneAndIDString = event["phoneAndID"].trim();
console.log(phoneAndIDString);
let globalIDString = event["globalID"].trim();
// stop condition for recursive call
if (phoneAndIDString.length === 0) {
return callback(null, true);
}
let numbersArray = phoneAndIDString.split(",");
console.log(numbersArray);
// take the first item of array
let number = numbersArray.shift();
console.log(number);
// the remaining array will be passed to the next function call
phoneAndIDString = numbersArray.join();
console.log(phoneAndIDString);
let phoneNumberSplit = "+" + number.slice(0, 11);
let idSplit = number.slice(11);
console.log("Send to number: " + phoneNumberSplit + " - with ID: " + idSplit);
context
.getTwilioClient()
.messages.create({
to: phoneNumberSplit,
from: "+17775553333",
body: "Hello World: " + idSplit
})
.then(msg => {
console.log("Message sent: " + msg.sid);
axios
.get(
"https://foo-bar-1234.twil.io/send-messages?globalID=baz&phoneAndID=" +
phoneAndIDString
)
.then(function(response) {
// handle success
console.log(response.status);
return callback(null, true);
})
.catch(function(err) {
// handle error
console.error(err);
});
})
.catch(function(err) {
console.error(err);
});
};
Try to go step by step with it while testing, console log things and then return early with return callback(null, true) as you go from top to bottom so you make sure you don't go in a loop.

Pass custom data to service worker sync?

I need to make a POST request and send some data. I'm using the service worker sync to handle offline situation.
But is there a way to pass the POST data to the service worker, so it makes the same request again?
Cause apparently the current solution is to store requests in some client side storage and after client gets connection - get the requests info from the storage and then send them.
Any more elegant way?
PS: I thought about just making the service worker send message to the application code so it does the request again ... but unfortunately it doesn't know the exact client that registered the service worker :(
You can use fetch-sync
or i use postmessage to fix this problem, which i agree that indexedDB looks trouble.
first of all, i send the message from html.
// send message to serviceWorker
function sync (url, options) {
navigator.serviceWorker.controller.postMessage({type: 'sync', url, options})
}
i got this message in serviceworker, and then i store it.
const syncStore = {}
self.addEventListener('message', event => {
if(event.data.type === 'sync') {
// get a unique id to save the data
const id = uuid()
syncStore[id] = event.data
// register a sync and pass the id as tag for it to get the data
self.registration.sync.register(id)
}
console.log(event.data)
})
in the sync event, i got the data and fetch
self.addEventListener('sync', event => {
// get the data by tag
const {url, options} = syncStore[event.tag]
event.waitUntil(fetch(url, options))
})
it works well in my test, what's more you can delete the memory store after the fetch
what's more, you may want to send back the result to the page. i will do this in the same way by postmessage.
as now i have to communicate between each other, i will change the fucnction sync into this way
// use messagechannel to communicate
sendMessageToSw (msg) {
return new Promise((resolve, reject) => {
// Create a Message Channel
const msg_chan = new MessageChannel()
// Handler for recieving message reply from service worker
msg_chan.port1.onmessage = event => {
if(event.data.error) {
reject(event.data.error)
} else {
resolve(event.data)
}
}
navigator.serviceWorker.controller.postMessage(msg, [msg_chan.port2])
})
}
// send message to serviceWorker
// you can see that i add a parse argument
// this is use to tell the serviceworker how to parse our data
function sync (url, options, parse) {
return sendMessageToSw({type: 'sync', url, options, parse})
}
i also have to change the message event, so that i can pass the port to sync event
self.addEventListener('message', event => {
if(isObject(event.data)) {
if(event.data.type === 'sync') {
// in this way, you can decide your tag
const id = event.data.id || uuid()
// pass the port into the memory stor
syncStore[id] = Object.assign({port: event.ports[0]}, event.data)
self.registration.sync.register(id)
}
}
})
up to now, we can handle the sync event
self.addEventListener('sync', event => {
const {url, options, port, parse} = syncStore[event.tag] || {}
// delete the memory
delete syncStore[event.tag]
event.waitUntil(fetch(url, options)
.then(response => {
// clone response because it will fail to parse if it parse again
const copy = response.clone()
if(response.ok) {
// parse it as you like
copy[parse]()
.then(data => {
// when success postmessage back
port.postMessage(data)
})
} else {
port.postMessage({error: response.status})
}
})
.catch(error => {
port.postMessage({error: error.message})
})
)
})
At the end. you cannot use postmessage to send response directly.Because it's illegal.So you need to parse it, such as text, json, blob, etc. i think that's enough.
As you have mention that, you may want to open the window.
i advice that you can use serviceworker to send a notification.
self.addEventListener('push', function (event) {
const title = 'i am a fucking test'
const options = {
body: 'Yay it works.',
}
event.waitUntil(self.registration.showNotification(title, options))
})
self.addEventListener('notificationclick', function (event) {
event.notification.close()
event.waitUntil(
clients.openWindow('https://yoursite.com')
)
})
when the client click we can open the window.
To comunicate with the serviceworker I use a trick:
in the fetch eventlistener I put this:
self.addEventListener('fetch', event => {
if (event.request.url.includes("sw_messages.js")) {
var zib = "some data";
event.respondWith(new Response("window.msg=" + JSON.stringify(zib) + ";", {
headers: {
'Content-Type': 'application/javascript'
}
}));
}
return;
});
then, in the main html I just add:
<script src="sw_messages.js"></script>
as the page loads, global variable msg will contain (in this example) "some data".

Resources