log(LOG_DEBUG,'- INIT: ANSITEX'); // Load many SBBS definitions require('sbbsdefs.js','SS_UNUSED'); // Load text.dat definitions require('text.js','TOTAL_TEXT'); // Key definitions require('key_defs.js','KEY_ESC'); ansi = load({},'ansiterm_lib.js'); load('ansitex/load/funcs.js'); // Ansitex specific includes require('ansitex/load/defs.js','ACTION_EXIT'); require('ansitex/load/ansiframe.js','FRAME_ANSI'); require('ansitex/load/viewdataframe.js','FRAME_VIEWDATA'); // @TODO LIST // login screen - backspace not working // login screen ** to clear the current field not working // Returning from chat should refresh the frame // Supress displays of telegrams log(LOG_DEBUG,'Socket:'+JSON.stringify(client.socket.local_port)); while(bbs.online) { var mode = false; // Initial mode // If the user is already on, our start page is 98b var next_page = user.number ? { frame: 98,index: 'b' } : { frame: 980,index: 'a' }; // Start Frame var action = ACTION_GOTO; // Initial action var inkey_timeout = INKEY_TIMEOUT; // Timeout waiting for input var fo = null; // Current Frame var fn = null; // Current Field Number for an Input Frame var fe = null; // Frame to edit var history = []; // Page history var cf = null; // Current Input Field var cc = null; // Current Control Method var timeout = false; // Track our inactivity timeout var timer = time(); var control = []; // Methods that need to process input var extendedkey = ''; // Current Extended Key being captured while (action != ACTION_TERMINATE && action !=ACTION_EXIT) { bbs.nodesync(); // @todo Stop the display of telegrams var read = ''; var esc = false; // If we have no action, read from the terminal if (action == false) { while (esc || ! read) { log(LOG_DEBUG,'- READ START'); read = console.inkey(K_NONE,inkey_timeout); // We are intering a special keyboard char. if (read == KEY_ESC) { log(LOG_DEBUG,'- READ SPECIAL KEY COMING'); esc = true; inkey_timeout = 200; } else if (esc && ! extendedkey && read != '[') { log(LOG_DEBUG,'- READ SPECIAL KEY ABANDONED: ['+read+'] ('+read.charCodeAt(0)+')'); esc = false; inkey_timeout = INKEY_TIMEOUT; read = KEY_ESC; } else if (esc && extendedkey && (read == '~' || read == ';')) { switch (extendedkey) { case '[15': read = false; break; // F5 case '[17': read = false; break; // F6 case '[18': read = false; break; // F7 case '[19': read = false; break; // F8 case '[20': read = false; break; // F9 case '[21': read = false; break; // F10 case '[23': read = false; break; // F11 case '[24': read = false; break; // F12 default: log(LOG_DEBUG,'- READ UNKNOWN KEY: ['+extendedkey+']'); read = ''; } esc = false; extendedkey = ''; inkey_timeout = INKEY_TIMEOUT; } else if (esc) { log(LOG_DEBUG,'- READ SPECIAL KEY ['+read+'] ('+read.charCodeAt(0)+')'); extendedkey += read; read = false; } if (read === '' && ! (user.security.exemptions&UFLAG_H) ) { if (time() > timer+((user.number ? INACTIVE_LOGIN : INACTIVE_NOLOGIN)+INKEY_TIMEOUT)/1000) { fo.sendBaseline('INACTIVE',false); bbs.hangup(); } else if (time() > timer+(user.number ? INACTIVE_LOGIN : INACTIVE_NOLOGIN)/1000) { timeout = true; fo.sendBaseline('INACTIVITY',false); if (cf) { fo.gotoxy(cf.c+cf.fvalue.length,cf.r); fo.attr(cf.attribute); } } } else { if (timeout) { fo.clearBaseline(false); if (cf) { fo.gotoxy(cf.c+cf.fvalue.length,cf.r); fo.attr(cf.attribute); } } timer = time(); timeout = false; } if (esc) { log(LOG_DEBUG,'- READ SHOULD LOOP'); } } } log(LOG_DEBUG,'READ: ['+read+'] ('+read.charCodeAt(0)+')'); system.node_list[bbs.node_num-1].action=0xff; // to ensure our node status is updated correctly if (mode != MODE_BL && control.length) { cc = control[control.length-1]; log(LOG_DEBUG,'CONTROL START: ['+read+'] ('+cc.getName+')'); read = cc.handle(read); log(LOG_DEBUG,'CONTROL RETURN: ['+read+'] ('+cc.isComplete+')'); if (cc.isComplete) { control.pop(); log(LOG_DEBUG,'CONTROL COMPLETE: ['+read+'] ('+control.length+')'); cc = null; } log(LOG_DEBUG,'CONTROL END: ['+read+']'); } log(LOG_DEBUG,'MODE START: ['+read.charCodeAt(0)+']'); switch (mode) { case false: log(LOG_DEBUG,'- false: ['+read+']'); cmd=''; switch (read) { case '*': action = ACTION_STAR; break; // Frame Routing case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': log(LOG_DEBUG,'- false: Key ['+read+'] Route ['+fo.key[read]+']'); if (fo.key[read] !== null) { // If are requesting a home page if (fo.key[read] === 0) { next_page = { frame: user.number ? 1 : 98,index: 'a' }; } else { next_page = { frame: fo.key[read] }; } action = ACTION_GOTO; log(LOG_DEBUG,'- false: Key ['+read+'] ['+pageStr(next_page)+']'); } else { fo.sendBaseline('ERR_ROUTE',false); } break; case '#': log(LOG_DEBUG,'- false: Key ['+read+'] ['+pageStr(fo)+']'); if (fo.index !== 'z') { next_page = { frame: fo.frame, index: String.fromCharCode(fo.index.charCodeAt(0)+1) }; action = ACTION_GOTO; } else { fo.sendBaseline('ERR_ROUTE',false); } break; } break; // Command input on bottom line case MODE_BL: log(LOG_DEBUG,'- MODE_BL: ['+read+']'); if (read.match(/[0-9]/)) { cmd += read; console.write(read); } // @todo check if CTRL_H is required? if ((read == CTRL_H || read == KEY_DEL) && cmd.length) { console.backspace(); cmd = cmd.substring(0,cmd.length-1); } if (cmd == '00') { action = ACTION_RELOAD; cmd = ''; fo.cursorOff(); break; } // Invalid system pages. if (cmd.match(/^0[2367]/)) { fo.cursorOff(); fo.sendBaseline('ERR_ROUTE',false); mode = action = false; cmd = ''; } // Edit specific frame if (cmd.match(/^04/) && read.match(/[a-z]/)) { var page = cmd.substr(2,cmd.length-1); // If we are not a user if (! user.number) { fo.cursorOff(); fo.sendBaseline('ERR_ROUTE',false); action = false; } else { fe = { frame: page, index: read }; fo.cursorOff(); action = ACTION_EDIT; log(LOG_DEBUG,'- MODE_BL: EDIT ['+JSON.stringify(fe)+']'); } mode = false; cmd = ''; break; } // Bookmark frame if (cmd == '05') { if (! user.number) { fo.cursorOff(); fo.sendBaseline('ERR_ROUTE',false); mode = action = false; cmd = ''; } else { // @todo fo.cursorOff(); fo.sendBaseline('ERR_NOT_IMPLEMENTED',false); mode = action = false; cmd = ''; } break; } // Report Problem if (cmd == '08') { if (! user.number) { fo.cursorOff(); fo.sendBaseline('ERR_ROUTE',false); mode = action = false; cmd = ''; } else { // @todo fo.cursorOff(); fo.sendBaseline('ERR_NOT_IMPLEMENTED',false); mode = action = false; cmd = ''; } break; } // Reload frame if (cmd == '09') { fo.cursorOff(); action = ACTION_GOTO; cmd = ''; next_page = { frame: fo.frame, index: fo.index}; break; } // Another star aborts the command if (read == '*') { fo.clearBaseline(false); fo.cursorOff(); mode = action = false; cmd = ''; if (cf) { // If there is a control for this field, if (cc) cc.prefield(); mode = MODE_FIELD; fo.gotoxy(cf.c,cf.r); fo.attr(cf.attribute); console.write(cf.fchar.repeat(cf.fvalue.length)); fo.cursorOn(cf.c,cf.r); cf.fvalue = ''; } } if (read == '#' || read == '\r') { // Nothing typed between * and # // *# means go back if (cmd == '') { fo.clearBaseline(false); action = ACTION_BACKUP; } else if (cmd == '0') { next_page = { frame: user.number ? 1 : 98,index: 'a' }; // @todo specify home page in config action = ACTION_GOTO; // Edit frame } else if (cmd == '04') { // If we are not a user if (! user.number) { fo.sendBaseline('ERR_ROUTE',false); action = false; } else { action = ACTION_EDIT; } } else { next_page = { frame: cmd }; action = ACTION_GOTO; } // Clear the command we are finished processing... fo.cursorOff(); cmd = ''; mode = false; } break; // Key presses during field input. case MODE_FIELD: //$cmd = ''; action = false; switch (fo.type) { // Login frame. case FRAME_TYPE_LOGIN: switch (read) { case '#': case '\r': log(LOG_DEBUG,'- MODE_FIELD:FRAME_TYPE_LOGIN: ['+read+'] A'); // If we are the main login screen, see if it is a new user // @todo Need to make sure this is only actioned on user login if (cf.ftype == 't' && cf.fvalue.toUpperCase() == 'NEW') { action = ACTION_GOTO; next_page = { frame: 981,index: 'a' }; // @todo This should be in the INI. break; } break; } // Response frame. case FRAME_TYPE_RESPONSE: // If we came from FRAME_TYPE_LOGIN and the user typed NEW to register if (action == ACTION_GOTO) break; switch (read) { // End of field entry. case '#': case '\r': log(LOG_DEBUG,'- MODE_FIELD:FRAME_TYPE_RESPONSE: # ['+read+']'); // Next Field fn++; cf = fo.frame_fields[fn]; log(LOG_DEBUG,'fn:'+fn+', cf'+JSON.stringify(cf)); if (cf) { // If there is a control for this field, if (cc) cc.prefield(); mode = MODE_FIELD; fo.gotoxy(cf.c,cf.r); fo.attr(cf.attribute); // Finished all editable fields. } else { action = ACTION_SUBMITRF; } break; case '*': log(LOG_DEBUG,'- MODE_FIELD:FRAME_TYPE_RESPONSE: ['+read+']'); //$current['prevmode'] = MODE_FIELD; action = ACTION_STAR; break; // Delete Key pressed case CTRL_H: log(LOG_DEBUG,'- MODE_FIELD:FRAME_TYPE_RESPONSE: DEL ['+read+']'+' cf:'+(cf ? cf.fvalue.length : '{}')+' ct:'+cf.ftype); if (cf.fvalue.length > 0) { cf.fvalue = cf.fvalue.substring(0,cf.fvalue.length-1); fo.fieldbs(cf.fchar); } break; case KEY_ESC: log(LOG_DEBUG,'- MODE_FIELD:FRAME_TYPE_RESPONSE: ESC ['+read+']'); break; case KEY_DOWN: log(LOG_DEBUG,'- MODE_FIELD:FRAME_TYPE_RESPONSE: DOWN ['+read+']'); // Next Field fn++; cf = fo.frame_fields[fn]; log(LOG_DEBUG,'fn:'+fn+', cf'+JSON.stringify(cf)); if (! cf) { fn = 0; cf = fo.frame_fields[fn]; } // If there is a control for this field, if (cc) cc.prefield(); mode = MODE_FIELD; fo.gotoxy(cf.c+cf.fvalue.length,cf.r); fo.attr(cf.attribute); break; case KEY_UP: log(LOG_DEBUG,'- MODE_FIELD:FRAME_TYPE_RESPONSE: UP ['+read+']'); // Next Field fn--; if (fn < 0) { fn = fo.frame_fields.length-1; } cf = fo.frame_fields[fn]; log(LOG_DEBUG,'fn:'+fn+', cf'+JSON.stringify(cf)); // If there is a control for this field, if (cc) cc.prefield(); mode = MODE_FIELD; fo.gotoxy(cf.c+cf.fvalue.length,cf.r); fo.attr(cf.attribute); break; // Record Data Entry default: log(LOG_DEBUG,'- MODE_FIELD:FRAME_TYPE_RESPONSE: ['+read+'] E:'+read.charCodeAt(0)+' cf:'+(cf ? cf.flength : '{}')); if (read.charCodeAt(0) > 31 && cf.fvalue.length < cf.flength) { cf.fvalue += read; console.write((cf.ftype == 't') ? read : 'x'); } } break; // Other Frame Types - Shouldnt get here. default: log(LOG_DEBUG,'- SHOULDNT GET HERE: ['+read+']'); action = ACTION_TERMINATE; } break; // Form submission: 1 to send, 2 not to send. case MODE_SUBMITRF: switch (read) { case '1': log(LOG_DEBUG,'- MODE_SUBMITRF: Key ['+read+'] ['+pageStr(fo)+']'); log(LOG_DEBUG,' - Frame fields: '+JSON.stringify(fo.frame_fields)); log(LOG_DEBUG,' - Key 1 is:'+JSON.stringify(fo.key[1])); // If we are in a control method, complete it if (control.count) { log(LOG_DEBUG,'Last control method is:'+JSON.stringify(control[control.length-1])); control[control.length-1].process(); } else if (fo.key[1] == '*' || fo.key[1].match(/[0-9]/)) { fo.sendBaseline('NOACTION',false); mode = MODE_RFSENT; } else { log(LOG_DEBUG,' - Key 1 is a METHOD check it exists:'+JSON.stringify(fo.key[1])); switch(fo.key[1]) { // User is logging in to system or CUG case 'login': log(LOG_DEBUG,' - User:'+JSON.stringify(fo.frame_fields[0].fvalue)); // If login is successful, we'll exit here if (bbs.login(fo.frame_fields[0].fvalue,null,fo.frame_fields[1].fvalue)) { log(LOG_DEBUG,' - User:'+JSON.stringify(user.number)); bbs.logon(); log(LOG_DEBUG,' - SEND TO EXIT:'); action = ACTION_EXIT; break; } log(LOG_DEBUG,' ! Login failed for User:'+JSON.stringify(fo.frame_fields[0].fvalue)); break; default: // Its assumed that you get here after completing a form and you have pressed 1 to submit that form. log(LOG_DEBUG,' ! EVAL method:'+JSON.stringify(fo.key)); x = cc.process(); log(LOG_DEBUG,' = EVAL method:'+JSON.stringify(x)); } /* } elseif ($ao = FrameClass\Action::factory($this->fo->route(1),$this,$user,$action,$mode)) { $ao->handle(); $mode = $ao->mode; $action = $ao->action; if ($ao->page) $next_page = $ao->page; } else { fo.sendBaseline('ERR_METHOD_NOT_EXIST',false); mode = MODE_RFSENT; } */ } break; case '2': log(LOG_DEBUG,'- MODE_SUBMITRF: Key ['+read+'] ['+pageStr(fo)+']'); // @todo Check if HASH is a valid next destination if (fo.type == 'l') { action = ACTION_RELOAD; mode = false; } else { fo.sendBaseline('MSG_NOTSENT',false); mode = MODE_RFNOTSENT; } /* // If a Control method was rejected, we can clear it if ($control AND $method->count()) { $save = $method->pop(); if ($method->count()) { $control = $method->last()->state['control']; } else { $mode = $save->state['mode']; $action = $save->state['action']; $control = FALSE; } } */ break; case '*': action = ACTION_STAR; break; } // Destroy the object. cc = null; break; // Response form after Sent processing case MODE_RFSENT: fo.cursorOff(); switch (read) { case '*': action = ACTION_STAR; break; } /* if ($read == HASH) { if ($x = $this->fo->route(2) AND $x !== '*' AND is_numeric($x)) { $next_page = ['frame'=>$x]; } elseif (FrameModel::where('frame',$this->fo->frame())->where('index',$this->fo->index_next())->exists()) { $next_page = ['frame'=>$this->fo->frame(),'index'=>$this->fo->index_next()]; } elseif ($x = $this->fo->route(0) AND $x !== '*' AND is_numeric($x)) { $next_page = ['frame'=>$x]; // No further routes defined, go home. } else { $next_page = ['frame'=>0]; } $action = ACTION_GOTO; } */ break; // Response form after NOT sending case MODE_RFNOTSENT: // Response form ERROR case MODE_RFERROR: fo.cursorOff(); if (read == '#') { /* if ($x = $this->fo->route(2) AND $x !== '*' AND is_numeric($x)) { $next_page = ['frame'=>$x]; } elseif (FrameModel::where('frame',$this->fo->frame())->where('index',$this->fo->index_next())->exists()) { $next_page = ['frame'=>$this->fo->frame(),'index'=>$this->fo->index_next()]; } elseif ($x = $this->fo->route(0) AND $x !== '*' AND is_numeric($x)) { $next_page = ['frame'=>$x]; // No further routes defined, go home. } else { $next_page = ['frame'=>0]; } */ action = ACTION_GOTO; } else if (read == '*') { action = ACTION_STAR; break; } break; // @todo MODE_CONTROL default: log(LOG_DEBUG,'- SHOULDNT GET HERE: ['+read+']'); action = ACTION_TERMINATE; } log(LOG_DEBUG,'MODE END: ['+read+']'); log(LOG_DEBUG,'ACTION START: ['+read+']'); switch (action) { // Start command entry case ACTION_STAR: log(LOG_DEBUG,'- ACTION_STAR: ['+(next_page ? pageStr(next_page) : '')+']'); // @todo If something on the baseline preserve it fo.cursorOn(0,24); fo.sendBaseline('BASESTAR',true); action = false; mode = MODE_BL; bbs.replace_text(NodeActionRetrieving,'\1h%s \1n\1gJumping to page'); bbs.node_action=NODE_RFSD; break; // Submitting forms case ACTION_SUBMITRF: action = false; fo.cursorOff(); log(LOG_DEBUG,'- ACTION_SUBMITRF: ['+fo.type+']'); fo.sendBaseline((fo.type == 'l' ? 'MSG_LOGON' : 'MSG_SENDORNOT'),true); mode = MODE_SUBMITRF; break; // Edit a frame case ACTION_EDIT: log(LOG_DEBUG,'- ACTION_EDIT: ['+JSON.stringify(fe)+']'); if (! pageEditor(fe ? fe.frame : fo.frame)) { fo.cursorOff(); fo.sendBaseline('ACCESS_DENIED',false); action = false; break; } require('ansitex/load/edit.js','CONTROL_EDIT'); // If we are editing a specific frame, attempt to load it if (fe) { current = fo; fo = (client.socket.local_port !== 516) ? new ANSIFrame() : new VIEWDATAFrame(); fo.load(pageStr(fe)); // If the frame doesnt exist, check that the parent frame exists in case we are creating a new one if (fo.page == null) { log(LOG_DEBUG,'- ACTION_EDIT: check index: '+fe.index+' ('+String.fromCharCode(fe.index.charCodeAt(0)-1)+')'); // We can always create an 'a' frame if (fe.index !== 'a') { fo = (client.socket.local_port !== 516) ? new ANSIFrame() : new VIEWDATAFrame(); fo.load(pageStr({frame: fe.frame, index: String.fromCharCode(fe.index.charCodeAt(0)-1)})); log(LOG_DEBUG,'- ACTION_EDIT: check index: '+JSON.stringify(fo)+' ('+String.fromCharCode(fe.index.charCodeAt(0)-1)+')'); if (fo.page == null) { fo = current; // sendbaseline ERR_PAGE fo.sendBaseline('ERR_NO_PARENT',false); mode = action = false; break; } } // New frame fo = (client.socket.local_port !== 516) ? new ANSIFrame() : new VIEWDATAFrame(); fo.frame = fe.frame; fo.index = fe.index; fo.cost = 0; fo.owner = base64_decode(pageOwner(pageStr(fe)).logo); fo.content = base64_encode('Start your new page...'); } } control.push(new edit(fo)); mode = false; action = false; break; // GO Backwards case ACTION_BACKUP: // Do we have anywhere to go, drop the current page from the history. if (history.length > 1) history.pop(); // @todo If in control... next_page = (history.length > 0) ? history[history.length-1] : null; log(LOG_DEBUG,'- ACTION_BACKUP: Backing up to ['+(next_page ? pageStr(next_page) : '')+'] current ['+fo.page+']'); // If there is no next page, we'll ignore the request. if (! next_page || (pageStr(next_page) == fo.page)) { action = false; break; } // Goto specific page case ACTION_GOTO: log(LOG_DEBUG,'- ACTION_GOTO: ['+(next_page ? pageStr(next_page) : '')+']'); if (next_page !== null) { current = fo; fo = (client.socket.local_port !== 516) ? new ANSIFrame() : new VIEWDATAFrame(); fo.load(pageStr(next_page)); if (fo.page == null) { fo = current; // In case the frame doesnt exist if (fo == null) fo = (client.socket.local_port !== 516) ? new ANSIFrame() : new VIEWDATAFrame(); // sendbaseline ERR_PAGE fo.sendBaseline('ERR_ROUTE',false); mode = action = false; break; } } // If the user has access to the frame if (fo.accessible) { if (fo.isMember && fo.type == FRAME_TYPE_LOGIN) { fo.sendBaseline(ALREADY_MEMBER,false); mode = action = false; break; } // Check if the frame exists, and the user is the Service Provider } else { fo.sendBaseline('ACCESS_DENIED',false); mode = action = false; // Reset the current frame to what it was. fo = current; break; } current = null; log(LOG_DEBUG,'- ACTION_GOTO: next_page ['+JSON.stringify(next_page)+'] last history ['+JSON.stringify(history[history.length-1])+']'); // Record our history if (next_page && (! history.length || (pageStr(history[history.length-1]) != pageStr(next_page))) && (fo.type != FRAME_TYPE_LOGIN)) { // Ignore the login frames if (NO_HISTORY_FRAMES.indexOf(pageStr(next_page)) == -1) { history.push(next_page); log(LOG_DEBUG,'- ACTION_GOTO: Added to history ['+(next_page ? pageStr(next_page) : '')+'] now ['+history.length+']'); } } next_page = null; // Load frame case ACTION_RELOAD: log(LOG_DEBUG,'- ACTION_RELOAD: ['+(next_page ? pageStr(next_page) : '')+']'); // Clear the baseline history // $this->sendBaseline($client,''); // $current['baseline'] = ''; console.line_counter=0; // @todo fix to suppress a pause that is occurring before clear() fo.cursorOff(); // Clear any controls control = []; bbs.replace_text(NodeActionMain,'\1h%s \1nViewing \1h*'+fo.frame+'#\1n ['+fo.index+']'); bbs.log_str(fo.page+'|'); bbs.node_action=NODE_MAIN; switch(fo.type) { // Terminate frame case FRAME_TYPE_TERMINATE: fo.render(); mode = false; action = ACTION_TERMINATE; break; // External Frame // @todo returning from the frame, go to the 0 key if it is set case FRAME_TYPE_EXTERNAL: var content = base64_decode(fo.content); log(LOG_DEBUG,'- ACTION_GOTO: EXTERNAL ['+JSON.stringify(content)+']'); switch(content.replace(/\n/,'')) { case 'bbs.user_config()': case 'bbs.read_mail(MAIL_YOUR)': case 'bbs.scan_subs(SCAN_NEW)': case 'bbs.scan_posts()': case 'bbs.post_msg()': eval(content); // Check and see if our shell was changed if (user.command_shell != 'ansitex') { exit(); } action = ACTION_BACKUP; break; default: console.putmsg(JSON.stringify(content)); fo.sendBaseline('ERR_ROUTE',false); action = false; break; } mode = false; break; case FRAME_TYPE_LOGIN: action = false; case FRAME_TYPE_RESPONSE: //log(LOG_DEBUG,'FRAME_TYPE_RESPONSE :'+fo.page+', FIELDS: '+fo.frame_fields.count); fn = 0; cf = null; fo.render(); if (fo.frame_fields.length) { cf = fo.frame_fields[fn]; log(LOG_DEBUG,'cf'+JSON.stringify(cf)); if (cf) { mode = MODE_FIELD; fo.cursorOn(cf.c,cf.r); fo.attr(cf.attribute); // There were no editable fields. } else { mode = MODE_COMPLETE; fo.cursorOff(); } } else { mode = false; } // If this is the register page // @todo this needs to be configurable if (fo.page == '981a') { log(LOG_DEBUG,'Adding REGISTER to control stack'); require('ansitex/load/'+fo.key[1]+'.js','CONTROL_REGISTER'); control.push(eval("new "+fo.key[1]+'();')); } break; // Standard Frame case FRAME_TYPE_INFO: default: fo.render(); mode = action = false; break; // Active frame } break; } log(LOG_DEBUG,'ACTION END: ['+read+']'); } log(LOG_DEBUG,'- FINISHED'); if (action == ACTION_TERMINATE) { log(LOG_DEBUG,'! Hangup'); bbs.hangup(); } exit(); }