FreeSwitch ESL for Node.js

[Updated 2012-02-29]

This article uses the old API (pre-0.2) for the esl module; please refer to the documentation for updated examples.

You may also want to look at these two example: a script to modify a channel variable based on a web query and a CouchDB-backed voicemail application.

[Updated 2011-12-05]

esl is a Node.js module that provides both (fully asynchronous) client and server implementations. [Source] [On Github]

The Documentation is now part of the (independent) package.

The module is available on the npm registry. To install:

npm install esl
(The module is written in CoffeeScript but can be used from Javascript/Node.js just like any other module.)

Here is an example (first in CoffeeScript, then in Javascript) for a client which opens a connection, sends one arbitrary API command, and disconnects.

###
(c) 2010 Stephane Alnet
Released under the AGPL3 license
###

esl = require 'esl'

fs_command = (cmd,cb) ->
  client = esl.createClient()
  client.on 'esl_auth_request', (req,res) ->
    res.auth 'ClueCon', (req,res) ->
      res.api cmd, (req,res) ->
        res.exit ->
          client.end()
  if cb?
    client.on 'close', cb
  client.connect(8021, '127.0.0.1')

# Equivalent of the command line  "fs_cli -x reloadxml"
fs_command "reloadxml"

The same code in Javascript (thanks to coffee -c).

var esl, fs_command;
esl = require('esl');
fs_command = function(cmd, cb) {
  var client;
  client = esl.createClient();
  client.on('esl_auth_request', function(req, res) {
    return res.auth('ClueCon', function(req, res) {
      return res.api(cmd, function(req, res) {
        return res.exit(function() {
          return client.end();
        });
      });
    });
  });
  if (cb != null) {
    client.on('close', cb);
  }
  return client.connect(8021, '127.0.0.1');
};
fs_command("reloadxml");

Here is an example (in CoffeeScript) for a prepaid server. This uses a CouchDB database to retrieve and store data, and has some extraneous instrumentation to support the underlying project, but should give you a good idea of how to build a moderately complex application. [Source of the complete application]

###
(c) 2010 Stephane Alnet
Released under the AGPL3 license
###
# This script expects the following variables:
#   ccnq_account       -- account to be decremented
#   target             -- where to bridge the call
#   prepaid_uri        -- URI for the prepaid API

esl = require 'esl'
util = require 'util'
querystring = require 'querystring'
cdb = require 'cdb'

require('ccnq3_config').get (config)->

  # esl.debug = true

  Unique_ID = 'Unique-ID'

  server = esl.createServer (res) ->

    res.connect (req,res) ->

      # Retrieve channel parameters
      channel_data = req.body

      unique_id             = channel_data[Unique_ID]
      util.log "Incoming call UUID = #{unique_id}"

      prepaid_account      = channel_data.variable_ccnq_account
      prepaid_destination  = channel_data.variable_target

      prepaid_cdb = cdb.new config.prepaid.uri

      # Common values
      interval_id = null

      on_disconnect = (req,res) ->
        util.log "Receiving disconnection"
        switch req.headers['Content-Disposition']
          when 'linger'      then res.exit()
          when 'disconnect'  then res.end()

      res.on 'esl_disconnect_notice', on_disconnect


      force_disconnect = (res) ->
        util.log 'Hangup call'
        res.bgapi "uuid_kill #{unique_id}"

      prepaid_cdb.exists (it_does) ->
        if not it_does
          util.log "Database #{channel_data.prepaid_uri} is not accessible."
          return force_disconnect(res)

        # Get account parameters
        prepaid_cdb.get prepaid_account, (r) ->
          if r.error
            util.log "Account #{prepaid_account} Error: #{r.error}"
            return force_disconnect(res)

          interval_duration = r.interval_duration # seconds
          util.log "Account #{prepaid_account} interval duration is #{interval_duration} seconds."

          check_time = (cb) ->
            util.log "Checking account #{prepaid_account}."
            account_key = "\"#{prepaid_account}\""
            options =
              uri: "/_design/prepaid/_view/current?reduce=true&group=true&key=#{querystring.escape(account_key)}"
            prepaid_cdb.req options, (r) ->
              if r.error
                util.log "Error: #{r.error}"
                return force_disconnect(res)

              intervals_remaining = r?.rows?[0]?.value

              if intervals_remaining? and intervals_remaining > 1
                util.log "Account #{prepaid_account} has #{intervals_remaining} intervals left."
                return cb?()

              util.log "Account #{prepaid_account} is exhausted."
              return force_disconnect(res)

          # During call progress, check every ten seconds whether
          # the account is exhausted.
          interval_id = setInterval check_time, 10*1000

          record_interval = (intervals,cb) ->
            util.log "Recording #{intervals} intervals for account #{prepaid_account}."
            rec =
              type: 'interval_record'
              account: prepaid_account
              intervals: - intervals

            prepaid_cdb.post rec, (r) ->
              if r.error
                util.log "Error: #{r.error}"
                return force_disconnect(res)

              util.log "Recorded #{intervals} intervals for account #{prepaid_account}."
              cb?()

          each_interval = (cb) ->
            record_interval 1, () ->
              check_time(cb)

          # Handle ANSWER event
          on_answer = (req,res) ->
            util.log "Call was answered"

            # Clear the ringback timer
            clearInterval interval_id
            interval_id = null

            # First interval for the connected call
            each_interval()

            # Set the in-call timer
            interval_id = setInterval each_interval, interval_duration*1000

            util.log "Call answer processed."

          res.on 'esl_event', (req,res) ->
            switch req.body['Event-Name']

              when 'CHANNEL_ANSWER'
                on_answer(req,res)

              when 'CHANNEL_HANGUP_COMPLETE'
                util.log 'Channel hangup complete'
                clearInterval interval_id
                interval_id = null

              else
                util.log "Unhandled event #{req.body['Event-Name']}"

          on_connect = (req,res) ->

              # Check whether the call can proceed
              check_time () ->

                util.log 'Bridging call'
                res.execute 'bridge', prepaid_destination, (req,res) ->
                  util.log "Call bridged"

          # Handle the incoming connection
          res.linger (req,res) ->
            res.filter Unique_ID, unique_id, (req,res) ->
              res.event_json 'ALL', on_connect

  server.listen(config.prepaid.port)
posted: 2011-11-10 23:09

Written on November 10, 2011