1
votes

I already had this function in my Twilio Functions section, although I'm not sure if I copy-pasted this from somewhere a while ago or if it came by default:

var moment = require('moment-timezone')

exports.handler = function(context, event, callback) {

  let now = moment().tz('Australia/Brisbane');

  let isWorkday = (now.isoWeekday() < 6);
  let isWorkingHour = (now.hour() > 7 && now.hour() < 17);
  let response = {};


  if(isWorkday && isWorkingHour) {
    callback(null, response);
  } else {
    callback("Service is closed");
  }  
};

I also found another SO post where someone included a basic function to divert to voicemail if a call is unanswered:

exports.handler = function(context, event, callback) {
    const twiml = new Twilio.twiml.VoiceResponse();
    if (event.DialCallStatus === 'completed' || event.DialCallStatus === 'answered') {
        twiml.hangup();
    } else {
        twiml.say("Service is closed");
        twiml.record({
            transcribe: true,
            transcribeCallback: "http://twimlets.com/[email protected]",
             action: "/hangup"
        });
    }
    callback(null, twiml);
};

What I want to do is basically combine these two so that:

  1. If call is received where !isWorkday || !isWorkingHour then send straight to voicemail. Don't ring the phone at all.
  2. If call is receive where isWorkday && isWorkingHour then run something like this Twiml bin:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Dial>
    <Sip>
      [email protected];region=au1
    </Sip>
  </Dial>
</Response>
  1. If the call is not answered within 20 seconds then send to voicemail (with a different greeting to step 1).

Bonus question: I obviously also need to be able to listen to the voicemail as I doubt the transcribing will be very accurate. Is there any way to include a link to the voicemail (or the voicemail mp3 itself) in the email I receive when a new voicemail is created? Or can I create a function/twiml bin for outgoing calls that would let me dial a number and listen to my voicemails, like how normal voicemail works??

1
Looks like you have all the ingredients there. Is something causing you trouble? Are you getting an error? What have you tried so far?philnash
I've got the ingredients but I don't know the recipe. I'm not sure how to tie it all together. How do I convert the callback(null, response) in the first code block to be the Twiml Bin in the third code block, then how do I tell it to try for 20 seconds before diverting to voicemail? I'm guessing I should replace callback("Service is closed"); in the first code block with the else statement in the second code block. Then I also have no idea at all about how to do the bonus question at the bottom. The documentation is very scattered and lacking for Twilio.Jake
Looks like Stefan beat me to getting back to this, but I'd check out his answer. Can I ask what was lacking in the documentation though? Is it an issue with the Functions docs, how the TwiML should work together, how to generate TwiML with Node or something else? If you want to email me at [email protected], I'd be interested to hear where we could improve.philnash
Well for example if you go to the Functions documentation (twilio.com/docs/runtime/functions) it doesn't mention anything at all about twiml.dial, twiml.record, etc. That's all located elsewhere. It also doesn't explain anywhere how to do twiml.dial.sip. And if I search for how to setup voicemail I only find disjointed guides for different products. There's also no guides on how to merge different functions or twimlets. The guides are all over the place, it needs to be organised a lot better with each section being complete and independent of other guides.Jake
Ah, ok, yeah. The Functions documentation covers how to run Node.js code in the Functions environment. The documentation for the TwiML verbs, including the Node.js helper library, is under the Voice documentation (<Sip> can be found here). As fof functions and Twimlets we have a project to turn all Twimlets into Functions which should simplify that too.philnash

1 Answers

3
votes

Heyo, Twilio Developer Evangelist here. 👋

I just set down and built your use case in a single function. There is a lot going on inside of this one function (and I might recommend splitting the work into several functions).

I'm happy to answer your bonus questions, too, but please open it separately to keep this answer focused. :)

Let's have a look at a working example!

const moment = require('moment-timezone');

function isServiceOpen() {
  let now = moment().tz('Australia/Brisbane');
  let isWorkday = now.isoWeekday() < 6;
  let isWorkingHour = now.hour() > 7 && now.hour() < 17;

  return isWorkday && isWorkingHour;
}

