nl_tokens: / * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pl_tokens: function Conversation ( task , message ) { this . messages = [ ] ; this . sent = [ ] ; this . transcript = [ ] ; this . context = { user : message . user , channel : message . channel , bot : task . bot , } ; this . events = { } ; this . vars = { } ; this . threads = { } ; this . thread = null ; this . status = 'new' ; this . task = task ; this . source_message = message ; this . handler = null ; this . responses = { } ; this . capture_options = { } ; this . startTime = new Date ( ) ; this . lastActive = new Date ( ) ; /** will be pointing to a callback which will be called after timeout,
       * conversation will be not be ended and should be taken care by callback
       */ this . timeOutHandler = null ; this . collectResponse = function ( key , value ) { this . responses [ key ] = value ; } ; this . capture = function ( response , cb ) { var that = this ; var capture_key = this . sent [ this . sent . length - 1 ] . text ; botkit . middleware . capture . run ( that . task . bot , response , that , function ( err , bot , response , convo ) { if ( response . text ) { response . text = response . text . trim ( ) ; } else { response . text = '' ; } if ( that . capture_options . key != undefined ) { capture_key = that . capture_options . key ; } // capture the question that was asked // if text is an array, get 1st if ( typeof ( that . sent [ that . sent . length - 1 ] . text ) == 'string' ) { response . question = that . sent [ that . sent . length - 1 ] . text ; } else if ( Array . isArray ( that . sent [ that . sent . <mask> - 1 ] . text ) ) { response . question = that . sent [ that . sent . length - 1 ] . text [ 0 ] ; } else { response . question = '' ; } if ( that . capture_options . multiple ) { if ( ! that . responses [ capture_key ] ) { that . responses [ capture_key ] = [ ] ; } that . responses [ capture_key ] . push ( response ) ; } else { that . responses [ capture_key ] = response ; } if ( cb ) cb ( response ) ; } ) ; } ; this . handle = function ( message ) { var that = this ; this . lastActive = new Date ( ) ; this . transcript . push ( message ) ; botkit . logger . debug ( 'HANDLING MESSAGE IN CONVO' , message ) ; // do other stuff like call custom callbacks if ( this . handler ) { this . capture ( message , function ( message ) { // if the handler is a normal function, just execute it! // NOTE: anyone who passes in their own handler has to call // convo.next() to continue after completing whatever it is they want to do. if ( typeof ( that . handler ) == 'function' ) { that . handler ( message , that ) ; } else { // handle might be a mapping of keyword to callback. // lets see if the message matches any of the keywords let match , patterns = that . handler ; for ( let p = 0 ; p < patterns . length ; p ++ ) { if ( patterns [ p ] . pattern && botkit . hears_test ( [ patterns [ p ] . pattern ] , message ) ) { botkit . middleware . heard . run ( that . task . bot , message , function ( err , bot , message ) { patterns [ p ] . callback ( message , that ) ; } ) ; return ; } } // none of the messages matched! What do we do? // if a default exists, fire it! for ( let p = 0 ; p < patterns . length ; p ++ ) { if ( patterns [ p ] . default ) { botkit . middleware . heard . run ( that . task . bot , message , function ( err , bot , message ) { patterns [ p ] . callback ( message , that ) ; } ) ; return ; } } } } ) ; } else { // do nothing } } ; this . setVar = function ( field , value ) { if ( ! this . vars ) { this . vars = { } ; } this . vars [ field ] = value ; } ; this . activate = function ( ) { this . task . trigger ( 'conversationStarted' , [ this ] ) ; this . task . botkit . trigger ( 'conversationStarted' , [ this . task . bot , this ] ) ; this . status = 'active' ; } ; /**
       * active includes both ACTIVE and ENDING
       * in order to allow the timeout end scripts to play out
       **/ this . isActive = function ( ) { return ( this . status == 'active' || this . status == 'ending' ) ; } ; this . deactivate = function ( ) { this . status = 'inactive' ; } ; this . say = function ( message ) { this . addMessage ( message ) ; } ; this . sayFirst = function ( message ) { if ( typeof ( message ) == 'string' ) { message = { text : message , channel : this . source_message . channel , } ; } else { message . channel = this . source_message . channel ; } this . messages . unshift ( message ) ; } ; this . on = function ( event , cb ) { botkit . logger . debug ( 'Setting up a handler for' , event ) ; var events = event . split ( / \, / g ) ; for ( var e in events ) { if ( ! this . events [ events [ e ] ] ) { this . events [ events [ e ] ] = [ ] ; } this . events [ events [ e ] ] . push ( cb ) ; } return this ; } ; this . trigger = function ( event , data ) { if ( this . events [ event ] ) { for ( var e = 0 ; e < this . events [ event ] . length ; e ++ ) { var res = this . events [ event ] [ e ] . apply ( this , data ) ; if ( res === false ) { return ; } } } else { } } ; // proceed to the next message after waiting for an answer this . next = function ( ) { this . handler = null ; } ; this . repeat = function ( ) { if ( this . sent . length ) { this . messages . push ( this . sent [ this . sent . length - 1 ] ) ; } else { // console.log('TRIED TO REPEAT, NOTHING TO SAY'); } } ; this . silentRepeat = function ( ) { return ; } ; this . addQuestion = function ( message , cb , capture_options , thread ) { if ( typeof ( message ) == 'string' ) { message = { text : message , channel : this . source_message . channel } ; } else { message . channel = this . source_message . channel ; } if ( capture_options ) { message . capture_options = capture_options ; } message . handler = cb ; this . addMessage ( message , thread ) ; } ; this . ask = function ( message , cb , capture_options ) { this . addQuestion ( message , cb , capture_options , this . thread || 'default' ) ; } ; this . addMessage = function ( message , thread ) { if ( ! thread ) { thread = this . thread ; } if ( typeof ( message ) == 'string' ) { message = { text : message , channel : this . source_message . channel , } ; } else { message . channel = this . source_message . channel ; } if ( ! this . threads [ thread ] ) { this . threads [ thread ] = [ ] ; } this . threads [ thread ] . push ( message ) ; // this is the current topic, so add it here as well if ( this . thread == thread ) { this . messages . push ( message ) ; } } ; // how long should the bot wait while a user answers? this . setTimeout = function ( timeout ) { this . task . timeLimit = timeout ; } ; // For backwards compatibility, wrap gotoThread in its previous name this . changeTopic = function ( topic ) { this . gotoThread ( topic ) ; } ; this . hasThread = function ( thread ) { return ( this . threads [ thread ] != undefined ) ; } ; this . transitionTo = function ( thread , message ) { // add a new transition thread // add this new message to it // set that message action to execute the actual transition // then change threads to transition thread var num = 1 ; while ( this . hasThread ( 'transition_' + num ) ) { num ++ ; } var threadname = 'transition_' + num ; if ( typeof ( message ) == 'string' ) { message = { text : message , action : thread } ; } else { message . action = thread ; } this . addMessage ( message , threadname ) ; this . gotoThread ( threadname ) ; } ; this . beforeThread = function ( thread , callback ) { if ( ! this . before_hooks ) { this . before_hooks = { } ; } if ( ! this . before_hooks [ thread ] ) { this . before_hooks [ thread ] = [ ] ; } this . before_hooks [ thread ] . push ( callback ) ; } ; this . gotoThread = function ( thread ) { var that = this ; that . next_thread = thread ; that . processing = true ; var makeChange = function ( ) { if ( ! that . hasThread ( that . next_thread ) ) { if ( that . next_thread == 'default' ) { that . threads [ that . next_thread ] = [ ] ; } else { botkit . logger . debug ( 'WARN: gotoThread() to an invalid thread!' , thread ) ; that . stop ( 'unknown_thread' ) ; return ; } } that . thread = that . next_thread ; that . messages = that . threads [ that . next_thread ] . slice ( ) ; that . handler = null ; that . processing = false ; } ; if ( that . before_hooks && that . before_hooks [ that . next_thread ] ) { // call any beforeThread hooks in sequence async . eachSeries ( this . before_hooks [ that . next_thread ] , function ( before_hook , next ) { before_hook ( that , next ) ; } , function ( err ) { if ( ! err ) { makeChange ( ) ; } } ) ; } else { makeChange ( ) ; } } ; this . combineMessages = function ( messages ) { if ( ! messages ) { return '' ; } if ( Array . isArray ( messages ) && ! messages . length ) { return '' ; } if ( messages . length > 1 ) { var txt = [ ] ; var last_user = null ; var multi_users = false ; last_user = messages [ 0 ] . user ; for ( let x = 0 ; x < messages . length ; x ++ ) { if ( messages [ x ] . user != last_user ) { multi_users = true ; } } last_user = '' ; for ( let x = 0 ; x < messages . length ; x ++ ) { if ( multi_users && messages [ x ] . user != last_user ) { last_user = messages [ x ] . user ; if ( txt . length ) { txt . push ( '' ) ; } txt . push ( '<@' + messages [ x ] . user + '>:' ) ; } txt . push ( messages [ x ] . text ) ; } return txt . join ( '\n' ) ; } else { if ( messages . length ) { return messages [ 0 ] . text ; } else { return messages . text ; } } } ; this . getResponses = function ( ) { var res = { } ; for ( var key in this . responses ) { res [ key ] = { question : this . responses [ key ] . length ? this . responses [ key ] [ 0 ] . question : this . responses [ key ] . question , key : key , answer : this . extractResponse ( key ) , } ; } return res ; } ; this . getResponsesAsArray = function ( ) { var res = [ ] ; for ( var key in this . responses ) { res . push ( { question : this . responses [ key ] . length ? this . responses [ key ] [ 0 ] . question : this . responses [ key ] . question , key : key , answer : this . extractResponse ( key ) , } ) ; } return res ; } ; this . extractResponses = function ( ) { var res = { } ; for ( var key in this . responses ) { res [ key ] = this . extractResponse ( key ) ; } return res ; } ; this . extractResponse = function ( key ) { return this . combineMessages ( this . responses [ key ] ) ; } ; this . replaceAttachmentTokens = function ( attachments ) { if ( attachments && attachments . length ) { for ( let a = 0 ; a < attachments . length ; a ++ ) { for ( let key in attachments [ a ] ) { if ( typeof ( attachments [ a ] [ key ] ) == 'string' ) { attachments [ a ] [ key ] = this . replaceTokens ( attachments [ a ] [ key ] ) ; } else { attachments [ a ] [ key ] = this . replaceAttachmentTokens ( attachments [ a ] [ key ] ) ; } } } } else { for ( let a in attachments ) { if ( typeof ( attachments [ a ] ) == 'string' ) { attachments [ a ] = this . replaceTokens ( attachments [ a ] ) ; } else { attachments [ a ] = this . replaceAttachmentTokens ( attachments [ a ] ) ; } } } return attachments ; } ; this . replaceTokens = function ( text ) { var vars = { identity : this . task . bot . identity , responses : this . extractResponses ( ) , origin : this . task . source_message , vars : this . vars , } ; var rendered = '' ; try { rendered = mustache . render ( text , vars ) ; } catch ( err ) { botkit . logger . error ( 'Error in message template. Mustache failed with error: ' , err ) ; rendered = text ; } return rendered ; } ; this . stop = function ( status ) { this . handler = null ; this . messages = [ ] ; this . status = status || 'stopped' ; botkit . logger . debug ( 'Conversation is over with status ' + this . status ) ; this . task . conversationEnded ( this ) ; } ; // was this conversation successful? // return true if it was completed // otherwise, return false // false could indicate a variety of failed states: // manually stopped, timed out, etc this . successful = function ( ) { // if the conversation is still going, it can't be successful yet if ( this . isActive ( ) ) { return false ; } if ( this . status == 'completed' ) { return true ; } else { return false ; } } ; this . cloneMessage = function ( message ) { // clone this object so as not to modify source var outbound = clone ( message ) ; if ( typeof ( message . text ) == 'string' ) { outbound . text = this . replaceTokens ( message . text ) ; } else if ( message . text ) { outbound . text = this . replaceTokens ( message . text [ Math . floor ( Math . random ( ) * message . text . length ) ] ) ; } if ( outbound . attachments ) { outbound . attachments = this . replaceAttachmentTokens ( outbound . attachments ) ; } if ( outbound . attachment ) { // pick one variation of the message text at random if ( outbound . attachment . payload . text && typeof ( outbound . attachment . payload . text ) != 'string' ) { outbound . attachment . payload . text = this . replaceTokens ( outbound . attachment . payload . text [ Math . floor ( Math . random ( ) * outbound . attachment . payload . text . length ) ] ) ; } outbound . attachment = this . replaceAttachmentTokens ( [ outbound . attachment ] ) [ 0 ] ; } if ( this . messages . length && ! message . handler ) { outbound . continue_typing = true ; } if ( typeof ( message . attachments ) == 'function' ) { outbound . attachments = message . attachments ( this ) ; } return outbound ; } ; this . onTimeout = function ( handler ) { if ( typeof ( handler ) == 'function' ) { this . timeOutHandler = handler ; } else { botkit . logger . debug ( 'Invalid timeout function passed to onTimeout' ) ; } } ; this . tick = function ( ) { var now = new Date ( ) ; if ( this . isActive ( ) ) { if ( this . processing ) { // do nothing. The bot is waiting for async process to complete. } else if ( this . handler ) { // check timeout! // how long since task started? var duration = ( now . getTime ( ) - this . task . startTime . getTime ( ) ) ; // how long since last active? var lastActive = ( now . getTime ( ) - this . lastActive . getTime ( ) ) ; if ( this . task . timeLimit && // has a timelimit ( duration > this . task . timeLimit ) && // timelimit is up ( lastActive > this . task . timeLimit ) // nobody has typed for 60 seconds at least ) { // if timeoutHandler is set then call it, otherwise follow the normal flow // this will not break others code, after the update if ( this . timeOutHandler ) { this . timeOutHandler ( this ) ; } else if ( this . hasThread ( 'on_timeout' ) ) { this . status = 'ending' ; this . gotoThread ( 'on_timeout' ) ; } else { this . stop ( 'timeout' ) ; } } // otherwise do nothing } else { if ( this . messages . length ) { if ( this . sent . length && ! this . sent [ this . sent . length - 1 ] . sent ) { return ; } if ( this . task . bot . botkit . config . require_delivery && this . sent . length && ! this . sent [ this . sent . length - 1 ] . delivered ) { return ; } if ( typeof ( this . messages [ 0 ] . timestamp ) == 'undefined' || this . messages [ 0 ] . timestamp <= now . getTime ( ) ) { var message = this . messages . shift ( ) ; //console.log('HANDLING NEW MESSAGE',message); // make sure next message is delayed appropriately if ( this . messages . length && this . messages [ 0 ] . delay ) { this . messages [ 0 ] . timestamp = now . getTime ( ) + this . messages [ 0 ] . delay ; } if ( message . handler ) { //console.log(">>>>>> SET HANDLER IN TICK"); this . handler = message . handler ; } else { this . handler = null ; //console.log(">>>>>>> CLEARING HANDLER BECAUSE NO HANDLER NEEDED"); } if ( message . capture_options ) { this . capture_options = message . capture_options ; } else { this . capture_options = { } ; } this . lastActive = new Date ( ) ; // is there any text? // or an attachment? (facebook) // or multiple attachments (slack) // if (message.text || message.attachments || message.attachment) { if ( message ) { var outbound = this . cloneMessage ( message ) ; var that = this ; outbound . sent_timestamp = new Date ( ) . getTime ( ) ; that . sent . push ( outbound ) ; that . transcript . push ( outbound ) ; this . task . bot . reply ( this . source_message , outbound , function ( err , sent_message ) { if ( err ) { botkit . logger . error ( 'An error occurred while sending a message: ' , err ) ; // even though an error occured, set sent to true // this will allow the conversation to keep going even if one message fails // TODO: make a message that fails to send _resend_ at least once that . sent [ that . sent . length - 1 ] . sent = true ; that . sent [ that . sent . length - 1 ] . api_response = err ; } else { that . sent [ that . sent . length - 1 ] . sent = true ; that . sent [ that . sent . length - 1 ] . api_response = sent_message ; // if sending via slack's web api, there is no further confirmation // so we can mark the message delivered if ( that . task . bot . type == 'slack' && sent_message && sent_message . ts ) { that . sent [ that . sent . length - 1 ] . delivered = true ; } that . trigger ( 'sent' , [ sent_message ] ) ; } } ) ; } if ( message . action ) { if ( typeof ( message . action ) == 'function' ) { message . action ( this ) ; } else if ( message . action == 'repeat' ) { this . repeat ( ) ; } else if ( message . action == 'wait' ) { this . silentRepeat ( ) ; } else if ( message . action == 'stop' ) { this . stop ( ) ; } else if ( message . action == 'timeout' ) { this . stop ( 'timeout' ) ; } else if ( this . threads [ message . action ] ) { this . gotoThread ( message . action ) ; } } } else { //console.log('Waiting to send next message...'); } // end immediately instad of waiting til next tick. // if it hasn't already been ended by a message action! if ( this . isActive ( ) && ! this . messages . length && ! this . handler && ! this . processing ) { this . stop ( 'completed' ) ; } } else if ( this . sent . length ) { // sent at least 1 message this . stop ( 'completed' ) ; } } } } ; botkit . logger . debug ( 'CREATED A CONVO FOR' , this . source_message . user , this . source_message . channel ) ; this . gotoThread ( 'default' ) ; }