Source: Dialog.js

module.exports = Dialog;


var C = {
  // Dialog states
  STATUS_EARLY:       1,
  STATUS_CONFIRMED:   2
};

/**
 * Expose C object.
 */
Dialog.C = C;


/**
 * Dependencies.
 */
var debug = require('debug')('JsSIP:Dialog');
var SIPMessage = require('./SIPMessage');
var JsSIP_C = require('./Constants');
var Transactions = require('./Transactions');
var Dialog_RequestSender = require('./Dialog/RequestSender');


// RFC 3261 12.1
function Dialog(owner, message, type, state) {
  var contact;

  this.uac_pending_reply = false;
  this.uas_pending_reply = false;

  if(!message.hasHeader('contact')) {
    return {
      error: 'unable to create a Dialog without Contact header field'
    };
  }

  if(message instanceof SIPMessage.IncomingResponse) {
    state = (message.status_code < 200) ? C.STATUS_EARLY : C.STATUS_CONFIRMED;
  } else {
    // Create confirmed dialog if state is not defined
    state = state || C.STATUS_CONFIRMED;
  }

  contact = message.parseHeader('contact');

  // RFC 3261 12.1.1
  if(type === 'UAS') {
    this.id = {
      call_id: message.call_id,
      local_tag: message.to_tag,
      remote_tag: message.from_tag,
      toString: function() {
        return this.call_id + this.local_tag + this.remote_tag;
      }
    };
    this.state = state;
    this.remote_seqnum = message.cseq;
    this.local_uri = message.parseHeader('to').uri;
    this.remote_uri = message.parseHeader('from').uri;
    this.remote_target = contact.uri;
    this.route_set = message.getHeaders('record-route');
  }
  // RFC 3261 12.1.2
  else if(type === 'UAC') {
    this.id = {
      call_id: message.call_id,
      local_tag: message.from_tag,
      remote_tag: message.to_tag,
      toString: function() {
        return this.call_id + this.local_tag + this.remote_tag;
      }
    };
    this.state = state;
    this.local_seqnum = message.cseq;
    this.local_uri = message.parseHeader('from').uri;
    this.remote_uri = message.parseHeader('to').uri;
    this.remote_target = contact.uri;
    this.route_set = message.getHeaders('record-route').reverse();
  }

  this.owner = owner;
  owner.ua.dialogs[this.id.toString()] = this;
  debug('new ' + type + ' dialog created with status ' + (this.state === C.STATUS_EARLY ? 'EARLY': 'CONFIRMED'));
}


Dialog.prototype = {
  update: function(message, type) {
    this.state = C.STATUS_CONFIRMED;

    debug('dialog '+ this.id.toString() +'  changed to CONFIRMED state');

    if(type === 'UAC') {
      // RFC 3261 13.2.2.4
      this.route_set = message.getHeaders('record-route').reverse();
    }
  },

  terminate: function() {
    debug('dialog ' + this.id.toString() + ' deleted');
    delete this.owner.ua.dialogs[this.id.toString()];
  },

  // RFC 3261 12.2.1.1
  createRequest: function(method, extraHeaders, body) {
    var cseq, request;
    extraHeaders = extraHeaders && extraHeaders.slice() || [];

    if(!this.local_seqnum) { this.local_seqnum = Math.floor(Math.random() * 10000); }

    cseq = (method === JsSIP_C.CANCEL || method === JsSIP_C.ACK) ? this.local_seqnum : this.local_seqnum += 1;

    request = new SIPMessage.OutgoingRequest(
      method,
      this.remote_target,
      this.owner.ua, {
        'cseq': cseq,
        'call_id': this.id.call_id,
        'from_uri': this.local_uri,
        'from_tag': this.id.local_tag,
        'to_uri': this.remote_uri,
        'to_tag': this.id.remote_tag,
        'route_set': this.route_set
      }, extraHeaders, body);

    request.dialog = this;

    return request;
  },

  // RFC 3261 12.2.2
  checkInDialogRequest: function(request) {
    var self = this;

    if(!this.remote_seqnum) {
      this.remote_seqnum = request.cseq;
    } else if(request.cseq < this.remote_seqnum) {
        //Do not try to reply to an ACK request.
        if (request.method !== JsSIP_C.ACK) {
          request.reply(500);
        }
        return false;
    } else if(request.cseq > this.remote_seqnum) {
      this.remote_seqnum = request.cseq;
    }

    // RFC3261 14.2 Modifying an Existing Session -UAS BEHAVIOR-
    if (request.method === JsSIP_C.INVITE || (request.method === JsSIP_C.UPDATE && request.body)) {
      if (this.uac_pending_reply === true) {
        request.reply(491);
      } else if (this.uas_pending_reply === true) {
        var retryAfter = (Math.random() * 10 | 0) + 1;
        request.reply(500, null, ['Retry-After:'+ retryAfter]);
        return false;
      } else {
        this.uas_pending_reply = true;
        request.server_transaction.on('stateChanged', function stateChanged(e){
          if (e.sender.state === Transactions.C.STATUS_ACCEPTED ||
              e.sender.state === Transactions.C.STATUS_COMPLETED ||
              e.sender.state === Transactions.C.STATUS_TERMINATED) {

            request.server_transaction.removeListener('stateChanged', stateChanged);
            self.uas_pending_reply = false;

            if (self.uac_pending_reply === false) {
              self.owner.onReadyToReinvite();
            }
          }
        });
      }

      // RFC3261 12.2.2 Replace the dialog`s remote target URI if the request is accepted
      if(request.hasHeader('contact')) {
        request.server_transaction.on('stateChanged', function(e){
          if (e.sender.state === Transactions.C.STATUS_ACCEPTED) {
            self.remote_target = request.parseHeader('contact').uri;
          }
        });
      }
    }
    else if (request.method === JsSIP_C.NOTIFY) {
      // RFC6665 3.2 Replace the dialog`s remote target URI if the request is accepted
      if(request.hasHeader('contact')) {
        request.server_transaction.on('stateChanged', function(e){
          if (e.sender.state === Transactions.C.STATUS_COMPLETED) {
            self.remote_target = request.parseHeader('contact').uri;
          }
        });
      }
    }

    return true;
  },

  sendRequest: function(applicant, method, options) {
    options = options || {};

    var
      extraHeaders = options.extraHeaders && options.extraHeaders.slice() || [],
      body = options.body || null,
      request = this.createRequest(method, extraHeaders, body),
      request_sender = new Dialog_RequestSender(this, applicant, request);

      request_sender.send();
  },

  receiveRequest: function(request) {
    //Check in-dialog request
    if(!this.checkInDialogRequest(request)) {
      return;
    }

    this.owner.receiveRequest(request);
  }
};