exports.handler = function(context, event, callback) {
  const twiml = new Twilio.twiml.VoiceResponse();

  console.log(event);

  const callIncludesRecording = !!event.RecordingUrl;
  const callWasAnswered = event.DialCallStatus === 'completed';
  const callWasNotAnswered = event.DialCallStatus === 'no-answer';
  const serviceIsOpen = isServiceOpen();

  if (callWasAnswered) {
    twiml.hangup();
  } else if (callIncludesRecording) {
    console.log('Call includes recording!');

    // do something with the recording URL here
    console.log(event.RecordingUrl);

    twiml.say("Thank you! We'll come back to you shortly.");
    twiml.hangup();
  } else if (callWasNotAnswered) {
    console.log('Call was not answered...');

    twiml.say(
      'Unfortunately no one can answer right now. But you can leave a message.'
    );
    twiml.record({
      action: '/handle-call'
    });
  } else if (!serviceIsOpen) {
    console.log('Service is closed...');
    twiml.say('Service is closed but you can leave a message');
    twiml.record({
      action: '/handle-call'
    });
  } else {
    twiml.dial(
      {
        action: '/handle-call',
        method: 'POST',
        timeout: 5
      },
      '+4915...'
    );
  }

  callback(null, twiml);
};

The function you see above is available under a /handle-call endpoint and answers all webhooks for a call.

Scenario 1 - a call is not answered

At the end of the function, you see the dial function call. The important piece for this case is that dial supports a timeout and an action attribute.

twiml.dial(
  {
    action: '/handle-call',
    method: 'POST',
    timeout: 30
  },
  '+49157...'
);

The above tells Twilio to try to call the number +49157... for 30 seconds (it's actually closer to 35 – you can read details in the docs). If the call ends or no one answered the call until the timeout is reached Twilio will ask the defined action URL for additional TwiML configuration.

The URL in the action attribute references the same function path (/handle-call) and the same function will be executed again but this time the event object will include a DialCallStatus of no-answer (have a look at the variable callWasNotAnswered). If the call was not answered you can return TwiML to say a message and tell the API to start the recording of the call.

// no one answered – let's record
twiml.say(
  'Unfortunately, no one can answer right now. But you can leave a message.'
);
twiml.record({
  action: '/handle-call'
});

The record verb also allows an action attribute which lets you define a URL that should be requested when the recording finished (we'll use again the same function under the same /handle-call endpoint).

The call to the same URL will then include a RecordingUrl inside of the event object. If this property is present you know that it is the result of the recording. At this stage, it is time to do something with the recording URL (send a message, log it, ...), to finish the call after saying "goodbye" and to hang up.

// do something with the recording URL here
console.log(event.RecordingUrl);

twiml.say("Thank you! We'll come back to you shortly.");
twiml.hangup();

The webhook flow is as follows:

  1. POST /handle-call (initial webhook) -> dial +49157...
  2. POST /handle-call (call after time out) -> say "unfortunately, ..." & record
  3. POST /handle-call (recording finished) -> say "thank you" & hangup

Scenario 2 - the call is outside of business hours

For this scenario, I took the logic that you already provided and created a isServiceOpen helper function. When a call comes in outside of business hours the function responds with TwiML defining a message and a recording.

    twiml.say('Service is closed but you can leave a message');
    twiml.record({
      action: '/handle-call'
    });

After the finished recording the our function will be called (/handle-call). This time, the request will include a RecordingUrl and the handling will be the same as in scenario 1 (log the recording URL and hangup).

The webhook flow is as follows:

  1. POST /handle-call (initial webhook) -> say "service is closed" & record
  2. POST /handle-call (recording finished) -> say "thank you" & hangup

Scenario 3 – the call was answered

In case the call happens in business hours and was answered quickly enough there won't be a need for a recording. Because the dial verb includes an action attribute (we used this in scenario 1) another webhook will be sent after the call is ended.

This webhook will include a DialCallStatus parameter with the completed value. Then it's time to end the call and hang up.

twiml.hangup();
  1. POST /handle-call (initial webhook) -> dial "+49157..."
  2. POST /handle-call (call finished) -> hangup

I hope the above helps. 😊 As mentioned initially, it might be a good idea to split the functionality into /handle-call, /handle-recording and other functions.

Let me know if that helps and you have any further questions. :)