diff --git a/core/menu_util.js b/core/menu_util.js index 77ea42f7..41107000 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -38,7 +38,7 @@ function getMenuConfig(client, name, cb) { if(_.isString(menuConfig.prompt)) { if(_.has(client.currentTheme, [ 'prompts', menuConfig.prompt ])) { menuConfig.promptConfig = client.currentTheme.prompts[menuConfig.prompt]; - callback(null); + callback(null); } else { callback(new Error('No prompt entry for \'' + menuConfig.prompt + '\'')); } @@ -213,7 +213,8 @@ function handleNext(client, nextSpec, conf) { } var nextAsset = asset.getAssetWithShorthand(nextSpec, 'menu'); - + // :TODO: getAssetWithShorthand() can return undefined - handle it! + conf = conf || {}; var extraArgs = conf.extraArgs || {}; diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 0bc4621b..9a30bfd2 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -784,7 +784,7 @@ function FTNMessageScanTossModule() { } Message.getMessageIdsByMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.REPLY, (err, msgIds) => { - if(msgIds) { + if(msgIds && msgIds.length > 0) { // expect a single match, but dupe checking is not perfect - warn otherwise if(1 === msgIds.length) { message.replyToMsgId = msgIds[0]; diff --git a/core/servers/telnet.js b/core/servers/telnet.js index ba85599f..7def16c8 100644 --- a/core/servers/telnet.js +++ b/core/servers/telnet.js @@ -2,17 +2,17 @@ 'use strict'; // ENiGMA½ -var baseClient = require('../client.js'); -var Log = require('../logger.js').log; -var ServerModule = require('../server_module.js').ServerModule; -var Config = require('../config.js').config; +const baseClient = require('../client.js'); +const Log = require('../logger.js').log; +const ServerModule = require('../server_module.js').ServerModule; +const Config = require('../config.js').config; -var net = require('net'); -var buffers = require('buffers'); -var binary = require('binary'); -var stream = require('stream'); -var assert = require('assert'); -var util = require('util'); +// deps +const net = require('net'); +const buffers = require('buffers'); +const binary = require('binary'); +const assert = require('assert'); +const util = require('util'); //var debug = require('debug')('telnet'); @@ -116,11 +116,11 @@ var OPTIONS = { SEND_LOCATION : 23, // RFC 779 TERMINAL_TYPE : 24, // aka 'TTYPE': RFC 1091 @ http://tools.ietf.org/html/rfc1091 //END_OF_RECORD : 25, // RFC 885 - //TACACS_USER_ID : 26, // RFC 927 - //OUTPUT_MARKING : 27, // RFC 933 - //TERMINCAL_LOCATION_NUMBER : 28, // RFC 946 - //TELNET_3270_REGIME : 29, // RFC 1041 - WINDOW_SIZE : 31, // aka 'NAWS': RFC 1073 @ http://tools.ietf.org/html/rfc1073 + //TACACS_USER_ID : 26, // RFC 927 + //OUTPUT_MARKING : 27, // RFC 933 + //TERMINCAL_LOCATION_NUMBER : 28, // RFC 946 + //TELNET_3270_REGIME : 29, // RFC 1041 + WINDOW_SIZE : 31, // aka 'NAWS': RFC 1073 @ http://tools.ietf.org/html/rfc1073 TERMINAL_SPEED : 32, // RFC 1079 @ http://tools.ietf.org/html/rfc1079 REMOTE_FLOW_CONTROL : 33, // RFC 1072 @ http://tools.ietf.org/html/rfc1372 LINEMODE : 34, // RFC 1184 @ http://tools.ietf.org/html/rfc1184 @@ -155,19 +155,19 @@ var NEW_ENVIRONMENT_COMMANDS = { USERVAR : 3, }; -var IAC_BUF = new Buffer([ COMMANDS.IAC ]); -var SB_BUF = new Buffer([ COMMANDS.SB ]); -var SE_BUF = new Buffer([ COMMANDS.SE ]); -var IAC_SE_BUF = new Buffer([ COMMANDS.IAC, COMMANDS.SE ]); +const IAC_BUF = new Buffer([ COMMANDS.IAC ]); +//var SB_BUF = new Buffer([ COMMANDS.SB ]); +//var SE_BUF = new Buffer([ COMMANDS.SE ]); +const IAC_SE_BUF = new Buffer([ COMMANDS.IAC, COMMANDS.SE ]); -var COMMAND_NAMES = Object.keys(COMMANDS).reduce(function(names, name) { +const COMMAND_NAMES = Object.keys(COMMANDS).reduce(function(names, name) { names[COMMANDS[name]] = name.toLowerCase(); return names; }, {}); -var COMMAND_IMPLS = {}; -['do', 'dont', 'will', 'wont', 'sb'].forEach(function(command) { - var code = COMMANDS[command.toUpperCase()]; +const COMMAND_IMPLS = {}; +[ 'do', 'dont', 'will', 'wont', 'sb' ].forEach(function(command) { + const code = COMMANDS[command.toUpperCase()]; COMMAND_IMPLS[code] = function(bufs, i, event) { if(bufs.length < (i + 1)) { return MORE_DATA_REQUIRED; @@ -430,9 +430,6 @@ function TelnetClient(input, output) { var bufs = buffers(); this.bufs = bufs; - var readyFired = false; - var encodingSet = false; - this.setInputOutput(input, output); this.negotiationsComplete = false; // are we in the 'negotiation' phase? diff --git a/mods/bbs_list.js b/mods/bbs_list.js new file mode 100644 index 00000000..b7c19fb2 --- /dev/null +++ b/mods/bbs_list.js @@ -0,0 +1,447 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const MenuModule = require('../core/menu_module.js').MenuModule; +const getModDatabasePath = require('../core/database.js').getModDatabasePath; +const ViewController = require('../core/view_controller.js').ViewController; +const ansi = require('../core/ansi_term.js'); +const theme = require('../core/theme.js'); +const getUserName = require('../core/user.js').getUserName; + +// deps +const async = require('async'); +const sqlite3 = require('sqlite3'); +const _ = require('lodash'); + +// :TODO: add notes field + +exports.getModule = BBSListModule; + +const moduleInfo = { + name : 'BBS List', + desc : 'List of other BBSes', + author : 'Andrew Pamment', + packageName : 'com.magickabbs.enigma.bbslist' +}; + +exports.moduleInfo = moduleInfo; + +const MciViewIds = { + view : { + BBSList : 1, + SelectedBBSName : 2, + SelectedBBSSysOp : 3, + SelectedBBSTelnet : 4, + SelectedBBSWww : 5, + SelectedBBSLoc : 6, + SelectedBBSSoftware : 7, + SelectedBBSNotes : 8, + SelectedBBSSubmitter : 9, + }, + add : { + BBSName : 1, + Sysop : 2, + Telnet : 3, + Www : 4, + Location : 5, + Software : 6, + Notes : 7, + Error : 8, + } +}; + +const FormIds = { + View : 0, + Add : 1, +}; + +const SELECTED_MCI_NAME_TO_ENTRY = { + SelectedBBSName : 'bbsName', + SelectedBBSSysOp : 'sysOp', + SelectedBBSTelnet : 'telnet', + SelectedBBSWww : 'www', + SelectedBBSLoc : 'location', + SelectedBBSSoftware : 'software', + SelectedBBSSubmitter : 'submitter', + SelectedBBSSubmitterId : 'submitterUserId', + SelectedBBSNotes : 'notes', +}; + +function BBSListModule(options) { + MenuModule.call(this, options); + + const self = this; + const config = this.menuConfig.config; + + this.initSequence = function() { + async.series( + [ + function beforeDisplayArt(callback) { + self.beforeArt(callback); + }, + function display(callback) { + self.displayBBSList(false, callback); + } + ], + err => { + if(err) { + // :TODO: Handle me -- initSequence() should really take a completion callback + } + self.finishedLoading(); + } + ); + }; + + this.drawSelectedEntry = function(entry) { + if(!entry) { + Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { + self.setViewText(MciViewIds.view[mciName], ''); + }); + } else { + // :TODO: we really need pipe code support for TextView!! + const youSubmittedFormat = config.youSubmittedFormat || '{submitter} (You!)'; + + Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { + const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]]; + if(MciViewIds.view[mciName]) { + + if('SelectedBBSSubmitter' == mciName && entry.submitterUserId == self.client.user.userId) { + self.setViewText(MciViewIds.view.SelectedBBSSubmitter, youSubmittedFormat.format(entry)); + } else { + self.setViewText(MciViewIds.view[mciName], t); + } + } + }); + } + }; + + this.setEntries = function(entriesView) { + /* + :TODO: This is currently disabled until VerticalMenuView 'justify' works properly with pipe code strings + + const listFormat = config.listFormat || '{bbsName}'; + const focusListFormat = config.focusListFormat || '{bbsName}'; + + entriesView.setItems(self.entries.map( e => { + return listFormat.format(e); + })); + + entriesView.setFocusItems(self.entries.map( e => { + return focusListFormat.format(e); + })); + */ + entriesView.setItems(self.entries.map(e => e.bbsName)); + }; + + this.displayBBSList = function(clearScreen, cb) { + async.waterfall( + [ + function clearAndDisplayArt(callback) { + if(self.viewControllers.add) { + self.viewControllers.add.setFocus(false); + } + if (clearScreen) { + self.client.term.rawWrite(ansi.resetScreen()); + } + theme.displayThemedAsset( + config.art.entries, + self.client, + { font : self.menuConfig.font, trailingLF : false }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function initOrRedrawViewController(artData, callback) { + if(_.isUndefined(self.viewControllers.add)) { + const vc = self.addViewController( + 'view', + new ViewController( { client : self.client, formId : FormIds.View } ) + ); + + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.View, + }; + + return vc.loadFromMenuConfig(loadOpts, callback); + } else { + self.viewControllers.view.setFocus(true); + self.viewControllers.view.getView(MciViewIds.view.BBSList).redraw(); + return callback(null); + } + }, + function fetchEntries(callback) { + const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); + self.entries = []; + + self.database.each( + `SELECT id, bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes + FROM bbs_list;`, + (err, row) => { + if (!err) { + self.entries.push({ + id : row.id, + bbsName : row.bbs_name, + sysOp : row.sysop, + telnet : row.telnet, + www : row.www, + location : row.location, + software : row.software, + submitterUserId : row.submitter_user_id, + notes : row.notes, + }); + } + }, + err => { + return callback(err, entriesView); + } + ); + }, + function getUserNames(entriesView, callback) { + async.each(self.entries, (entry, next) => { + getUserName(entry.submitterUserId, (err, username) => { + if(username) { + entry.submitter = username; + } else { + entry.submitter = 'N/A'; + } + return next(); + }); + }, () => { + return callback(null, entriesView); + }); + }, + function populateEntries(entriesView, callback) { + self.setEntries(entriesView); + + entriesView.on('index update', idx => { + const entry = self.entries[idx]; + + self.drawSelectedEntry(entry); + + if(!entry) { + self.selectedBBS = -1; + } else { + self.selectedBBS = idx; + } + }); + + if (self.selectedBBS >= 0) { + entriesView.setFocusItemIndex(self.selectedBBS); + self.drawSelectedEntry(self.entries[self.selectedBBS]); + } else if (self.entries.length > 0) { + entriesView.setFocusItemIndex(0); + self.drawSelectedEntry(self.entries[0]); + } + + entriesView.redraw(); + + return callback(null); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + }; + + this.displayAddScreen = function(cb) { + async.waterfall( + [ + function clearAndDisplayArt(callback) { + self.viewControllers.view.setFocus(false); + self.client.term.rawWrite(ansi.resetScreen()); + + theme.displayThemedAsset( + config.art.add, + self.client, + { font : self.menuConfig.font }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function initOrRedrawViewController(artData, callback) { + if(_.isUndefined(self.viewControllers.add)) { + const vc = self.addViewController( + 'add', + new ViewController( { client : self.client, formId : FormIds.Add } ) + ); + + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.Add, + }; + + return vc.loadFromMenuConfig(loadOpts, callback); + } else { + self.viewControllers.add.setFocus(true); + self.viewControllers.add.redrawAll(); + self.viewControllers.add.switchFocus(MciViewIds.add.BBSName); + return callback(null); + } + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + }; + + this.clearAddForm = function() { + [ 'BBSName', 'Sysop', 'Telnet', 'Www', 'Location', 'Software', 'Error', 'Notes' ].forEach( mciName => { + const v = self.viewControllers.add.getView(MciViewIds.add[mciName]); + if(v) { + v.setText(''); + } + }); + }; + + this.menuMethods = { + // + // Validators + // + viewValidationListener : function(err, cb) { + const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error); + if(errMsgView) { + if(err) { + errMsgView.setText(err.message); + } else { + errMsgView.clearText(); + } + } + + return cb(null); + }, + + // + // Key & submit handlers + // + quitBBSList : function() { + self.prevMenu(); + }, + addBBS : function() { + self.displayAddScreen(); + }, + deleteBBS : function() { + const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); + + if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) { + // must be owner or +op + return; + } + + const entry = self.entries[self.selectedBBS]; + if(!entry) { + return; + } + + self.database.run( + `DELETE FROM bbs_list + WHERE id=?;`, + [ entry.id ], + err => { + if (err) { + self.client.log.error( { error : err.message }, 'Error deleting from BBS list'); + } else { + self.entries.splice(self.selectedBBS, 1); + + self.setEntries(entriesView); + + if(self.entries.length > 0) { + entriesView.focusPrevious(); + } + + self.viewControllers.view.redrawAll(); + } + } + ); + }, + submitBBS : function(formData) { + + let ok = true; + [ 'BBSName', 'Sysop', 'Telnet' ].forEach( mciName => { + if('' === self.viewControllers.add.getView(MciViewIds.add[mciName]).getData()) { + ok = false; + } + }); + if(!ok) { + // validators should prevent this! + return; + } + + self.database.run( + `INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes) + VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, + [ formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www, formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes ], + err => { + if(err) { + self.client.log.error( { error : err.message }, 'Error adding to BBS list'); + } + + self.clearAddForm(); + self.displayBBSList(true); + } + ); + }, + cancelSubmit : function() { + self.clearAddForm(); + self.displayBBSList(true); + } + }; + + this.setViewText = function(id, text) { + var v = self.viewControllers.view.getView(id); + if(v) { + v.setText(text); + } + }; + + this.initDatabase = function(cb) { + async.series( + [ + function openDatabase(callback) { + self.database = new sqlite3.Database( + getModDatabasePath(moduleInfo), + callback + ); + }, + function createTables(callback) { + self.database.serialize( () => { + self.database.run( + `CREATE TABLE IF NOT EXISTS bbs_list ( + id INTEGER PRIMARY KEY, + bbs_name VARCHAR NOT NULL, + sysop VARCHAR NOT NULL, + telnet VARCHAR NOT NULL, + www VARCHAR, + location VARCHAR, + software VARCHAR, + submitter_user_id INTEGER NOT NULL, + notes VARCHAR + );` + ); + }); + callback(null); + } + ], + cb + ); + }; +} + +require('util').inherits(BBSListModule, MenuModule); + +BBSListModule.prototype.beforeArt = function(cb) { + BBSListModule.super_.prototype.beforeArt.call(this, err => { + return err ? cb(err) : this.initDatabase(cb); + }); +}; diff --git a/mods/menu.hjson b/mods/menu.hjson index 1ed12a98..858cd1f2 100644 --- a/mods/menu.hjson +++ b/mods/menu.hjson @@ -739,6 +739,10 @@ value: { command: "CHAT"} action: @menu:ercClient } + { + value: { command: "BBS"} + action: @menu:bbsList + } { value: 1 action: @menu:mainMenu @@ -1140,6 +1144,109 @@ } } + bbsList: { + desc: Viewing BBS List + module: bbs_list + options: { + cls: true + } + config: { + art: { + entries: BBSLIST + add: BBSADD + } + } + + form: { + 0: { + mci: { + VM1: { maxLength: 32 } + TL2: { maxLength: 32 } + TL3: { maxLength: 32 } + TL4: { maxLength: 32 } + TL5: { maxLength: 32 } + TL6: { maxLength: 32 } + TL7: { maxLength: 32 } + TL8: { maxLength: 32 } + TL9: { maxLength: 32 } + } + actionKeys: [ + { + keys: [ "a" ] + action: @method:addBBS + } + { + // :TODO: add delete key + keys: [ "d" ] + action: @method:deleteBBS + } + { + keys: [ "q", "escape" ] + action: @method:quitBBSList + } + ] + } + 1: { + mci: { + ET1: { + argName: name + maxLength: 32 + validate: @systemMethod:validateNonEmpty + } + ET2: { + argName: sysop + maxLength: 32 + validate: @systemMethod:validateNonEmpty + } + ET3: { + argName: telnet + maxLength: 32 + validate: @systemMethod:validateNonEmpty + } + ET4: { + argName: www + maxLength: 32 + } + ET5: { + argName: location + maxLength: 32 + } + ET6: { + argName: software + maxLength: 32 + } + ET7: { + argName: notes + maxLength: 32 + } + TM17: { + argName: submission + items: [ "save", "cancel" ] + submit: true + } + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelSubmit + } + ] + submit: { + *: [ + { + value: { "submission" : 0 } + action: @method:submitBBS + } + { + value: { "submission" : 1 } + action: @method:cancelSubmit + } + ] + } + } + } + } + /////////////////////////////////////////////////////////////////////// // Doors Menu /////////////////////////////////////////////////////////////////////// diff --git a/mods/nua.js b/mods/nua.js index 7d83e518..a7e1a3a2 100644 --- a/mods/nua.js +++ b/mods/nua.js @@ -77,16 +77,16 @@ function NewUserAppModule(options) { newUser.properties = { real_name : formData.value.realName, - birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), + birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format sex : formData.value.sex, location : formData.value.location, affiliation : formData.value.affils, email_address : formData.value.email, web_address : formData.value.web, - account_created : new Date().toISOString(), + account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format - message_conf_tag : confTag, - message_area_tag : areaTag, + message_conf_tag : confTag, + message_area_tag : areaTag, term_height : self.client.term.termHeight, term_width : self.client.term.termWidth, diff --git a/mods/telnet_bridge.js b/mods/telnet_bridge.js index 6f7f4366..e5b806b3 100644 --- a/mods/telnet_bridge.js +++ b/mods/telnet_bridge.js @@ -7,6 +7,7 @@ const resetScreen = require('../core/ansi_term.js').resetScreen; const async = require('async'); const _ = require('lodash'); const net = require('net'); +const EventEmitter = require('events'); /* Expected configuration block: @@ -32,6 +33,50 @@ exports.moduleInfo = { author : 'Andrew Pamment', }; +class TelnetClientConnection extends EventEmitter { + constructor(client) { + super(); + + this.client = client; + } + + restorePipe() { + this.client.term.output.unpipe(this.bridgeConnection); + this.client.term.output.resume(); + } + + connect(connectOpts) { + this.bridgeConnection = net.createConnection(connectOpts, () => { + this.emit('connected'); + + this.client.term.output.pipe(this.bridgeConnection); + }); + + this.bridgeConnection.on('data', data => { + // :TODO: The idea here is that we can handle |data| as if we're the client & respond to commands (cont.) + // ...the normal telnet *server* would not. + return this.client.term.rawWrite(data); + }); + + this.bridgeConnection.once('end', () => { + this.restorePipe(); + this.emit('end'); + }); + + this.bridgeConnection.once('error', err => { + this.restorePipe(); + this.emit('end', err); + }); + } + + disconnect() { + if(this.bridgeConnection) { + this.bridgeConnection.end(); + } + } + +} + function TelnetBridgeModule(options) { MenuModule.call(this, options); @@ -64,39 +109,27 @@ function TelnetBridgeModule(options) { self.client.term.write(resetScreen()); self.client.term.write(` Connecting to ${connectOpts.host}, please wait...\n`); - let bridgeConnection = net.createConnection(connectOpts, () => { + const telnetConnection = new TelnetClientConnection(self.client); + + telnetConnection.on('connected', () => { self.client.log.info(connectOpts, 'Telnet bridge connection established'); - self.client.term.output.pipe(bridgeConnection); - self.client.once('end', () => { self.client.log.info('Connection ended. Terminating connection'); clientTerminated = true; - return bridgeConnection.end(); + telnetConnection.disconnect(); }); }); - const restorePipe = function() { - self.client.term.output.unpipe(bridgeConnection); - self.client.term.output.resume(); - }; + telnetConnection.on('end', err => { + if(err) { + self.client.log.info(`Telnet bridge connection error: ${err.message}`); + } - bridgeConnection.on('data', data => { - // pass along - // :TODO: just pipe this as well - return self.client.term.rawWrite(data); + callback(clientTerminated ? new Error('Client connection terminated') : null); }); - bridgeConnection.once('end', () => { - restorePipe(); - return callback(clientTerminated ? new Error('Client connection terminated') : null); - }); - - bridgeConnection.once('error', err => { - self.client.log.info(`Telnet bridge connection error: ${err.message}`); - restorePipe(); - return callback(err); - }); + telnetConnection.connect(connectOpts); } ], err => { diff --git a/mods/themes/luciano_blocktronics/BBSADD.ANS b/mods/themes/luciano_blocktronics/BBSADD.ANS new file mode 100644 index 00000000..f341f486 Binary files /dev/null and b/mods/themes/luciano_blocktronics/BBSADD.ANS differ diff --git a/mods/themes/luciano_blocktronics/BBSLIST.ANS b/mods/themes/luciano_blocktronics/BBSLIST.ANS new file mode 100644 index 00000000..ec2974e9 Binary files /dev/null and b/mods/themes/luciano_blocktronics/BBSLIST.ANS differ diff --git a/mods/themes/luciano_blocktronics/MMENU.ANS b/mods/themes/luciano_blocktronics/MMENU.ANS index 0fc985de..3b02a643 100644 Binary files a/mods/themes/luciano_blocktronics/MMENU.ANS and b/mods/themes/luciano_blocktronics/MMENU.ANS differ diff --git a/mods/themes/luciano_blocktronics/theme.hjson b/mods/themes/luciano_blocktronics/theme.hjson index ac359ac8..2bb2ab4f 100644 --- a/mods/themes/luciano_blocktronics/theme.hjson +++ b/mods/themes/luciano_blocktronics/theme.hjson @@ -243,6 +243,43 @@ } } + bbsList: { + 0: { + mci: { + VM1: { + height: 11 + width: 22 + focusTextStyle: first upper + } + TL2: { width: 28 } + TL3: { width: 28 } + TL4: { width: 28 } + TL5: { width: 28 } + TL6: { width: 28 } + TL7: { width: 28 } + TL8: { width: 28 } + TL9: { width: 28 } + } + }, + 1: { + mci: { + ET1: { width: 32 } + ET2: { width: 32 } + ET3: { width: 32 } + ET4: { width: 32 } + ET5: { width: 32 } + ET6: { width: 32 } + ET7: { width: 32 } + ET8: { width: 32 } + + TM17: { + focusTextStyle: first upper + } + } + } + } + + messageAreaViewPost: { 0: { mci: {