From 7e006f1c6847c4eb3dcebe55b39259ebedf0d750 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 7 Jun 2020 15:26:11 -0600 Subject: [PATCH 01/43] Default configuration moved to config_default.js --- core/config.js | 941 +---------------------------------- core/config_default.js | 928 ++++++++++++++++++++++++++++++++++ core/oputil/oputil_config.js | 2 +- 3 files changed, 936 insertions(+), 935 deletions(-) create mode 100644 core/config_default.js diff --git a/core/config.js b/core/config.js index 7e467709..4a235c5e 100644 --- a/core/config.js +++ b/core/config.js @@ -2,17 +2,17 @@ 'use strict'; // ENiGMA½ -const Errors = require('./enig_error.js').Errors; +const Errors = require('./enig_error.js').Errors; +const DefaultConfig = require('./config_default'); // deps -const paths = require('path'); -const async = require('async'); -const _ = require('lodash'); -const assert = require('assert'); +const paths = require('path'); +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); exports.init = init; exports.getDefaultPath = getDefaultPath; -exports.getDefaultConfig = getDefaultConfig; let currentConfiguration = {}; @@ -55,7 +55,7 @@ const ArrayReplaceKeys = [ ]; function mergeValidateAndFinalize(config, cb) { - const defaultConfig = getDefaultConfig(); + const defaultConfig = DefaultConfig(); const arrayReplaceKeyPathsMutable = _.clone(ArrayReplaceKeyPaths); const shouldReplaceArray = (arr, key) => { @@ -164,930 +164,3 @@ function getDefaultPath() { // e.g. /enigma-bbs-install-path/config/ return './config/'; } - -function getDefaultConfig() { - return { - general : { - boardName : 'Another Fine ENiGMA½ BBS', - prettyBoardName : '|08A|07nother |07F|08ine |07E|08NiGMA|07½ B|08BS', - telnetHostname : '', - sshHostname : '', - website : 'https://enigma-bbs.github.io', - description : 'An ENiGMA½ BBS', - - // :TODO: closedSystem prob belongs under users{}? - closedSystem : false, // is the system closed to new users? - - menuFile : 'menu.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path - promptFile : 'prompt.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path - achievementFile : 'achievements.hjson', - }, - - users : { - usernameMin : 2, - usernameMax : 16, // Note that FidoNet wants 36 max - usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+ .]+$', - - passwordMin : 6, - passwordMax : 128, - - // - // The bad password list is a text file containing a password per line. - // Entries in this list are not allowed to be used on the system as they - // are known to be too common. - // - // A great resource can be found at https://github.com/danielmiessler/SecLists - // - // Current list source: https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/probable-v2-top12000.txt - // - badPassFile : paths.join(__dirname, '../misc/bad_passwords.txt'), - - realNameMax : 32, - locationMax : 32, - affilsMax : 32, - emailMax : 255, - webMax : 255, - - requireActivation : false, // require SysOp activation? false = auto-activate - - groups : [ 'users', 'sysops' ], // built in groups - defaultGroups : [ 'users' ], // default groups new users belong to - - newUserNames : [ 'new', 'apply' ], // Names reserved for applying - - badUserNames : [ - 'sysop', 'admin', 'administrator', 'root', 'all', - 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix', - 'server', 'client', 'notme' - ], - - preAuthIdleLogoutSeconds : 60 * 3, // 3m - idleLogoutSeconds : 60 * 6, // 6m - - failedLogin : { - disconnect : 3, // 0=disabled - lockAccount : 9, // 0=disabled; Mark user status as "locked" if >= N - autoUnlockMinutes : 60 * 6, // 0=disabled; Auto unlock after N minutes. - }, - unlockAtEmailPwReset : true, // if true, password reset via email will unlock locked accounts - - twoFactorAuth : { - method : 'googleAuth', - - otp : { - registerEmailText : paths.join(__dirname, '../misc/otp_register_email.template.txt'), - registerEmailHtml : paths.join(__dirname, '../misc/otp_register_email.template.html'), - registerPageTemplate : paths.join(__dirname, '../www/otp_register.template.html'), - } - } - }, - - theme : { - default : 'luciano_blocktronics', - preLogin : 'luciano_blocktronics', - - passwordChar : '*', - dateFormat : { - short : 'MM/DD/YYYY', - long : 'ddd, MMMM Do, YYYY', - }, - timeFormat : { - short : 'h:mm a', - }, - dateTimeFormat : { - short : 'MM/DD/YYYY h:mm a', - long : 'ddd, MMMM Do, YYYY, h:mm a', - } - }, - - menus : { - cls : true, // Clear screen before each menu by default? - }, - - paths : { - config : paths.join(__dirname, './../config/'), - security : paths.join(__dirname, './../config/security'), // certs, keys, etc. - mods : paths.join(__dirname, './../mods/'), - loginServers : paths.join(__dirname, './servers/login/'), - contentServers : paths.join(__dirname, './servers/content/'), - chatServers : paths.join(__dirname, './servers/chat/'), - - scannerTossers : paths.join(__dirname, './scanner_tossers/'), - mailers : paths.join(__dirname, './mailers/') , - - art : paths.join(__dirname, './../art/general/'), - themes : paths.join(__dirname, './../art/themes/'), - logs : paths.join(__dirname, './../logs/'), - db : paths.join(__dirname, './../db/'), - modsDb : paths.join(__dirname, './../db/mods/'), - dropFiles : paths.join(__dirname, './../drop/'), // + "/node/ - misc : paths.join(__dirname, './../misc/'), - }, - - loginServers : { - telnet : { - port : 8888, - enabled : true, - firstMenu : 'telnetConnected', - }, - ssh : { - port : 8889, - enabled : false, // default to false as PK/pass in config.hjson are required - // - // To enable SSH, perform the following steps: - // - // 1 - Generate a Private Key (PK): - // Currently ENiGMA 1/2 requires a PKCS#1 PEM formatted PK. - // To generate a secure PK, issue the following command: - // - // > openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ - // -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa \ - // -out ./config/security/ssh_private_key.pem -aes128 - // - // (The above is a more modern equivalent of the following): - // > openssl genrsa -aes128 -out ./config/security/ssh_private_key.pem 2048 - // - // 2 - Set 'privateKeyPass' to the password you used in step #1 - // - // 3 - Finally, set 'enabled' to 'true' - // - // Additional reading: - // - https://blog.sleeplessbeastie.eu/2017/12/28/how-to-generate-private-key/ - // - https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b - // - privateKeyPem : paths.join(__dirname, './../config/security/ssh_private_key.pem'), - firstMenu : 'sshConnected', - firstMenuNewUser : 'sshConnectedNewUser', - - // - // SSH details that can affect security. Stronger ciphers are better for example, - // but terminals such as SyncTERM require KEX diffie-hellman-group14-sha1, - // cipher 3des-cbc, etc. - // - // See https://github.com/mscdex/ssh2-streams for the full list of supported - // algorithms. - // - algorithms : { - kex : [ - 'ecdh-sha2-nistp256', - 'ecdh-sha2-nistp384', - 'ecdh-sha2-nistp521', - 'diffie-hellman-group-exchange-sha256', - 'diffie-hellman-group14-sha1', - 'diffie-hellman-group-exchange-sha1', - 'diffie-hellman-group1-sha1', - ], - cipher : [ - 'aes128-ctr', - 'aes192-ctr', - 'aes256-ctr', - 'aes128-gcm', - 'aes128-gcm@openssh.com', - 'aes256-gcm', - 'aes256-gcm@openssh.com', - 'aes256-cbc', - 'aes192-cbc', - 'aes128-cbc', - 'blowfish-cbc', - '3des-cbc', - 'arcfour256', - 'arcfour128', - 'cast128-cbc', - 'arcfour', - ], - hmac : [ - 'hmac-sha2-256', - 'hmac-sha2-512', - 'hmac-sha1', - 'hmac-md5', - 'hmac-sha2-256-96', - 'hmac-sha2-512-96', - 'hmac-ripemd160', - 'hmac-sha1-96', - 'hmac-md5-96', - ], - // note that we disable compression by default due to issues with many clients. YMMV. - compress : [ 'none' ] - }, - }, - webSocket : { - ws : { - // non-secure ws:// - enabled : false, - port : 8810, - }, - wss : { - // secure ws:// - // must provide valid certPem and keyPem - enabled : false, - port : 8811, - certPem : paths.join(__dirname, './../config/https_cert.pem'), - keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), - }, - }, - }, - - contentServers : { - web : { - domain : 'another-fine-enigma-bbs.org', - - staticRoot : paths.join(__dirname, './../www'), - - resetPassword : { - // - // The following templates have these variables available to them: - // - // * %BOARDNAME% : Name of BBS - // * %USERNAME% : Username of whom to reset password - // * %TOKEN% : Reset token - // * %RESET_URL% : In case of email, the link to follow for reset. In case of landing page, - // URL to POST submit reset form. - - // templates for pw reset *email* - resetPassEmailText : paths.join(__dirname, '../misc/reset_password_email.template.txt'), // plain text version - resetPassEmailHtml : paths.join(__dirname, '../misc/reset_password_email.template.html'), // HTML version - - // tempalte for pw reset *landing page* - // - resetPageTemplate : paths.join(__dirname, './../www/reset_password.template.html'), - }, - - http : { - enabled : false, - port : 8080, - }, - https : { - enabled : false, - port : 8443, - certPem : paths.join(__dirname, './../config/https_cert.pem'), - keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), - } - }, - - gopher : { - enabled : false, - port : 8070, - publicHostname : 'another-fine-enigma-bbs.org', - publicPort : 8070, // adjust if behind NAT/etc. - bannerFile : 'gopher_banner.asc', - - // - // Set messageConferences{} to maps of confTag -> [ areaTag1, areaTag2, ... ] - // to export message confs/areas - // - }, - - nntp : { - // internal caching of groups, message lists, etc. - cache : { - maxItems : 200, - maxAge : 1000 * 30, // 30s - }, - - // - // Set publicMessageConferences{} to a map of confTag -> [ areaTag1, areaTag2, ... ] - // in order to export *public* conf/areas that are available to anonymous - // NNTP users. Other conf/areas: Standard ACS rules apply. - // - publicMessageConferences: {}, - - nntp : { - enabled : false, - port : 8119, - }, - - nntps : { - enabled : false, - port : 8563, - certPem : paths.join(__dirname, './../config/nntps_cert.pem'), - keyPem : paths.join(__dirname, './../config/nntps_key.pem'), - } - } - }, - - chatServers : { - mrc: { - enabled : false, - serverHostname : 'mrc.bottomlessabyss.net', - serverPort : 5000, - retryDelay : 10000, - multiplexerPort : 5000, - } - }, - - infoExtractUtils : { - Exiftool2Desc : { - cmd : `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x - }, - Exiftool : { - cmd : 'exiftool', - args : [ - '-charset', 'utf8', '{filePath}', - // exclude the following: - '--directory', '--filepermissions', '--exiftoolversion', '--filename', '--filesize', - '--filemodifydate', '--fileaccessdate', '--fileinodechangedate', '--createdate', '--modifydate', - '--metadatadate', '--xmptoolkit' - ] - }, - XDMS2Desc : { - // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html - cmd : 'xdms', - args : [ 'd', '{filePath}' ] - }, - XDMS2LongDesc : { - // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html - cmd : 'xdms', - args : [ 'f', '{filePath}' ] - }, - }, - - fileTypes : { - // - // File types explicitly known to the system. Here we can configure - // information extraction, archive treatment, etc. - // - // MIME types can be found in mime-db: https://github.com/jshttp/mime-db - // - // Resources for signature/magic bytes: - // * http://www.garykessler.net/library/file_sigs.html - // - // - // :TODO: text/x-ansi -> SAUCE extraction for .ans uploads - // :TODO: textual : bool -- if text, we can view. - // :TODO: asText : { cmd, args[] } -> viewable text - - // - // Audio - // - 'audio/mpeg' : { - desc : 'MP3 Audio', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - 'application/pdf' : { - desc : 'Adobe PDF', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - // - // Video - // - 'video/mp4' : { - desc : 'MPEG Video', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - 'video/x-matroska ' : { - desc : 'Matroska Video', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - 'video/x-msvideo' : { - desc : 'Audio Video Interleave', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - // - // Images - // - 'image/jpeg' : { - desc : 'JPEG Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - 'image/png' : { - desc : 'Portable Network Graphic Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - 'image/gif' : { - desc : 'Graphics Interchange Format Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - 'image/webp' : { - desc : 'WebP Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - // - // Archives - // - 'application/zip' : { - desc : 'ZIP Archive', - sig : '504b0304', - offset : 0, - archiveHandler : 'InfoZip', - }, - /* - 'application/x-cbr' : { - desc : 'Comic Book Archive', - sig : '504b0304', - }, - */ - 'application/x-arj' : { - desc : 'ARJ Archive', - sig : '60ea', - offset : 0, - archiveHandler : 'Arj', - }, - 'application/x-rar-compressed' : { - desc : 'RAR Archive', - sig : '526172211a07', - offset : 0, - archiveHandler : 'Rar', - }, - 'application/gzip' : { - desc : 'Gzip Archive', - sig : '1f8b', - offset : 0, - archiveHandler : 'TarGz', - }, - // :TODO: application/x-bzip - 'application/x-bzip2' : { - desc : 'BZip2 Archive', - sig : '425a68', - offset : 0, - archiveHandler : '7Zip', - }, - 'application/x-lzh-compressed' : { - desc : 'LHArc Archive', - sig : '2d6c68', - offset : 2, - archiveHandler : 'Lha', - }, - 'application/x-lzx' : { - desc : 'LZX Archive', - sig : '4c5a5800', - offset : 0, - archiveHandler : 'Lzx', - }, - 'application/x-7z-compressed' : { - desc : '7-Zip Archive', - sig : '377abcaf271c', - offset : 0, - archiveHandler : '7Zip', - }, - - // - // Generics that need further mapping - // - 'application/octet-stream' : [ - { - desc : 'Amiga DISKMASHER', - sig : '444d5321', // DMS! - ext : '.dms', - shortDescUtil : 'XDMS2Desc', - longDescUtil : 'XDMS2LongDesc', - }, - { - desc : 'SIO2PC Atari Disk Image', - sig : '9602', // 16bit sum of "NICKATARI" - ext : '.atr', - archiveHandler : 'Atr', - } - ] - }, - - archives : { - archivers : { - '7Zip' : { // p7zip package - compress : { - cmd : '7za', - args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], - }, - decompress : { - cmd : '7za', - args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'? - }, - list : { - cmd : '7za', - args : [ 'l', '{archivePath}' ], - entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$', - }, - extract : { - cmd : '7za', - args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ], - }, - }, - - InfoZip: { - compress : { - cmd : 'zip', - args : [ '{archivePath}', '{fileList}' ], - }, - decompress : { - cmd : 'unzip', - args : [ '-n', '{archivePath}', '-d', '{extractPath}' ], - }, - list : { - cmd : 'unzip', - args : [ '-l', '{archivePath}' ], - // Annoyingly, dates can be in YYYY-MM-DD or MM-DD-YYYY format - entryMatch : '^\\s*([0-9]+)\\s+[0-9]{2,4}-[0-9]{2}-[0-9]{2,4}\\s+[0-9]{2}:[0-9]{2}\\s+([^\\r\\n]+)$', - }, - extract : { - cmd : 'unzip', - args : [ '-n', '{archivePath}', '{fileList}', '-d', '{extractPath}' ], - } - }, - - Lha : { - // - // 'lha' command can be obtained from: - // * apt-get: lhasa - // - // (compress not currently supported) - // - decompress : { - cmd : 'lha', - args : [ '-efw={extractPath}', '{archivePath}' ], - }, - list : { - cmd : 'lha', - args : [ '-l', '{archivePath}' ], - entryMatch : '^[\\[a-z\\]]+(?:\\s+[0-9]+\\s+[0-9]+|\\s+)([0-9]+)\\s+[0-9]{2}\\.[0-9]\\%\\s+[A-Za-z]{3}\\s+[0-9]{1,2}\\s+[0-9]{4}\\s+([^\\r\\n]+)$', - }, - extract : { - cmd : 'lha', - args : [ '-efw={extractPath}', '{archivePath}', '{fileList}' ] - } - }, - - Lzx : { - // - // 'unlzx' command can be obtained from: - // * Debian based: https://launchpad.net/~rzr/+archive/ubuntu/ppa/+build/2486127 (amd64/x86_64) - // * RedHat: https://fedora.pkgs.org/28/rpm-sphere/unlzx-1.1-4.1.x86_64.rpm.html - // * Source: http://xavprods.free.fr/lzx/ - // - decompress : { - cmd : 'unlzx', - // unzlx doesn't have a output dir option, but we'll cwd to the temp output dir first - args : [ '-x', '{archivePath}' ], - }, - list : { - cmd : 'unlzx', - args : [ '-v', '{archivePath}' ], - entryMatch : '^\\s+([0-9]+)\\s+[^\\s]+\\s+[0-9]{2}:[0-9]{2}:[0-9]{2}\\s+[0-9]{1,2}-[a-z]{3}-[0-9]{4}\\s+[a-z\\-]+\\s+\\"([^"]+)\\"$', - } - }, - - Arj : { - // - // 'arj' command can be obtained from: - // * apt-get: arj - // - decompress : { - cmd : 'arj', - args : [ 'x', '{archivePath}', '{extractPath}' ], - }, - list : { - cmd : 'arj', - args : [ 'l', '{archivePath}' ], - entryMatch : '^([^\\s]+)\\s+([0-9]+)\\s+[0-9]+\\s[0-9\\.]+\\s+[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\s+(?:[^\\r\\n]+)$', - entryGroupOrder : { // defaults to { byteSize : 1, fileName : 2 } - fileName : 1, - byteSize : 2, - } - }, - extract : { - cmd : 'arj', - args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], - } - }, - - Rar : { - decompress : { - cmd : 'unrar', - args : [ 'x', '{archivePath}', '{extractPath}' ], - }, - list : { - cmd : 'unrar', - args : [ 'l', '{archivePath}' ], - entryMatch : '^\\s+[\\.A-Z]+\\s+([\\d]+)\\s{2}[0-9]{2,4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s{2}([^\\r\\n]+)$', - }, - extract : { - cmd : 'unrar', - args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], - } - }, - - TarGz : { - decompress : { - cmd : 'tar', - args : [ '-xf', '{archivePath}', '-C', '{extractPath}', '--strip-components=1' ], - }, - list : { - cmd : 'tar', - args : [ '-tvf', '{archivePath}' ], - entryMatch : '^[drwx\\-]{10}\\s[A-Za-z0-9\\/]+\\s+([0-9]+)\\s[0-9]{4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s([^\\r\\n]+)$', - }, - extract : { - cmd : 'tar', - args : [ '-xvf', '{archivePath}', '-C', '{extractPath}', '{fileList}' ], - } - }, - - Atr : { - decompress : { - cmd : 'atr', - args : [ '{archivePath}', 'x', '-a', '-o', '{extractPath}' ] - }, - list : { - cmd : 'atr', - args : [ '{archivePath}', 'ls', '-la1' ], - entryMatch : '^[rwxs-]{5}\\s+([0-9]+)\\s\\([0-9\\s]+\\)\\s([^\\r\\n\\s]*)(?:[^\\r\\n]+)?$', - }, - extract : { - cmd : 'atr', - // note: -l converts Atari 0x9b line feeds to 0x0a; not ideal if we're dealing with a binary of course. - args : [ '{archivePath}', 'x', '-a', '-l', '-o', '{extractPath}', '{fileList}' ] - } - } - }, - }, - - fileTransferProtocols : { - // - // See http://www.synchro.net/docs/sexyz.txt for information on SEXYZ - // - zmodem8kSexyz : { - name : 'ZModem 8k (SEXYZ)', - type : 'external', - sort : 1, - external : { - // :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems - // Linux x86_64 binary: https://l33t.codes/outgoing/sexyz - sendCmd : 'sexyz', - sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ], - recvCmd : 'sexyz', - recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ], - recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ], - } - }, - - xmodemSexyz : { - name : 'XModem (SEXYZ)', - type : 'external', - sort : 3, - external : { - sendCmd : 'sexyz', - sendArgs : [ '-telnet', 'sX', '@{fileListPath}' ], - recvCmd : 'sexyz', - recvArgsNonBatch : [ '-telnet', 'rC', '{fileName}' ] - } - }, - - ymodemSexyz : { - name : 'YModem (SEXYZ)', - type : 'external', - sort : 4, - external : { - sendCmd : 'sexyz', - sendArgs : [ '-telnet', 'sY', '@{fileListPath}' ], - recvCmd : 'sexyz', - recvArgs : [ '-telnet', 'ry', '{uploadDir}' ], - } - }, - - zmodem8kSz : { - name : 'ZModem 8k', - type : 'external', - sort : 2, - external : { - sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" - sendArgs : [ - // :TODO: try -q - '--zmodem', '--try-8k', '--binary', '--restricted', '{filePaths}' - ], - recvCmd : 'rz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" - recvArgs : [ - '--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir} - ], - processIACs : true, // escape/de-escape IACs (0xff) - } - } - }, - - messageAreaDefaults : { - // - // The following can be override per-area as well - // - maxMessages : 1024, // 0 = unlimited - maxAgeDays : 0, // 0 = unlimited - }, - - messageConferences : { - system_internal : { - name : 'System Internal', - desc : 'Built in conference for private messages, bulletins, etc.', - - areas : { - private_mail : { - name : 'Private Mail', - desc : 'Private user to user mail/email', - maxExternalSentAgeDays : 30, // max external "outbox" item age - }, - - local_bulletin : { - name : 'System Bulletins', - desc : 'Bulletin messages for all users', - } - } - } - }, - - scannerTossers : { - ftn_bso : { - paths : { - outbound : paths.join(__dirname, './../mail/ftn_out/'), - inbound : paths.join(__dirname, './../mail/ftn_in/'), - secInbound : paths.join(__dirname, './../mail/ftn_secin/'), - reject : paths.join(__dirname, './../mail/reject/'), // bad pkt, bundles, TIC attachments that fail any check, etc. - //outboundNetMail : paths.join(__dirname, './../mail/ftn_netmail_out/'), - // set 'retain' to a valid path to keep good pkt files - }, - - // - // Packet and (ArcMail) bundle target sizes are just that: targets. - // Actual sizes may be slightly larger when we must place a full - // PKT contents *somewhere* - // - packetTargetByteSize : 512000, // 512k, before placing messages in a new pkt - bundleTargetByteSize : 2048000, // 2M, before creating another archive - packetMsgEncoding : 'utf8', // default packet encoding. Override per node if desired. - packetAnsiMsgEncoding : 'cp437', // packet encoding for *ANSI ART* messages - - tic : { - secureInOnly : true, // only bring in from secure inbound (|secInbound| path, password protected) - uploadBy : 'ENiGMA TIC', // default upload by username (override @ network) - allowReplace : false, // use "Replaces" TIC field - descPriority : 'diz', // May be diz=.DIZ/etc., or tic=from TIC Ldesc - } - } - }, - - fileBase: { - // areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|: - areaStoragePrefix : paths.join(__dirname, './../file_base/'), - - maxDescFileByteSize : 471859, // ~1/4 MB - maxDescLongFileByteSize : 524288, // 1/2 MB - - fileNamePatterns: { - // These are NOT case sensitive - // FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ - // Some groups include a FILE_ID.ANS. We try to use that over FILE_ID.DIZ if available. - desc : [ - '^.*FILE_ID\.ANS$', '^.*FILE_ID\.DIZ$', // eslint-disable-line no-useless-escape - '^.*DESC\.SDI$', // eslint-disable-line no-useless-escape - '^.*DESCRIPT\.ION$', // eslint-disable-line no-useless-escape - '^.*FILE\.DES$', // eslint-disable-line no-useless-escape - '^.*FILE\.SDI$', // eslint-disable-line no-useless-escape - '^.*DISK\.ID$' // eslint-disable-line no-useless-escape - ], - - // common README filename - https://en.wikipedia.org/wiki/README - descLong : [ - '^[^/\]*\.NFO$', // eslint-disable-line no-useless-escape - '^.*README\.1ST$', // eslint-disable-line no-useless-escape - '^.*README\.NOW$', // eslint-disable-line no-useless-escape - '^.*README\.TXT$', // eslint-disable-line no-useless-escape - '^.*READ\.ME$', // eslint-disable-line no-useless-escape - '^.*README$', // eslint-disable-line no-useless-escape - '^.*README\.md$', // eslint-disable-line no-useless-escape - '^RELEASE-INFO.ASC$' // eslint-disable-line no-useless-escape - ], - }, - - yearEstPatterns: [ - // - // Patterns should produce the year in the first submatch. - // The extracted year may be YY or YYYY - // - '\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yyyy-mm-dd, yyyy/mm/dd, ... - '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1-2][0-9][0-9]{2}))\\b', // mm/dd/yyyy, mm.dd.yyyy, ... - '\\b((?:[1789][0-9]))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yy-mm-dd, yy-mm-dd, ... - '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1789][0-9]))\\b', // mm-dd-yy, mm/dd/yy, ... - //'\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]|[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', // yyyy-mm-dd, m/d/yyyy, mm-dd-yyyy, etc. - //"\\b('[1789][0-9])\\b", // eslint-disable-line quotes - '\\b[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', - '\\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)?,?\\s((?:[0-9]{2})?[0-9]{2})\\b', // November 29th, 1997 - '\\(((?:19|20)[0-9]{2})\\)', // (19xx) or (20xx) -- with parens -- do this before 19xx 20xx such that this has priority - '\\b((?:19|20)[0-9]{2})\\b', // simple 19xx or 20xx with word boundaries - '\\b\'([17-9][0-9])\\b', // '95, '17, ... - // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. - ], - - web : { - path : '/f/', - routePath : '/f/[a-zA-Z0-9]+$', - expireMinutes : 1440, // 1 day - }, - - // - // File area storage location tag/value pairs. - // Non-absolute paths are relative to |areaStoragePrefix|. - // - storageTags : { - sys_msg_attach : 'sys_msg_attach', - sys_temp_download : 'sys_temp_download', - }, - - areas: { - system_message_attachment : { - name : 'System Message Attachments', - desc : 'File attachments to messages', - storageTags : [ 'sys_msg_attach' ], - }, - - system_temporary_download : { - name : 'System Temporary Downloads', - desc : 'Temporary downloadables', - storageTags : [ 'sys_temp_download' ], - } - } - }, - - eventScheduler : { - - events : { - dailyMaintenance : { - schedule : 'at 11:59pm', - action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent', - }, - trimMessageAreas : { - // may optionally use [or ]@watch:/path/to/file - schedule : 'every 24 hours', - - // action: - // - @method:path/to/module.js:theMethodName - // (path is relative to ENiGMA base dir) - // - // - @execute:/path/to/something/executable.sh - // - action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', - }, - - nntpMaintenance : { - schedule : 'every 12 hours', // should generally be < trimMessageAreas interval - action : '@method:core/servers/content/nntp.js:performMaintenanceTask', - }, - - updateFileAreaStats : { - schedule : 'every 1 hours', - action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', - }, - - forgotPasswordMaintenance : { - schedule : 'every 24 hours', - action : '@method:core/web_password_reset.js:performMaintenanceTask', - args : [ '24 hours' ] // items older than this will be removed - }, - - twoFactorRegisterTokenMaintenance : { - schedule : 'every 24 hours', - action : '@method:core/user_temp_token.js:temporaryTokenMaintenanceTask', - args : [ - 'auth_factor2_otp_register', - '24 hours', // expire time - ] - }, - - // - // Enable the following entry in your config.hjson to periodically create/update - // DESCRIPT.ION files for your file base - // - /* - updateDescriptIonFiles : { - schedule : 'on the last day of the week', - action : '@method:core/file_base_list_export.js:updateFileBaseDescFilesScheduledEvent', - } - */ - } - }, - - logging : { - rotatingFile : { // set to 'disabled' or false to disable - type : 'rotating-file', - fileName : 'enigma-bbs.log', - period : '1d', - count : 3, - level : 'debug', - } - - // :TODO: syslog - https://github.com/mcavage/node-bunyan-syslog - }, - - debug : { - assertsEnabled : false, - }, - - statLog : { - systemEvents : { - loginHistoryMax: -1, // set to -1 for forever - } - }, - }; -} diff --git a/core/config_default.js b/core/config_default.js new file mode 100644 index 00000000..063f3899 --- /dev/null +++ b/core/config_default.js @@ -0,0 +1,928 @@ +const paths = require('path'); + +module.exports = () => { + return { + general : { + boardName : 'Another Fine ENiGMA½ BBS', + prettyBoardName : '|08A|07nother |07F|08ine |07E|08NiGMA|07½ B|08BS', + telnetHostname : '', + sshHostname : '', + website : 'https://enigma-bbs.github.io', + description : 'An ENiGMA½ BBS', + + // :TODO: closedSystem prob belongs under users{}? + closedSystem : false, // is the system closed to new users? + + menuFile : 'menu.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path + promptFile : 'prompt.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path + achievementFile : 'achievements.hjson', + }, + + users : { + usernameMin : 2, + usernameMax : 16, // Note that FidoNet wants 36 max + usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+ .]+$', + + passwordMin : 6, + passwordMax : 128, + + // + // The bad password list is a text file containing a password per line. + // Entries in this list are not allowed to be used on the system as they + // are known to be too common. + // + // A great resource can be found at https://github.com/danielmiessler/SecLists + // + // Current list source: https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/probable-v2-top12000.txt + // + badPassFile : paths.join(__dirname, '../misc/bad_passwords.txt'), + + realNameMax : 32, + locationMax : 32, + affilsMax : 32, + emailMax : 255, + webMax : 255, + + requireActivation : false, // require SysOp activation? false = auto-activate + + groups : [ 'users', 'sysops' ], // built in groups + defaultGroups : [ 'users' ], // default groups new users belong to + + newUserNames : [ 'new', 'apply' ], // Names reserved for applying + + badUserNames : [ + 'sysop', 'admin', 'administrator', 'root', 'all', + 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix', + 'server', 'client', 'notme' + ], + + preAuthIdleLogoutSeconds : 60 * 3, // 3m + idleLogoutSeconds : 60 * 6, // 6m + + failedLogin : { + disconnect : 3, // 0=disabled + lockAccount : 9, // 0=disabled; Mark user status as "locked" if >= N + autoUnlockMinutes : 60 * 6, // 0=disabled; Auto unlock after N minutes. + }, + unlockAtEmailPwReset : true, // if true, password reset via email will unlock locked accounts + + twoFactorAuth : { + method : 'googleAuth', + + otp : { + registerEmailText : paths.join(__dirname, '../misc/otp_register_email.template.txt'), + registerEmailHtml : paths.join(__dirname, '../misc/otp_register_email.template.html'), + registerPageTemplate : paths.join(__dirname, '../www/otp_register.template.html'), + } + } + }, + + theme : { + default : 'luciano_blocktronics', + preLogin : 'luciano_blocktronics', + + passwordChar : '*', + dateFormat : { + short : 'MM/DD/YYYY', + long : 'ddd, MMMM Do, YYYY', + }, + timeFormat : { + short : 'h:mm a', + }, + dateTimeFormat : { + short : 'MM/DD/YYYY h:mm a', + long : 'ddd, MMMM Do, YYYY, h:mm a', + } + }, + + menus : { + cls : true, // Clear screen before each menu by default? + }, + + paths : { + config : paths.join(__dirname, './../config/'), + security : paths.join(__dirname, './../config/security'), // certs, keys, etc. + mods : paths.join(__dirname, './../mods/'), + loginServers : paths.join(__dirname, './servers/login/'), + contentServers : paths.join(__dirname, './servers/content/'), + chatServers : paths.join(__dirname, './servers/chat/'), + + scannerTossers : paths.join(__dirname, './scanner_tossers/'), + mailers : paths.join(__dirname, './mailers/') , + + art : paths.join(__dirname, './../art/general/'), + themes : paths.join(__dirname, './../art/themes/'), + logs : paths.join(__dirname, './../logs/'), + db : paths.join(__dirname, './../db/'), + modsDb : paths.join(__dirname, './../db/mods/'), + dropFiles : paths.join(__dirname, './../drop/'), // + "/node/ + misc : paths.join(__dirname, './../misc/'), + }, + + loginServers : { + telnet : { + port : 8888, + enabled : true, + firstMenu : 'telnetConnected', + }, + ssh : { + port : 8889, + enabled : false, // default to false as PK/pass in config.hjson are required + // + // To enable SSH, perform the following steps: + // + // 1 - Generate a Private Key (PK): + // Currently ENiGMA 1/2 requires a PKCS#1 PEM formatted PK. + // To generate a secure PK, issue the following command: + // + // > openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ + // -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa \ + // -out ./config/security/ssh_private_key.pem -aes128 + // + // (The above is a more modern equivalent of the following): + // > openssl genrsa -aes128 -out ./config/security/ssh_private_key.pem 2048 + // + // 2 - Set 'privateKeyPass' to the password you used in step #1 + // + // 3 - Finally, set 'enabled' to 'true' + // + // Additional reading: + // - https://blog.sleeplessbeastie.eu/2017/12/28/how-to-generate-private-key/ + // - https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b + // + privateKeyPem : paths.join(__dirname, './../config/security/ssh_private_key.pem'), + firstMenu : 'sshConnected', + firstMenuNewUser : 'sshConnectedNewUser', + + // + // SSH details that can affect security. Stronger ciphers are better for example, + // but terminals such as SyncTERM require KEX diffie-hellman-group14-sha1, + // cipher 3des-cbc, etc. + // + // See https://github.com/mscdex/ssh2-streams for the full list of supported + // algorithms. + // + algorithms : { + kex : [ + 'ecdh-sha2-nistp256', + 'ecdh-sha2-nistp384', + 'ecdh-sha2-nistp521', + 'diffie-hellman-group-exchange-sha256', + 'diffie-hellman-group14-sha1', + 'diffie-hellman-group-exchange-sha1', + 'diffie-hellman-group1-sha1', + ], + cipher : [ + 'aes128-ctr', + 'aes192-ctr', + 'aes256-ctr', + 'aes128-gcm', + 'aes128-gcm@openssh.com', + 'aes256-gcm', + 'aes256-gcm@openssh.com', + 'aes256-cbc', + 'aes192-cbc', + 'aes128-cbc', + 'blowfish-cbc', + '3des-cbc', + 'arcfour256', + 'arcfour128', + 'cast128-cbc', + 'arcfour', + ], + hmac : [ + 'hmac-sha2-256', + 'hmac-sha2-512', + 'hmac-sha1', + 'hmac-md5', + 'hmac-sha2-256-96', + 'hmac-sha2-512-96', + 'hmac-ripemd160', + 'hmac-sha1-96', + 'hmac-md5-96', + ], + // note that we disable compression by default due to issues with many clients. YMMV. + compress : [ 'none' ] + }, + }, + webSocket : { + ws : { + // non-secure ws:// + enabled : false, + port : 8810, + }, + wss : { + // secure ws:// + // must provide valid certPem and keyPem + enabled : false, + port : 8811, + certPem : paths.join(__dirname, './../config/https_cert.pem'), + keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), + }, + }, + }, + + contentServers : { + web : { + domain : 'another-fine-enigma-bbs.org', + + staticRoot : paths.join(__dirname, './../www'), + + resetPassword : { + // + // The following templates have these variables available to them: + // + // * %BOARDNAME% : Name of BBS + // * %USERNAME% : Username of whom to reset password + // * %TOKEN% : Reset token + // * %RESET_URL% : In case of email, the link to follow for reset. In case of landing page, + // URL to POST submit reset form. + + // templates for pw reset *email* + resetPassEmailText : paths.join(__dirname, '../misc/reset_password_email.template.txt'), // plain text version + resetPassEmailHtml : paths.join(__dirname, '../misc/reset_password_email.template.html'), // HTML version + + // tempalte for pw reset *landing page* + // + resetPageTemplate : paths.join(__dirname, './../www/reset_password.template.html'), + }, + + http : { + enabled : false, + port : 8080, + }, + https : { + enabled : false, + port : 8443, + certPem : paths.join(__dirname, './../config/https_cert.pem'), + keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), + } + }, + + gopher : { + enabled : false, + port : 8070, + publicHostname : 'another-fine-enigma-bbs.org', + publicPort : 8070, // adjust if behind NAT/etc. + bannerFile : 'gopher_banner.asc', + + // + // Set messageConferences{} to maps of confTag -> [ areaTag1, areaTag2, ... ] + // to export message confs/areas + // + }, + + nntp : { + // internal caching of groups, message lists, etc. + cache : { + maxItems : 200, + maxAge : 1000 * 30, // 30s + }, + + // + // Set publicMessageConferences{} to a map of confTag -> [ areaTag1, areaTag2, ... ] + // in order to export *public* conf/areas that are available to anonymous + // NNTP users. Other conf/areas: Standard ACS rules apply. + // + publicMessageConferences: {}, + + nntp : { + enabled : false, + port : 8119, + }, + + nntps : { + enabled : false, + port : 8563, + certPem : paths.join(__dirname, './../config/nntps_cert.pem'), + keyPem : paths.join(__dirname, './../config/nntps_key.pem'), + } + } + }, + + chatServers : { + mrc: { + enabled : false, + serverHostname : 'mrc.bottomlessabyss.net', + serverPort : 5000, + retryDelay : 10000, + multiplexerPort : 5000, + } + }, + + infoExtractUtils : { + Exiftool2Desc : { + cmd : `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x + }, + Exiftool : { + cmd : 'exiftool', + args : [ + '-charset', 'utf8', '{filePath}', + // exclude the following: + '--directory', '--filepermissions', '--exiftoolversion', '--filename', '--filesize', + '--filemodifydate', '--fileaccessdate', '--fileinodechangedate', '--createdate', '--modifydate', + '--metadatadate', '--xmptoolkit' + ] + }, + XDMS2Desc : { + // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html + cmd : 'xdms', + args : [ 'd', '{filePath}' ] + }, + XDMS2LongDesc : { + // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html + cmd : 'xdms', + args : [ 'f', '{filePath}' ] + }, + }, + + fileTypes : { + // + // File types explicitly known to the system. Here we can configure + // information extraction, archive treatment, etc. + // + // MIME types can be found in mime-db: https://github.com/jshttp/mime-db + // + // Resources for signature/magic bytes: + // * http://www.garykessler.net/library/file_sigs.html + // + // + // :TODO: text/x-ansi -> SAUCE extraction for .ans uploads + // :TODO: textual : bool -- if text, we can view. + // :TODO: asText : { cmd, args[] } -> viewable text + + // + // Audio + // + 'audio/mpeg' : { + desc : 'MP3 Audio', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'application/pdf' : { + desc : 'Adobe PDF', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + // + // Video + // + 'video/mp4' : { + desc : 'MPEG Video', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'video/x-matroska ' : { + desc : 'Matroska Video', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'video/x-msvideo' : { + desc : 'Audio Video Interleave', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + // + // Images + // + 'image/jpeg' : { + desc : 'JPEG Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'image/png' : { + desc : 'Portable Network Graphic Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'image/gif' : { + desc : 'Graphics Interchange Format Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'image/webp' : { + desc : 'WebP Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + // + // Archives + // + 'application/zip' : { + desc : 'ZIP Archive', + sig : '504b0304', + offset : 0, + archiveHandler : 'InfoZip', + }, + /* + 'application/x-cbr' : { + desc : 'Comic Book Archive', + sig : '504b0304', + }, + */ + 'application/x-arj' : { + desc : 'ARJ Archive', + sig : '60ea', + offset : 0, + archiveHandler : 'Arj', + }, + 'application/x-rar-compressed' : { + desc : 'RAR Archive', + sig : '526172211a07', + offset : 0, + archiveHandler : 'Rar', + }, + 'application/gzip' : { + desc : 'Gzip Archive', + sig : '1f8b', + offset : 0, + archiveHandler : 'TarGz', + }, + // :TODO: application/x-bzip + 'application/x-bzip2' : { + desc : 'BZip2 Archive', + sig : '425a68', + offset : 0, + archiveHandler : '7Zip', + }, + 'application/x-lzh-compressed' : { + desc : 'LHArc Archive', + sig : '2d6c68', + offset : 2, + archiveHandler : 'Lha', + }, + 'application/x-lzx' : { + desc : 'LZX Archive', + sig : '4c5a5800', + offset : 0, + archiveHandler : 'Lzx', + }, + 'application/x-7z-compressed' : { + desc : '7-Zip Archive', + sig : '377abcaf271c', + offset : 0, + archiveHandler : '7Zip', + }, + + // + // Generics that need further mapping + // + 'application/octet-stream' : [ + { + desc : 'Amiga DISKMASHER', + sig : '444d5321', // DMS! + ext : '.dms', + shortDescUtil : 'XDMS2Desc', + longDescUtil : 'XDMS2LongDesc', + }, + { + desc : 'SIO2PC Atari Disk Image', + sig : '9602', // 16bit sum of "NICKATARI" + ext : '.atr', + archiveHandler : 'Atr', + } + ] + }, + + archives : { + archivers : { + '7Zip' : { // p7zip package + compress : { + cmd : '7za', + args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], + }, + decompress : { + cmd : '7za', + args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'? + }, + list : { + cmd : '7za', + args : [ 'l', '{archivePath}' ], + entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$', + }, + extract : { + cmd : '7za', + args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ], + }, + }, + + InfoZip: { + compress : { + cmd : 'zip', + args : [ '{archivePath}', '{fileList}' ], + }, + decompress : { + cmd : 'unzip', + args : [ '-n', '{archivePath}', '-d', '{extractPath}' ], + }, + list : { + cmd : 'unzip', + args : [ '-l', '{archivePath}' ], + // Annoyingly, dates can be in YYYY-MM-DD or MM-DD-YYYY format + entryMatch : '^\\s*([0-9]+)\\s+[0-9]{2,4}-[0-9]{2}-[0-9]{2,4}\\s+[0-9]{2}:[0-9]{2}\\s+([^\\r\\n]+)$', + }, + extract : { + cmd : 'unzip', + args : [ '-n', '{archivePath}', '{fileList}', '-d', '{extractPath}' ], + } + }, + + Lha : { + // + // 'lha' command can be obtained from: + // * apt-get: lhasa + // + // (compress not currently supported) + // + decompress : { + cmd : 'lha', + args : [ '-efw={extractPath}', '{archivePath}' ], + }, + list : { + cmd : 'lha', + args : [ '-l', '{archivePath}' ], + entryMatch : '^[\\[a-z\\]]+(?:\\s+[0-9]+\\s+[0-9]+|\\s+)([0-9]+)\\s+[0-9]{2}\\.[0-9]\\%\\s+[A-Za-z]{3}\\s+[0-9]{1,2}\\s+[0-9]{4}\\s+([^\\r\\n]+)$', + }, + extract : { + cmd : 'lha', + args : [ '-efw={extractPath}', '{archivePath}', '{fileList}' ] + } + }, + + Lzx : { + // + // 'unlzx' command can be obtained from: + // * Debian based: https://launchpad.net/~rzr/+archive/ubuntu/ppa/+build/2486127 (amd64/x86_64) + // * RedHat: https://fedora.pkgs.org/28/rpm-sphere/unlzx-1.1-4.1.x86_64.rpm.html + // * Source: http://xavprods.free.fr/lzx/ + // + decompress : { + cmd : 'unlzx', + // unzlx doesn't have a output dir option, but we'll cwd to the temp output dir first + args : [ '-x', '{archivePath}' ], + }, + list : { + cmd : 'unlzx', + args : [ '-v', '{archivePath}' ], + entryMatch : '^\\s+([0-9]+)\\s+[^\\s]+\\s+[0-9]{2}:[0-9]{2}:[0-9]{2}\\s+[0-9]{1,2}-[a-z]{3}-[0-9]{4}\\s+[a-z\\-]+\\s+\\"([^"]+)\\"$', + } + }, + + Arj : { + // + // 'arj' command can be obtained from: + // * apt-get: arj + // + decompress : { + cmd : 'arj', + args : [ 'x', '{archivePath}', '{extractPath}' ], + }, + list : { + cmd : 'arj', + args : [ 'l', '{archivePath}' ], + entryMatch : '^([^\\s]+)\\s+([0-9]+)\\s+[0-9]+\\s[0-9\\.]+\\s+[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\s+(?:[^\\r\\n]+)$', + entryGroupOrder : { // defaults to { byteSize : 1, fileName : 2 } + fileName : 1, + byteSize : 2, + } + }, + extract : { + cmd : 'arj', + args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], + } + }, + + Rar : { + decompress : { + cmd : 'unrar', + args : [ 'x', '{archivePath}', '{extractPath}' ], + }, + list : { + cmd : 'unrar', + args : [ 'l', '{archivePath}' ], + entryMatch : '^\\s+[\\.A-Z]+\\s+([\\d]+)\\s{2}[0-9]{2,4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s{2}([^\\r\\n]+)$', + }, + extract : { + cmd : 'unrar', + args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], + } + }, + + TarGz : { + decompress : { + cmd : 'tar', + args : [ '-xf', '{archivePath}', '-C', '{extractPath}', '--strip-components=1' ], + }, + list : { + cmd : 'tar', + args : [ '-tvf', '{archivePath}' ], + entryMatch : '^[drwx\\-]{10}\\s[A-Za-z0-9\\/]+\\s+([0-9]+)\\s[0-9]{4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s([^\\r\\n]+)$', + }, + extract : { + cmd : 'tar', + args : [ '-xvf', '{archivePath}', '-C', '{extractPath}', '{fileList}' ], + } + }, + + Atr : { + decompress : { + cmd : 'atr', + args : [ '{archivePath}', 'x', '-a', '-o', '{extractPath}' ] + }, + list : { + cmd : 'atr', + args : [ '{archivePath}', 'ls', '-la1' ], + entryMatch : '^[rwxs-]{5}\\s+([0-9]+)\\s\\([0-9\\s]+\\)\\s([^\\r\\n\\s]*)(?:[^\\r\\n]+)?$', + }, + extract : { + cmd : 'atr', + // note: -l converts Atari 0x9b line feeds to 0x0a; not ideal if we're dealing with a binary of course. + args : [ '{archivePath}', 'x', '-a', '-l', '-o', '{extractPath}', '{fileList}' ] + } + } + }, + }, + + fileTransferProtocols : { + // + // See http://www.synchro.net/docs/sexyz.txt for information on SEXYZ + // + zmodem8kSexyz : { + name : 'ZModem 8k (SEXYZ)', + type : 'external', + sort : 1, + external : { + // :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems + // Linux x86_64 binary: https://l33t.codes/outgoing/sexyz + sendCmd : 'sexyz', + sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ], + recvCmd : 'sexyz', + recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ], + recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ], + } + }, + + xmodemSexyz : { + name : 'XModem (SEXYZ)', + type : 'external', + sort : 3, + external : { + sendCmd : 'sexyz', + sendArgs : [ '-telnet', 'sX', '@{fileListPath}' ], + recvCmd : 'sexyz', + recvArgsNonBatch : [ '-telnet', 'rC', '{fileName}' ] + } + }, + + ymodemSexyz : { + name : 'YModem (SEXYZ)', + type : 'external', + sort : 4, + external : { + sendCmd : 'sexyz', + sendArgs : [ '-telnet', 'sY', '@{fileListPath}' ], + recvCmd : 'sexyz', + recvArgs : [ '-telnet', 'ry', '{uploadDir}' ], + } + }, + + zmodem8kSz : { + name : 'ZModem 8k', + type : 'external', + sort : 2, + external : { + sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" + sendArgs : [ + // :TODO: try -q + '--zmodem', '--try-8k', '--binary', '--restricted', '{filePaths}' + ], + recvCmd : 'rz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" + recvArgs : [ + '--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir} + ], + processIACs : true, // escape/de-escape IACs (0xff) + } + } + }, + + messageAreaDefaults : { + // + // The following can be override per-area as well + // + maxMessages : 1024, // 0 = unlimited + maxAgeDays : 0, // 0 = unlimited + }, + + messageConferences : { + system_internal : { + name : 'System Internal', + desc : 'Built in conference for private messages, bulletins, etc.', + + areas : { + private_mail : { + name : 'Private Mail', + desc : 'Private user to user mail/email', + maxExternalSentAgeDays : 30, // max external "outbox" item age + }, + + local_bulletin : { + name : 'System Bulletins', + desc : 'Bulletin messages for all users', + } + } + } + }, + + scannerTossers : { + ftn_bso : { + paths : { + outbound : paths.join(__dirname, './../mail/ftn_out/'), + inbound : paths.join(__dirname, './../mail/ftn_in/'), + secInbound : paths.join(__dirname, './../mail/ftn_secin/'), + reject : paths.join(__dirname, './../mail/reject/'), // bad pkt, bundles, TIC attachments that fail any check, etc. + //outboundNetMail : paths.join(__dirname, './../mail/ftn_netmail_out/'), + // set 'retain' to a valid path to keep good pkt files + }, + + // + // Packet and (ArcMail) bundle target sizes are just that: targets. + // Actual sizes may be slightly larger when we must place a full + // PKT contents *somewhere* + // + packetTargetByteSize : 512000, // 512k, before placing messages in a new pkt + bundleTargetByteSize : 2048000, // 2M, before creating another archive + packetMsgEncoding : 'utf8', // default packet encoding. Override per node if desired. + packetAnsiMsgEncoding : 'cp437', // packet encoding for *ANSI ART* messages + + tic : { + secureInOnly : true, // only bring in from secure inbound (|secInbound| path, password protected) + uploadBy : 'ENiGMA TIC', // default upload by username (override @ network) + allowReplace : false, // use "Replaces" TIC field + descPriority : 'diz', // May be diz=.DIZ/etc., or tic=from TIC Ldesc + } + } + }, + + fileBase: { + // areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|: + areaStoragePrefix : paths.join(__dirname, './../file_base/'), + + maxDescFileByteSize : 471859, // ~1/4 MB + maxDescLongFileByteSize : 524288, // 1/2 MB + + fileNamePatterns: { + // These are NOT case sensitive + // FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ + // Some groups include a FILE_ID.ANS. We try to use that over FILE_ID.DIZ if available. + desc : [ + '^.*FILE_ID\.ANS$', '^.*FILE_ID\.DIZ$', // eslint-disable-line no-useless-escape + '^.*DESC\.SDI$', // eslint-disable-line no-useless-escape + '^.*DESCRIPT\.ION$', // eslint-disable-line no-useless-escape + '^.*FILE\.DES$', // eslint-disable-line no-useless-escape + '^.*FILE\.SDI$', // eslint-disable-line no-useless-escape + '^.*DISK\.ID$' // eslint-disable-line no-useless-escape + ], + + // common README filename - https://en.wikipedia.org/wiki/README + descLong : [ + '^[^/\]*\.NFO$', // eslint-disable-line no-useless-escape + '^.*README\.1ST$', // eslint-disable-line no-useless-escape + '^.*README\.NOW$', // eslint-disable-line no-useless-escape + '^.*README\.TXT$', // eslint-disable-line no-useless-escape + '^.*READ\.ME$', // eslint-disable-line no-useless-escape + '^.*README$', // eslint-disable-line no-useless-escape + '^.*README\.md$', // eslint-disable-line no-useless-escape + '^RELEASE-INFO.ASC$' // eslint-disable-line no-useless-escape + ], + }, + + yearEstPatterns: [ + // + // Patterns should produce the year in the first submatch. + // The extracted year may be YY or YYYY + // + '\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yyyy-mm-dd, yyyy/mm/dd, ... + '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1-2][0-9][0-9]{2}))\\b', // mm/dd/yyyy, mm.dd.yyyy, ... + '\\b((?:[1789][0-9]))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yy-mm-dd, yy-mm-dd, ... + '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1789][0-9]))\\b', // mm-dd-yy, mm/dd/yy, ... + //'\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]|[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', // yyyy-mm-dd, m/d/yyyy, mm-dd-yyyy, etc. + //"\\b('[1789][0-9])\\b", // eslint-disable-line quotes + '\\b[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', + '\\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)?,?\\s((?:[0-9]{2})?[0-9]{2})\\b', // November 29th, 1997 + '\\(((?:19|20)[0-9]{2})\\)', // (19xx) or (20xx) -- with parens -- do this before 19xx 20xx such that this has priority + '\\b((?:19|20)[0-9]{2})\\b', // simple 19xx or 20xx with word boundaries + '\\b\'([17-9][0-9])\\b', // '95, '17, ... + // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. + ], + + web : { + path : '/f/', + routePath : '/f/[a-zA-Z0-9]+$', + expireMinutes : 1440, // 1 day + }, + + // + // File area storage location tag/value pairs. + // Non-absolute paths are relative to |areaStoragePrefix|. + // + storageTags : { + sys_msg_attach : 'sys_msg_attach', + sys_temp_download : 'sys_temp_download', + }, + + areas: { + system_message_attachment : { + name : 'System Message Attachments', + desc : 'File attachments to messages', + storageTags : [ 'sys_msg_attach' ], + }, + + system_temporary_download : { + name : 'System Temporary Downloads', + desc : 'Temporary downloadables', + storageTags : [ 'sys_temp_download' ], + } + } + }, + + eventScheduler : { + + events : { + dailyMaintenance : { + schedule : 'at 11:59pm', + action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent', + }, + trimMessageAreas : { + // may optionally use [or ]@watch:/path/to/file + schedule : 'every 24 hours', + + // action: + // - @method:path/to/module.js:theMethodName + // (path is relative to ENiGMA base dir) + // + // - @execute:/path/to/something/executable.sh + // + action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', + }, + + nntpMaintenance : { + schedule : 'every 12 hours', // should generally be < trimMessageAreas interval + action : '@method:core/servers/content/nntp.js:performMaintenanceTask', + }, + + updateFileAreaStats : { + schedule : 'every 1 hours', + action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', + }, + + forgotPasswordMaintenance : { + schedule : 'every 24 hours', + action : '@method:core/web_password_reset.js:performMaintenanceTask', + args : [ '24 hours' ] // items older than this will be removed + }, + + twoFactorRegisterTokenMaintenance : { + schedule : 'every 24 hours', + action : '@method:core/user_temp_token.js:temporaryTokenMaintenanceTask', + args : [ + 'auth_factor2_otp_register', + '24 hours', // expire time + ] + }, + + // + // Enable the following entry in your config.hjson to periodically create/update + // DESCRIPT.ION files for your file base + // + /* + updateDescriptIonFiles : { + schedule : 'on the last day of the week', + action : '@method:core/file_base_list_export.js:updateFileBaseDescFilesScheduledEvent', + } + */ + } + }, + + logging : { + rotatingFile : { // set to 'disabled' or false to disable + type : 'rotating-file', + fileName : 'enigma-bbs.log', + period : '1d', + count : 3, + level : 'debug', + } + + // :TODO: syslog - https://github.com/mcavage/node-bunyan-syslog + }, + + debug : { + assertsEnabled : false, + }, + + statLog : { + systemEvents : { + loginHistoryMax: -1, // set to -1 for forever + } + }, + }; +}; \ No newline at end of file diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index 8a51fdb5..4548a910 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -159,7 +159,7 @@ function askNewConfigQuestions(cb) { }, function basic(callback) { getAnswers(QUESTIONS.Basic, answers => { - const defaultConfig = require('../../core/config.js').getDefaultConfig(); + const defaultConfig = require('../../core/config_default')(); // start by plopping in values we want directly from config.js const template = hjson.rt.parse(fs.readFileSync(paths.join(__dirname, '../../misc/config_template.in.hjson'), 'utf8')); From eb4f93328585ba98b17e4403ae154a7edacb2871 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 7 Jun 2020 16:25:18 -0600 Subject: [PATCH 02/43] Update various refs to config.js to config_default.js --- WHATSNEW.md | 1 + docs/configuration/archivers.md | 2 +- docs/configuration/config-hjson.md | 6 +++--- docs/configuration/file-transfer-protocols.md | 4 ++-- docs/modding/file-transfer-protocol-select.md | 2 +- docs/servers/ssh.md | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index e587adf8..35c26086 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -3,6 +3,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For ## 0.0.12-beta * The `master` branch has become mainline. What this means to users is `git pull` will always give you the latest and greatest. Make sure to read [Updating](/docs/admin/updating.md) and keep an eye on `WHATSNEW.md` (this file) and [UPGRADE](UPGRADE.md)! See also [ticket #276](https://github.com/NuSkooler/enigma-bbs/issues/276). +* The default configuration has been moved to [config_default.js](/core/config_default.js). ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/docs/configuration/archivers.md b/docs/configuration/archivers.md index 3f5d941e..52f724e5 100644 --- a/docs/configuration/archivers.md +++ b/docs/configuration/archivers.md @@ -74,7 +74,7 @@ For `list` commands, the `entryMatch` key must be provided. This key should prov ``` ## Archive Formats -Archive formats can be defined such that ENiGMA½ can detect them by signature or extension, then utilize the correct *archiver* to process them. Formats are defined in the `archives:formats` key in `config.hjson`. Many differnet types come pre-configured (see `core/config.js`). +Archive formats can be defined such that ENiGMA½ can detect them by signature or extension, then utilize the correct *archiver* to process them. Formats are defined in the `archives:formats` key in `config.hjson`. Many differnet types come pre-configured (see `core/config_default.js`). ### Example Archive Format Configuration ``` diff --git a/docs/configuration/config-hjson.md b/docs/configuration/config-hjson.md index 38392943..6f426a68 100644 --- a/docs/configuration/config-hjson.md +++ b/docs/configuration/config-hjson.md @@ -3,7 +3,7 @@ layout: page title: System Configuration --- ## System Configuration -The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `/enigma-bbs-install-path/config/config.hjson` though you can override the `config.hjson` location with the `--config` parameter when invoking `main.js`. Values found in `core/config.js` may be overridden by simply providing the object members you wish replace. +The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `/enigma-bbs-install-path/config/config.hjson` though you can override the `config.hjson` location with the `--config` parameter when invoking `main.js`. Values found in `core/config_default.js` may be overridden by simply providing the object members you wish replace. See also [HJSON General Information](hjson.md) for more information on the HJSON format. @@ -16,7 +16,7 @@ Your initial configuration skeleton should be created using the `oputil.js` comm You will be asked a series of questions to create an initial configuration. ### Overriding Defaults -The file `core/config.js` provides various defaults to the system that you can override via `config.hjson`. For example, the default system name is defined as follows: +The file `core/config_default.js` provides various defaults to the system that you can override via `config.hjson`. For example, the default system name is defined as follows: ```javascript general : { boardName : 'Another Fine ENiGMA½ System' @@ -32,7 +32,7 @@ general: { (Note the very slightly [HJSON](hjson.md) different syntax. **You can use standard JSON if you wish!**) -While not everything that is available in your `config.hjson` file can be found defaulted in `core/config.js`, a lot is. [Poke around and see what you can find](https://github.com/NuSkooler/enigma-bbs/blob/master/core/config.js)! +While not everything that is available in your `config.hjson` file can be found defaulted in `core/config_default.js`, a lot is. [Poke around and see what you can find](https://github.com/NuSkooler/enigma-bbs/blob/master/core/config_default.js)! ### Configuration Sections Below is a list of various configuration sections. There are many more, but this should get you started: diff --git a/docs/configuration/file-transfer-protocols.md b/docs/configuration/file-transfer-protocols.md index 2f7d48ac..a7dec599 100644 --- a/docs/configuration/file-transfer-protocols.md +++ b/docs/configuration/file-transfer-protocols.md @@ -28,12 +28,12 @@ For protocols of type `external` the following members may be defined: * `sendCmd`: Required for protocols that can send (allow user downloads); The command/binary to execute. * `sendArgs`: Required if using `sendCmd`; An array of arguments. A placeholder of `{fileListPath}` may be used to supply a path to a **file containing** a list of files to send, or `{filePaths}` to supply *1:n* individual file paths to send. * `recvCmd`: Required for protocols that can receive (allow user uploads); The command/binary to execute. -* `recvArgs`: Required if using `recvCmd` and supporting **batch** uploads; An array of arguments. A placeholder of `{uploadDir}` may be used to supply the system provided upload directory. If `{uploadDir}` is not present, the system expects uploaded files to be placed in CWD which will be set to the upload directory. +* `recvArgs`: Required if using `recvCmd` and supporting **batch** uploads; An array of arguments. A placeholder of `{uploadDir}` may be used to supply the system provided upload directory. If `{uploadDir}` is not present, the system expects uploaded files to be placed in CWD which will be set to the upload directory. * `recvArgsNonBatch`: Required if using `recvCmd` and supporting non-batch (single file) uploads; A placeholder of `{fileName}` may be supplied to indicate to the protocol what the uploaded file should be named (this will be collected from the user before the upload starts). * `escapeTelnet`: Optional; If set to `true`, escape all internal Telnet related codes such as IAC's. This option is required for external protocol handlers such as `sz` and `rz` that do not escape themselves. ### Adding Your Own -Take a look a the example below as well as [core/config.js](/core/config.js). +Take a look a the example below as well as [core/config_default.js](/core/config_default.js). #### Example File Transfer Protocol Configuration ``` diff --git a/docs/modding/file-transfer-protocol-select.md b/docs/modding/file-transfer-protocol-select.md index 72b8d124..741b6fbb 100644 --- a/docs/modding/file-transfer-protocol-select.md +++ b/docs/modding/file-transfer-protocol-select.md @@ -9,5 +9,5 @@ The built in `file_transfer_protocol_select` module provides a way to select a l ### Theming The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`) (the protocol list): -* `name`: The name of the protocol. Each entry is +op defined in `config.hjson` with defaults found in `config.js`. Note that the standard `{text}` field also contains this value. +* `name`: The name of the protocol. Each entry is +op defined in `config.hjson` with defaults found in `config_default.js`. Note that the standard `{text}` field also contains this value. diff --git a/docs/servers/ssh.md b/docs/servers/ssh.md index 2f8b7769..45a6a3ea 100644 --- a/docs/servers/ssh.md +++ b/docs/servers/ssh.md @@ -17,7 +17,7 @@ Entries available under `config.loginServers.ssh`: | `enabled` | :+1: | Set to `true` to enable the SSH server. | | `port` | :-1: | Override the default port of `8443`. | | `address` | :-1: | Sets an explicit bind address. | -| `algorithms` | :-1: | Configuration block for SSH algorithms. Includes keys of `kex`, `cipher`, `hmac`, and `compress`. See the algorithms section in the [ssh2-streams](https://github.com/mscdex/ssh2-streams#ssh2stream-methods) documentation for details. For defaults set by ENiGMA½, see `core/config.js`. +| `algorithms` | :-1: | Configuration block for SSH algorithms. Includes keys of `kex`, `cipher`, `hmac`, and `compress`. See the algorithms section in the [ssh2-streams](https://github.com/mscdex/ssh2-streams#ssh2stream-methods) documentation for details. For defaults set by ENiGMA½, see `core/config_default.js`. | `traceConnections` | :-1: | Set to `true` to enable full trace-level information on SSH connections. ### Example Configuration From dd2a6258d52bd72acfdf21132add58e90d005d69 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 7 Jun 2020 21:21:31 -0600 Subject: [PATCH 03/43] Fix up some deprecated config refs --- core/bbs.js | 7 ++++--- core/config.js | 4 +--- core/database.js | 34 ++++++++++++++++++---------------- core/login_server_module.js | 4 ++-- core/theme.js | 4 ++-- 5 files changed, 27 insertions(+), 26 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index e29b8d27..a0256d3e 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -175,10 +175,11 @@ function initialize(cb) { async.series( [ function createMissingDirectories(callback) { - async.each(Object.keys(conf.config.paths), function entry(pathKey, next) { - mkdirs(conf.config.paths[pathKey], function dirCreated(err) { + const Config = conf.get(); + async.each(Object.keys(Config.paths), function entry(pathKey, next) { + mkdirs(Config.paths[pathKey], function dirCreated(err) { if(err) { - console.error('Could not create path: ' + conf.config.paths[pathKey] + ': ' + err.toString()); + console.error('Could not create path: ' + Config.paths[pathKey] + ': ' + err.toString()); } return next(err); }); diff --git a/core/config.js b/core/config.js index 4a235c5e..d50ff356 100644 --- a/core/config.js +++ b/core/config.js @@ -106,9 +106,7 @@ function mergeValidateAndFinalize(config, cb) { return callback(null, mergedConfig); }, function setIt(mergedConfig, callback) { - // :TODO: .config property is to be deprecated once conversions are done - exports.config = currentConfiguration = mergedConfig; - + currentConfiguration = mergedConfig; exports.get = () => currentConfiguration; return callback(null); } diff --git a/core/database.js b/core/database.js index e0e58168..dd7cd88e 100644 --- a/core/database.js +++ b/core/database.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const conf = require('./config.js'); +const conf = require('./config'); // deps const sqlite3 = require('sqlite3'); @@ -30,7 +30,8 @@ function getTransactionDatabase(db) { } function getDatabasePath(name) { - return paths.join(conf.config.paths.db, `${name}.sqlite3`); + const Config = conf.get(); + return paths.join(Config.paths.db, `${name}.sqlite3`); } function getModDatabasePath(moduleInfo, suffix) { @@ -53,7 +54,8 @@ function getModDatabasePath(moduleInfo, suffix) { (full.split('.').length > 1 && HOST_RE.test(full)), 'packageName must follow Reverse Domain Name Notation - https://en.wikipedia.org/wiki/Reverse_domain_name_notation'); - return paths.join(conf.config.paths.modsDb, `${full}.sqlite3`); + const Config = conf.get(); + return paths.join(Config.paths.modsDb, `${full}.sqlite3`); } function loadDatabaseForMod(modInfo, cb) { @@ -162,9 +164,9 @@ const DB_INIT_TABLE = { enableForeignKeys(dbs.user); dbs.user.run( - `CREATE TABLE IF NOT EXISTS user ( + `CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY, - user_name VARCHAR NOT NULL, + user_name VARCHAR NOT NULL, UNIQUE(user_name) );` ); @@ -177,13 +179,13 @@ const DB_INIT_TABLE = { prop_name VARCHAR NOT NULL, prop_value VARCHAR, UNIQUE(user_id, prop_name), - FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE + FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE );` ); dbs.user.run( - `CREATE TABLE IF NOT EXISTS user_group_member ( - group_name VARCHAR NOT NULL, + `CREATE TABLE IF NOT EXISTS user_group_member ( + group_name VARCHAR NOT NULL, user_id INTEGER NOT NULL, UNIQUE(group_name, user_id) );` @@ -227,9 +229,9 @@ const DB_INIT_TABLE = { dbs.message.run( `CREATE TABLE IF NOT EXISTS message ( - message_id INTEGER PRIMARY KEY, + message_id INTEGER PRIMARY KEY, area_tag VARCHAR NOT NULL, - message_uuid VARCHAR(36) NOT NULL, + message_uuid VARCHAR(36) NOT NULL, reply_to_message_id INTEGER, to_user_name VARCHAR NOT NULL, from_user_name VARCHAR NOT NULL, @@ -237,7 +239,7 @@ const DB_INIT_TABLE = { message, /* FTS @ message_fts */ modified_timestamp DATETIME NOT NULL, view_count INTEGER NOT NULL DEFAULT 0, - UNIQUE(message_uuid) + UNIQUE(message_uuid) );` ); @@ -284,7 +286,7 @@ const DB_INIT_TABLE = { meta_category INTEGER NOT NULL, meta_name VARCHAR NOT NULL, meta_value VARCHAR NOT NULL, - UNIQUE(message_id, meta_category, meta_name, meta_value), + UNIQUE(message_id, meta_category, meta_name, meta_value), FOREIGN KEY(message_id) REFERENCES message(message_id) ON DELETE CASCADE );` ); @@ -342,7 +344,7 @@ const DB_INIT_TABLE = { file_name, /* FTS @ file_fts */ storage_tag VARCHAR NOT NULL, desc, /* FTS @ file_fts */ - desc_long, /* FTS @ file_fts */ + desc_long, /* FTS @ file_fts */ upload_timestamp DATETIME NOT NULL );` ); @@ -395,7 +397,7 @@ const DB_INIT_TABLE = { file_id INTEGER NOT NULL, meta_name VARCHAR NOT NULL, meta_value VARCHAR NOT NULL, - UNIQUE(file_id, meta_name, meta_value), + UNIQUE(file_id, meta_name, meta_value), FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE );` ); @@ -404,7 +406,7 @@ const DB_INIT_TABLE = { `CREATE TABLE IF NOT EXISTS hash_tag ( hash_tag_id INTEGER PRIMARY KEY, hash_tag VARCHAR NOT NULL, - + UNIQUE(hash_tag) );` ); @@ -413,7 +415,7 @@ const DB_INIT_TABLE = { `CREATE TABLE IF NOT EXISTS file_hash_tag ( hash_tag_id INTEGER NOT NULL, file_id INTEGER NOT NULL, - + UNIQUE(hash_tag_id, file_id) );` ); diff --git a/core/login_server_module.js b/core/login_server_module.js index da3e06de..6a47f7c5 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const conf = require('./config.js'); +const Config = require('./config').get; const logger = require('./logger.js'); const ServerModule = require('./server_module.js').ServerModule; const clientConns = require('./client_connections.js'); @@ -28,7 +28,7 @@ module.exports = class LoginServerModule extends ServerModule { // // Choose initial theme before we have user context // - const preLoginTheme = _.get(conf.config, 'theme.preLogin'); + const preLoginTheme = _.get(Config(), 'theme.preLogin'); if('*' === preLoginTheme) { client.user.properties[UserProps.ThemeId] = theme.getRandomTheme() || ''; } else { diff --git a/core/theme.js b/core/theme.js index a2d31df1..c477f92e 100644 --- a/core/theme.js +++ b/core/theme.js @@ -433,10 +433,10 @@ function getThemeArt(options, cb) { const config = Config(); if(!options.themeId && _.has(options, [ 'client', 'user', 'properties', UserProps.ThemeId ])) { options.themeId = options.client.user.properties[UserProps.ThemeId]; - } else { - options.themeId = config.theme.default; } + options.themeId = options.themeId || config.theme.default; + // :TODO: replace asAnsi stuff with something like retrieveAs = 'ansi' | 'pipe' | ... // :TODO: Some of these options should only be set if not provided! options.asAnsi = true; // always convert to ANSI From cfaff61abf7ebfea3354c18fe57fdb047064de5f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 7 Jun 2020 22:20:34 -0600 Subject: [PATCH 04/43] WIP new Configuration class --- core/config.js | 104 ++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 3 +- yarn.lock | 13 +++++++ 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/core/config.js b/core/config.js index d50ff356..f6e7eee3 100644 --- a/core/config.js +++ b/core/config.js @@ -8,12 +8,114 @@ const DefaultConfig = require('./config_default'); // deps const paths = require('path'); const async = require('async'); -const _ = require('lodash'); const assert = require('assert'); +const _ = require('lodash'); +const reduceDeep = require('deepdash/getReduceDeep')(_); + exports.init = init; exports.getDefaultPath = getDefaultPath; +class Configuration { + constructor(path, options) { + this.current = {}; + } + + static create(path, options, cb) { + + } + + get() { + return this.current; + } + + _convertTo(value, type) { + switch (type) { + case 'bool' : + case 'boolean' : + value = 'true' === value.toLowerCase(); + break; + + case 'number' : + { + const num = parseInt(value); + if (!isNaN(num)) { + value = num; + } + } + break; + + case 'object' : + try { + value = JSON.parse(value); + } catch(e) { } + break; + + case 'date' : + case 'time' : + case 'datetime' : + case 'timestamp' : + { + const m = moment(value); + if (m.isValid()) { + value = m; + } + } + break; + + case 'regex' : + // :TODO: What flags to use, etc.? + break; + } + + return value; + } + + _resolveEnvironmentVariable(spec) { + const [prefix, varName, type, array] = spec.split(':'); + if (!varName) { + return; + } + + let value = process.env[varName]; + if (!value) { + return; + } + + if ('array' === array) { + value = value.split(',').map(v => this._convertTo(v, type)); + } else { + value = this._convertTo(value, type); + } + + return value; + } + + _resolveCurrent() { + reduceDeep( + this.current, + (acc, value, key, parent, ctx) => { + // resolve self references; there may be a better way... + if (_.isString(value) && '@' === value.charAt(0)) { + if (value.startsWith('@reference:')) { + value = value.slice(11); + const ref = _.get(acc, value); + if (ref) { + _.set(acc, ctx.path, ref); + } + } else if (value.startsWith('@environment:')) { + value = this._resolveEnvironmentVariable(value); + if (!_.isUndefined(value)) { + _.set(acc, ctx.path, value); + } + } + } + return acc; + } + ); + } +}; + let currentConfiguration = {}; function hasMessageConferenceAndArea(config) { diff --git a/package.json b/package.json index 3c5c770d..509fde49 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "uuid-parse": "1.1.0", "ws": "^7.3.0", "xxhash": "^0.3.0", - "yazl": "^2.5.1" + "yazl": "^2.5.1", + "deepdash" : "^5.1.1" }, "devDependencies": {}, "engines": { diff --git a/yarn.lock b/yarn.lock index 492d4761..bd556bfc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -372,6 +372,14 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== +deepdash@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/deepdash/-/deepdash-5.1.1.tgz#dcf68b9e15085b5df18bdb4011723790e0c40430" + integrity sha512-esz3pjQJaeYO4z74seqCMrOYUsAAdrhO3KJuEnGEaxTGbSy8VGOWn7jTU2J3nR5WDyNpS5/hse3m/hdM1/8ZWA== + dependencies: + lodash "^4.17.15" + lodash-es "^4.17.15" + define-property@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" @@ -978,6 +986,11 @@ later@1.2.0: resolved "https://registry.yarnpkg.com/later/-/later-1.2.0.tgz#f2cf6c4dd7956dd2f520adf0329836e9876bad0f" integrity sha1-8s9sTdeVbdL1IK3wMpg26YdrrQ8= +lodash-es@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" + integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== + lodash@^4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" From e674079f539ed2df717b4f20ab2fa2a9331f979d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Jun 2020 21:18:17 -0600 Subject: [PATCH 05/43] noWatch -> hotReload --- core/archive_util.js | 8 ++++---- core/config_cache.js | 12 +++++++----- core/oputil/oputil_common.js | 4 ++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index 47291860..86bb7ce7 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -50,17 +50,17 @@ module.exports = class ArchiveUtil { } // singleton access - static getInstance(noWatch = false) { + static getInstance(hotReload = true) { if(!archiveUtil) { archiveUtil = new ArchiveUtil(); - archiveUtil.init(noWatch); + archiveUtil.init(hotReload); } return archiveUtil; } - init(noWatch = false) { + init(hotReload = true) { this.reloadConfig(); - if(!noWatch) { + if(hotReload) { Events.on(Events.getSystemEvents().ConfigChanged, () => { this.reloadConfig(); }); diff --git a/core/config_cache.js b/core/config_cache.js index 62c6bb55..631fb5f1 100644 --- a/core/config_cache.js +++ b/core/config_cache.js @@ -2,10 +2,11 @@ 'use strict'; // deps -const paths = require('path'); -const fs = require('graceful-fs'); -const hjson = require('hjson'); -const sane = require('sane'); +const paths = require('path'); +const fs = require('graceful-fs'); +const hjson = require('hjson'); +const sane = require('sane'); +const _ = require('lodash'); module.exports = new class ConfigCache { @@ -14,12 +15,13 @@ module.exports = new class ConfigCache } getConfigWithOptions(options, cb) { + options.hotReload = _.get(options, 'hotReload', true); const cached = this.cache.has(options.filePath); if(options.forceReCache || !cached) { this.recacheConfigFromFile(options.filePath, (err, config) => { if(!err && !cached) { - if(!options.noWatch) { + if(options.hotReload) { const watcher = sane( paths.dirname(options.filePath), { diff --git a/core/oputil/oputil_common.js b/core/oputil/oputil_common.js index e1d6c962..ae63fa2a 100644 --- a/core/oputil/oputil_common.js +++ b/core/oputil/oputil_common.js @@ -71,7 +71,7 @@ function getConfigPath() { function initConfig(cb) { const configPath = getConfigPath(); - config.init(configPath, { keepWsc : true, noWatch : true }, cb); + config.init(configPath, { keepWsc : true, hotReload : false }, cb); } function initConfigAndDatabases(cb) { @@ -85,7 +85,7 @@ function initConfigAndDatabases(cb) { }, function initArchiveUtil(callback) { // ensure we init ArchiveUtil without events - require('../../core/archive_util').getInstance(true); // true=noWatch + require('../../core/archive_util').getInstance(false); // false=hotReload return callback(null); } ], From 4237f78938d87b55e043331ca7d0624d90d3ac0c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Jun 2020 21:18:51 -0600 Subject: [PATCH 06/43] noWatch -> hotReload & experimental WIP Config class using ConfigLoader --- core/config.js | 110 ++++++------------------------------------------- 1 file changed, 13 insertions(+), 97 deletions(-) diff --git a/core/config.js b/core/config.js index f6e7eee3..1910fef0 100644 --- a/core/config.js +++ b/core/config.js @@ -4,6 +4,7 @@ // ENiGMA½ const Errors = require('./enig_error.js').Errors; const DefaultConfig = require('./config_default'); +const ConfigLoader = require('./config_loader'); // deps const paths = require('path'); @@ -11,108 +12,21 @@ const async = require('async'); const assert = require('assert'); const _ = require('lodash'); -const reduceDeep = require('deepdash/getReduceDeep')(_); exports.init = init; exports.getDefaultPath = getDefaultPath; -class Configuration { - constructor(path, options) { - this.current = {}; +class Config extends ConfigLoader { + constructor(options) { + super(options); } - static create(path, options, cb) { + static create(basePath, options, cb) { + options.mergeCustomizer = (objValue, srcValue, key, object, source, stack) => { + console.log(key); + }; - } - - get() { - return this.current; - } - - _convertTo(value, type) { - switch (type) { - case 'bool' : - case 'boolean' : - value = 'true' === value.toLowerCase(); - break; - - case 'number' : - { - const num = parseInt(value); - if (!isNaN(num)) { - value = num; - } - } - break; - - case 'object' : - try { - value = JSON.parse(value); - } catch(e) { } - break; - - case 'date' : - case 'time' : - case 'datetime' : - case 'timestamp' : - { - const m = moment(value); - if (m.isValid()) { - value = m; - } - } - break; - - case 'regex' : - // :TODO: What flags to use, etc.? - break; - } - - return value; - } - - _resolveEnvironmentVariable(spec) { - const [prefix, varName, type, array] = spec.split(':'); - if (!varName) { - return; - } - - let value = process.env[varName]; - if (!value) { - return; - } - - if ('array' === array) { - value = value.split(',').map(v => this._convertTo(v, type)); - } else { - value = this._convertTo(value, type); - } - - return value; - } - - _resolveCurrent() { - reduceDeep( - this.current, - (acc, value, key, parent, ctx) => { - // resolve self references; there may be a better way... - if (_.isString(value) && '@' === value.charAt(0)) { - if (value.startsWith('@reference:')) { - value = value.slice(11); - const ref = _.get(acc, value); - if (ref) { - _.set(acc, ctx.path, ref); - } - } else if (value.startsWith('@environment:')) { - value = this._resolveEnvironmentVariable(value); - if (!_.isUndefined(value)) { - _.set(acc, ctx.path, value); - } - } - } - return acc; - } - ); + return ConfigLoader.create(basePath, options, cb); } }; @@ -246,9 +160,9 @@ function init(configPath, options, cb) { const ConfigCache = require('./config_cache.js'); const getConfigOptions = { filePath : configPath, - noWatch : options.noWatch, + hotReload : options.hotReload, }; - if(!options.noWatch) { + if(options.hotReload) { getConfigOptions.callback = changed; } ConfigCache.getConfigWithOptions(getConfigOptions, (err, config) => { @@ -264,3 +178,5 @@ function getDefaultPath() { // e.g. /enigma-bbs-install-path/config/ return './config/'; } + +exports.Config = Config; \ No newline at end of file From 56472c80a8f76632af0a363adec825238495411a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 10 Jun 2020 19:15:49 -0600 Subject: [PATCH 07/43] Decent merge customizer --- core/bbs.js | 3 + core/config.js | 24 ++++- core/config_loader.js | 235 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 core/config_loader.js diff --git a/core/bbs.js b/core/bbs.js index a0256d3e..aff1bdb3 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -73,6 +73,9 @@ function main() { }, function initConfig(configPath, configPathSupplied, callback) { const configFile = configPath + 'config.hjson'; + conf.Config.create(configFile, {}, err => { + console.log(err); + }); conf.init(resolvePath(configFile), function configInit(err) { // diff --git a/core/config.js b/core/config.js index 1910fef0..d352a568 100644 --- a/core/config.js +++ b/core/config.js @@ -22,8 +22,28 @@ class Config extends ConfigLoader { } static create(basePath, options, cb) { - options.mergeCustomizer = (objValue, srcValue, key, object, source, stack) => { - console.log(key); + const replacePaths = [ + 'loginServers.ssh.algorithms.kex', + 'loginServers.ssh.algorithms.cipher', + 'loginServers.ssh.algorithms.hmac', + 'loginServers.ssh.algorithms.compress', + ]; + const replaceKeys = [ + 'args', 'sendArgs', 'recvArgs', 'recvArgsNonBatch', + ]; + + options.defaultConfig = DefaultConfig(); + + options.defaultsCustomizer = (defaultVal, configVal, key, path) => { + if (Array.isArray(defaultVal) && Array.isArray(configVal)) { + if (replacePaths.includes(path) || replaceKeys.includes(key)) { + // full replacement using user config value + return configVal; + } else { + // merge user config & default config; keep only unique + _.uniq(defaultVal.concat(configVal)); + } + } }; return ConfigLoader.create(basePath, options, cb); diff --git a/core/config_loader.js b/core/config_loader.js new file mode 100644 index 00000000..a4b76649 --- /dev/null +++ b/core/config_loader.js @@ -0,0 +1,235 @@ +// deps +const paths = require('path'); +const async = require('async'); + +const _ = require('lodash'); +const reduceDeep = require('deepdash/getReduceDeep')(_); + +module.exports = class ConfigLoader { + constructor(options) { + this.current = {}; + this.hotReload = _.get(options, 'hotReload', true); + } + + static create(basePath, options, cb) { + const config = new ConfigLoader(options); + config._init( + basePath, + options, + err => { + return cb(err, config); + } + ); + } + + get() { + return this.current; + } + + _convertTo(value, type) { + switch (type) { + case 'bool' : + case 'boolean' : + value = 'true' === value.toLowerCase(); + break; + + case 'number' : + { + const num = parseInt(value); + if (!isNaN(num)) { + value = num; + } + } + break; + + case 'object' : + try { + value = JSON.parse(value); + } catch(e) { } + break; + + case 'date' : + case 'time' : + case 'datetime' : + case 'timestamp' : + { + const m = moment(value); + if (m.isValid()) { + value = m; + } + } + break; + + case 'regex' : + // :TODO: What flags to use, etc.? + break; + } + + return value; + } + + _resolveEnvironmentVariable(spec) { + const [prefix, varName, type, array] = spec.split(':'); + if (!varName) { + return; + } + + let value = process.env[varName]; + if (!value) { + return; + } + + if ('array' === array) { + value = value.split(',').map(v => this._convertTo(v, type)); + } else { + value = this._convertTo(value, type); + } + + return value; + } + + _loadConfigFile(filePath, cb) { + const ConfigCache = require('./config_cache'); + + const options = { + filePath, + hotReload : this.hotReload, + callback : this._configFileChanged.bind(this), + }; + + ConfigCache.getConfigWithOptions(options, (err, config) => { + return cb(err, config); + }); + } + + _configFileChanged({fileName, fileRoot}) { + const reCachedPath = paths.join(fileRoot, fileName); + ConfigCache.getConfig(reCachedPath, (err, config) => { + /* + if(!err) { + mergeValidateAndFinalize(config, err => { + if(!err) { + const Events = require('./events.js'); + Events.emit(Events.getSystemEvents().ConfigChanged); + } + }); + } else { + console.stdout(`Configuration ${reCachedPath} is invalid: ${err.message}`); // eslint-disable-line no-console + } + */ + }); + } + + _init(basePath, options, cb) { + options.defaultConfig = options.defaultConfig || {}; + + // + // 1 - Fetch base configuration from |basePath| + // 2 - Merge with |defaultConfig|, if any + // 3 - Resolve any includes + // 4 - Resolve @reference and @environment + // 5 - Perform any validation + // + async.waterfall( + [ + (callback) => { + return this._loadConfigFile(basePath, callback); + }, + (config, callback) => { + if (_.isFunction(options.defaultsCustomizer)) { + const stack = []; + const mergedConfig = _.mergeWith( + options.defaultConfig, + config, + (defaultVal, configVal, key, target, source) => { + var path; + while (true) { + if (!stack.length) { + stack.push({source, path : []}); + } + + const prev = stack[stack.length - 1]; + if (source === prev.source) { + path = prev.path.concat(key); + stack.push({source : configVal, path}); + break; + } + stack.pop(); + } + path = path.join('.'); + return options.defaultsCustomizer(defaultVal, configVal, key, path); + } + ); + return callback(null, mergedConfig); + } else { + // :TODO: correct? + _.defaultsDeep(config, options.defaultConfig); + return callback(null, config); + } + }, + (config, callback) => { + const configRoot = paths.dirname(basePath); + return this._resolveIncludes(configRoot, config, callback); + }, + (config, callback) => { + config = this._resolveAtSpecs(config); + return callback(null, config); + }, + ], + (err, config) => { + if (!err) { + this.current = config; + } + return cb(err); + } + ); + } + + _resolveIncludes(configRoot, config, cb) { + if (!Array.isArray(config.includes)) { + return cb(null, config); + } + + // If a included file is changed, we need to re-cache, so this + // must be tracked... + const includePaths = config.includes.map(inc => paths.join(configRoot, inc)); + async.eachSeries(includePaths, (includePath, nextIncludePath) => { + this._loadConfigFile(includePath, (err, includedConfig) => { + if (err) { + return nextIncludePath(err); + } + + _.defaultsDeep(config, includedConfig); + return nextIncludePath(null); + }); + }, + err => { + return cb(err, config); + }); + } + + _resolveAtSpecs(config) { + // :TODO: mapValuesDeep may be better here + return reduceDeep( + config, + (acc, value, key, parent, ctx) => { + // resolve self references; there may be a better way... + if (_.isString(value) && '@' === value.charAt(0)) { + if (value.startsWith('@reference:')) { + value = value.slice(11); + const ref = _.get(acc, value); + if (ref) { + _.set(acc, ctx.path, ref); + } + } else if (value.startsWith('@environment:')) { + value = this._resolveEnvironmentVariable(value); + if (!_.isUndefined(value)) { + _.set(acc, ctx.path, value); + } + } + } + return acc; + } + ); + } +}; From 37bc2fc3aa45ba894b462e8f9c6e687f5fb081a4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 10 Jun 2020 22:52:19 -0600 Subject: [PATCH 08/43] Working well --- core/config_loader.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/config_loader.js b/core/config_loader.js index a4b76649..50549c85 100644 --- a/core/config_loader.js +++ b/core/config_loader.js @@ -149,23 +149,26 @@ module.exports = class ConfigLoader { } const prev = stack[stack.length - 1]; + if (source === prev.source) { path = prev.path.concat(key); stack.push({source : configVal, path}); break; } + stack.pop(); } + path = path.join('.'); return options.defaultsCustomizer(defaultVal, configVal, key, path); } ); + return callback(null, mergedConfig); - } else { - // :TODO: correct? - _.defaultsDeep(config, options.defaultConfig); - return callback(null, config); } + + // :TODO: correct? + return callback(null, _.merge(options.defaultConfig, config)); }, (config, callback) => { const configRoot = paths.dirname(basePath); From 93947300113294da211e612b21579eed52dd9a6f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 11 Jun 2020 21:16:15 -0600 Subject: [PATCH 09/43] New Config class working quite well for system config. Menu.hjson/etc. to come --- core/bbs.js | 7 +- core/config.js | 203 +++++++----------------------------------- core/config_cache.js | 2 +- core/config_loader.js | 182 +++++++++++++++++++------------------ 4 files changed, 128 insertions(+), 266 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index aff1bdb3..0bf8f8aa 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -69,15 +69,12 @@ function main() { const configOverridePath = argv.config; - return callback(null, configOverridePath || conf.getDefaultPath(), _.isString(configOverridePath)); + return callback(null, configOverridePath || conf.Config.getDefaultPath(), _.isString(configOverridePath)); }, function initConfig(configPath, configPathSupplied, callback) { const configFile = configPath + 'config.hjson'; - conf.Config.create(configFile, {}, err => { - console.log(err); - }); - conf.init(resolvePath(configFile), function configInit(err) { + conf.Config.create(resolvePath(configFile), err => { // // If the user supplied a path and we can't read/parse it // then it's a fatal error diff --git a/core/config.js b/core/config.js index d352a568..927d7bbf 100644 --- a/core/config.js +++ b/core/config.js @@ -1,202 +1,61 @@ -/* jslint node: true */ -'use strict'; - // ENiGMA½ -const Errors = require('./enig_error.js').Errors; const DefaultConfig = require('./config_default'); const ConfigLoader = require('./config_loader'); -// deps -const paths = require('path'); -const async = require('async'); -const assert = require('assert'); - const _ = require('lodash'); -exports.init = init; -exports.getDefaultPath = getDefaultPath; +// Global system configuration instance; see Config.create() +let systemConfigInstance; -class Config extends ConfigLoader { +exports.Config = class Config extends ConfigLoader { constructor(options) { super(options); } - static create(basePath, options, cb) { + static create(baseConfigPath, cb) { const replacePaths = [ 'loginServers.ssh.algorithms.kex', 'loginServers.ssh.algorithms.cipher', 'loginServers.ssh.algorithms.hmac', 'loginServers.ssh.algorithms.compress', ]; + const replaceKeys = [ 'args', 'sendArgs', 'recvArgs', 'recvArgsNonBatch', ]; - options.defaultConfig = DefaultConfig(); - - options.defaultsCustomizer = (defaultVal, configVal, key, path) => { - if (Array.isArray(defaultVal) && Array.isArray(configVal)) { - if (replacePaths.includes(path) || replaceKeys.includes(key)) { - // full replacement using user config value - return configVal; - } else { - // merge user config & default config; keep only unique - _.uniq(defaultVal.concat(configVal)); + const options = { + defaultConfig : DefaultConfig, + defaultsCustomizer : (defaultVal, configVal, key, path) => { + if (Array.isArray(defaultVal) && Array.isArray(configVal)) { + if (replacePaths.includes(path) || replaceKeys.includes(key)) { + // full replacement using user config value + return configVal; + } else { + // merge user config & default config; keep only unique + _.uniq(defaultVal.concat(configVal)); + } } - } + }, }; - return ConfigLoader.create(basePath, options, cb); - } -}; - -let currentConfiguration = {}; - -function hasMessageConferenceAndArea(config) { - assert(_.isObject(config.messageConferences)); // we create one ourself! - - const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => { - return 'system_internal' !== confTag; - }); - - if(0 === nonInternalConfs.length) { - return false; - } - - // :TODO: there is likely a better/cleaner way of doing this - - let result = false; - _.forEach(nonInternalConfs, confTag => { - if(_.has(config.messageConferences[confTag], 'areas') && - Object.keys(config.messageConferences[confTag].areas) > 0) - { - result = true; - return false; // stop iteration - } - }); - - return result; -} - -const ArrayReplaceKeyPaths = [ - 'loginServers.ssh.algorithms.kex', - 'loginServers.ssh.algorithms.cipher', - 'loginServers.ssh.algorithms.hmac', - 'loginServers.ssh.algorithms.compress', -]; - -const ArrayReplaceKeys = [ - 'args', - 'sendArgs', 'recvArgs', 'recvArgsNonBatch', -]; - -function mergeValidateAndFinalize(config, cb) { - const defaultConfig = DefaultConfig(); - - const arrayReplaceKeyPathsMutable = _.clone(ArrayReplaceKeyPaths); - const shouldReplaceArray = (arr, key) => { - if(ArrayReplaceKeys.includes(key)) { - return true; - } - for(let i = 0; i < arrayReplaceKeyPathsMutable.length; ++i) { - const o = _.get(defaultConfig, arrayReplaceKeyPathsMutable[i]); - if(_.isEqual(o, arr)) { - arrayReplaceKeyPathsMutable.splice(i, 1); - return true; - } - } - return false; - }; - - async.waterfall( - [ - function mergeWithDefaultConfig(callback) { - const mergedConfig = _.mergeWith( - defaultConfig, - config, - (defConfig, userConfig, key) => { - if(Array.isArray(defConfig) && Array.isArray(userConfig)) { - // - // Arrays are special: Some we merge, while others - // we simply replace. - // - if(shouldReplaceArray(defConfig, key)) { - return userConfig; - } else { - return _.uniq(defConfig.concat(userConfig)); - } - } - } - ); - - return callback(null, mergedConfig); - }, - function validate(mergedConfig, callback) { - // - // Various sections must now exist in config - // - // :TODO: Logic is broken here: - if(hasMessageConferenceAndArea(mergedConfig)) { - return callback(Errors.MissingConfig('Please create at least one message conference and area!')); - } - return callback(null, mergedConfig); - }, - function setIt(mergedConfig, callback) { - currentConfiguration = mergedConfig; - exports.get = () => currentConfiguration; - return callback(null); - } - ], - err => { - if(cb) { + systemConfigInstance = new Config(options); + systemConfigInstance.init(baseConfigPath, err => { + if (err) { + console.stdout(`Configuration ${baseConfigPath} error: ${err.message}`); // eslint-disable-line no-console return cb(err); } - } - ); -} -function init(configPath, options, cb) { - if(!cb && _.isFunction(options)) { - cb = options; - options = {}; - } + // late bind an exported get method to the global Config + // instance we just created + exports.get = systemConfigInstance.get.bind(systemConfigInstance); - const changed = ( { fileName, fileRoot } ) => { - const reCachedPath = paths.join(fileRoot, fileName); - ConfigCache.getConfig(reCachedPath, (err, config) => { - if(!err) { - mergeValidateAndFinalize(config, err => { - if(!err) { - const Events = require('./events.js'); - Events.emit(Events.getSystemEvents().ConfigChanged); - } - }); - } else { - console.stdout(`Configuration ${reCachedPath} is invalid: ${err.message}`); // eslint-disable-line no-console - } - }); - }; - - const ConfigCache = require('./config_cache.js'); - const getConfigOptions = { - filePath : configPath, - hotReload : options.hotReload, - }; - if(options.hotReload) { - getConfigOptions.callback = changed; - } - ConfigCache.getConfigWithOptions(getConfigOptions, (err, config) => { - if(err) { return cb(err); - } + }); + } - return mergeValidateAndFinalize(config, cb); - }); -} - -function getDefaultPath() { - // e.g. /enigma-bbs-install-path/config/ - return './config/'; -} - -exports.Config = Config; \ No newline at end of file + static getDefaultPath() { + // e.g. /enigma-bbs-install-path/config/ + return './config/'; + } +}; diff --git a/core/config_cache.js b/core/config_cache.js index 631fb5f1..fc3ba878 100644 --- a/core/config_cache.js +++ b/core/config_cache.js @@ -35,7 +35,7 @@ module.exports = new class ConfigCache this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => { if(!err) { if(options.callback) { - options.callback( { fileName, fileRoot } ); + options.callback( { fileName, fileRoot, configCache : this } ); } } }); diff --git a/core/config_loader.js b/core/config_loader.js index 50549c85..6df22bdb 100644 --- a/core/config_loader.js +++ b/core/config_loader.js @@ -6,26 +6,99 @@ const _ = require('lodash'); const reduceDeep = require('deepdash/getReduceDeep')(_); module.exports = class ConfigLoader { - constructor(options) { + constructor( + { hotReload = true, defaultConfig = {}, defaultsCustomizer = null } = { hotReload : true, defaultConfig : {}, defaultsCustomizer : null } ) + { this.current = {}; - this.hotReload = _.get(options, 'hotReload', true); + + this.hotReload = hotReload; + this.defaultConfig = defaultConfig; + this.defaultsCustomizer = defaultsCustomizer; } - static create(basePath, options, cb) { - const config = new ConfigLoader(options); - config._init( - basePath, - options, - err => { - return cb(err, config); - } - ); + init(baseConfigPath, cb) { + this.baseConfigPath = baseConfigPath; + return this._reload(baseConfigPath, cb); } get() { return this.current; } + _reload(baseConfigPath, cb) { + let defaultConfig; + if (_.isFunction(this.defaultConfig)) { + defaultConfig = this.defaultConfig(); + } else if (_.isObject(this.defaultConfig)) { + defaultConfig = this.defaultConfig; + } else { + defaultConfig = {}; + } + + // + // 1 - Fetch base configuration from |baseConfigPath| + // 2 - Merge with |defaultConfig| + // 3 - Resolve any includes + // 4 - Resolve @reference and @environment + // 5 - Perform any validation + // + async.waterfall( + [ + (callback) => { + return this._loadConfigFile(baseConfigPath, callback); + }, + (config, callback) => { + if (_.isFunction(this.defaultsCustomizer)) { + const stack = []; + const mergedConfig = _.mergeWith( + defaultConfig, + config, + (defaultVal, configVal, key, target, source) => { + var path; + while (true) { + if (!stack.length) { + stack.push({source, path : []}); + } + + const prev = stack[stack.length - 1]; + + if (source === prev.source) { + path = prev.path.concat(key); + stack.push({source : configVal, path}); + break; + } + + stack.pop(); + } + + path = path.join('.'); + return this.defaultsCustomizer(defaultVal, configVal, key, path); + } + ); + + return callback(null, mergedConfig); + } + + return callback(null, _.merge(defaultConfig, config)); + }, + (config, callback) => { + const configRoot = paths.dirname(baseConfigPath); + return this._resolveIncludes(configRoot, config, callback); + }, + (config, callback) => { + config = this._resolveAtSpecs(config); + return callback(null, config); + }, + ], + (err, config) => { + if (!err) { + this.current = config; + } + return cb(err); + } + ); + } + _convertTo(value, type) { switch (type) { case 'bool' : @@ -102,92 +175,24 @@ module.exports = class ConfigLoader { }); } - _configFileChanged({fileName, fileRoot}) { + _configFileChanged({fileName, fileRoot, configCache}) { const reCachedPath = paths.join(fileRoot, fileName); - ConfigCache.getConfig(reCachedPath, (err, config) => { - /* - if(!err) { - mergeValidateAndFinalize(config, err => { - if(!err) { + configCache.getConfig(reCachedPath, (err, config) => { + if (err) { + return console.stdout(`Configuration ${reCachedPath} is invalid: ${err.message}`); // eslint-disable-line no-console + } + + if (this.configPaths.includes(reCachedPath)) { + this._reload(this.baseConfigPath, err => { + if (!err) { const Events = require('./events.js'); Events.emit(Events.getSystemEvents().ConfigChanged); } }); - } else { - console.stdout(`Configuration ${reCachedPath} is invalid: ${err.message}`); // eslint-disable-line no-console } - */ }); } - _init(basePath, options, cb) { - options.defaultConfig = options.defaultConfig || {}; - - // - // 1 - Fetch base configuration from |basePath| - // 2 - Merge with |defaultConfig|, if any - // 3 - Resolve any includes - // 4 - Resolve @reference and @environment - // 5 - Perform any validation - // - async.waterfall( - [ - (callback) => { - return this._loadConfigFile(basePath, callback); - }, - (config, callback) => { - if (_.isFunction(options.defaultsCustomizer)) { - const stack = []; - const mergedConfig = _.mergeWith( - options.defaultConfig, - config, - (defaultVal, configVal, key, target, source) => { - var path; - while (true) { - if (!stack.length) { - stack.push({source, path : []}); - } - - const prev = stack[stack.length - 1]; - - if (source === prev.source) { - path = prev.path.concat(key); - stack.push({source : configVal, path}); - break; - } - - stack.pop(); - } - - path = path.join('.'); - return options.defaultsCustomizer(defaultVal, configVal, key, path); - } - ); - - return callback(null, mergedConfig); - } - - // :TODO: correct? - return callback(null, _.merge(options.defaultConfig, config)); - }, - (config, callback) => { - const configRoot = paths.dirname(basePath); - return this._resolveIncludes(configRoot, config, callback); - }, - (config, callback) => { - config = this._resolveAtSpecs(config); - return callback(null, config); - }, - ], - (err, config) => { - if (!err) { - this.current = config; - } - return cb(err); - } - ); - } - _resolveIncludes(configRoot, config, cb) { if (!Array.isArray(config.includes)) { return cb(null, config); @@ -207,6 +212,7 @@ module.exports = class ConfigLoader { }); }, err => { + this.configPaths = [ this.baseConfigPath, ...includePaths ]; return cb(err, config); }); } From 43a7bd9ba40efb54d796784644b96bfc4e681dad Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 11 Jun 2020 21:38:33 -0600 Subject: [PATCH 10/43] Use mapValuesDeep vs reduceDeep --- core/config_loader.js | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/core/config_loader.js b/core/config_loader.js index 6df22bdb..a5eee3e0 100644 --- a/core/config_loader.js +++ b/core/config_loader.js @@ -3,7 +3,7 @@ const paths = require('path'); const async = require('async'); const _ = require('lodash'); -const reduceDeep = require('deepdash/getReduceDeep')(_); +const mapValuesDeep = require('deepdash/getMapValuesDeep')(_); module.exports = class ConfigLoader { constructor( @@ -218,26 +218,19 @@ module.exports = class ConfigLoader { } _resolveAtSpecs(config) { - // :TODO: mapValuesDeep may be better here - return reduceDeep( + return mapValuesDeep( config, - (acc, value, key, parent, ctx) => { - // resolve self references; there may be a better way... + value => { if (_.isString(value) && '@' === value.charAt(0)) { if (value.startsWith('@reference:')) { - value = value.slice(11); - const ref = _.get(acc, value); - if (ref) { - _.set(acc, ctx.path, ref); - } + const refPath = value.slice(11); + value = _.get(config, refPath, value); } else if (value.startsWith('@environment:')) { - value = this._resolveEnvironmentVariable(value); - if (!_.isUndefined(value)) { - _.set(acc, ctx.path, value); - } + value = this._resolveEnvironmentVariable(value) || value; } } - return acc; + + return value; } ); } From 920684852c45818b37e20ca10da150d87851824b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 11 Jun 2020 22:40:05 -0600 Subject: [PATCH 11/43] eslint to dev deps --- package.json | 8 +- yarn.lock | 532 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 533 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 509fde49..038cce91 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "binary-parser": "^1.6.2", "buffers": "github:NuSkooler/node-buffers", "bunyan": "^1.8.12", + "deepdash": "^5.1.1", "exiftool": "^0.0.3", "fs-extra": "9.0.1", "glob": "7.1.6", @@ -58,10 +59,11 @@ "uuid-parse": "1.1.0", "ws": "^7.3.0", "xxhash": "^0.3.0", - "yazl": "^2.5.1", - "deepdash" : "^5.1.1" + "yazl": "^2.5.1" + }, + "devDependencies": { + "eslint": "^7.2.0" }, - "devDependencies": {}, "engines": { "node": ">=12" } diff --git a/yarn.lock b/yarn.lock index bd556bfc..85cd031a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,27 @@ # yarn lockfile v1 +"@babel/code-frame@^7.0.0": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.1.tgz#d5481c5095daa1c57e16e54c6f9198443afb49ff" + integrity sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw== + dependencies: + "@babel/highlight" "^7.10.1" + +"@babel/helper-validator-identifier@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz#5770b0c1a826c4f53f5ede5e153163e0318e94b5" + integrity sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw== + +"@babel/highlight@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.1.tgz#841d098ba613ba1a427a2b383d79e35552c38ae0" + integrity sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg== + dependencies: + "@babel/helper-validator-identifier" "^7.10.1" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -20,6 +41,26 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +acorn-jsx@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" + integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== + +acorn@^7.2.0: + version "7.3.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd" + integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA== + +ajv@^6.10.0, ajv@^6.10.2: + version "6.12.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" + integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ansi-escapes@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.2.1.tgz#4dccdb846c3eee10f6d64dea66273eab90c37228" @@ -47,6 +88,13 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" @@ -76,6 +124,13 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -120,6 +175,11 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + async@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" @@ -230,6 +290,11 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -237,6 +302,15 @@ capture-exit@^2.0.0: dependencies: rsvp "^4.8.4" +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -245,6 +319,14 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -295,6 +377,13 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -302,6 +391,11 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" @@ -343,6 +437,15 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + debug@^2.1.2, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -357,6 +460,13 @@ debug@^4.0.0: dependencies: ms "^2.1.1" +debug@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -372,6 +482,11 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== +deep-is@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + deepdash@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/deepdash/-/deepdash-5.1.1.tgz#dcf68b9e15085b5df18bdb4011723790e0c40430" @@ -434,6 +549,13 @@ detect-libc@^1.0.2: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + dtrace-provider@~0.8: version "0.8.7" resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.7.tgz#dc939b4d3e0620cfe0c1cd803d0d2d7ed04ffd04" @@ -441,6 +563,11 @@ dtrace-provider@~0.8: dependencies: nan "^2.10.0" +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -458,6 +585,111 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +eslint-scope@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" + integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd" + integrity sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz#74415ac884874495f78ec2a97349525344c981fa" + integrity sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ== + +eslint@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.2.0.tgz#d41b2e47804b30dbabb093a967fb283d560082e6" + integrity sha512-B3BtEyaDKC5MlfDa2Ha8/D6DsS4fju95zs0hjS3HdGazw+LNayai38A25qMppK37wWGWNYSPOR6oYzlz5MHsRQ== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^5.1.0" + eslint-utils "^2.0.0" + eslint-visitor-keys "^1.2.0" + espree "^7.1.0" + esquery "^1.2.0" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + inquirer "^7.0.0" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash "^4.17.14" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.1.0.tgz#a9c7f18a752056735bf1ba14cb1b70adc3a5ce1c" + integrity sha512-dcorZSyfmm4WTuTnE5Y7MEN1DyoPYy1ZR783QW1FJoenn7RailyWFsq/UL6ZAAA7uXurN9FIpYyUs3OfiIW+Qw== + dependencies: + acorn "^7.2.0" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.2.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + dependencies: + estraverse "^4.1.0" + +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" + integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + exec-sh@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" @@ -532,6 +764,21 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + fb-watchman@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" @@ -546,6 +793,13 @@ figures@^3.0.0: dependencies: escape-string-regexp "^1.0.5" +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -556,6 +810,20 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -598,6 +866,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -624,7 +897,14 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= -glob@7.1.6: +glob-parent@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + +glob@7.1.6, glob@^7.1.3: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -659,6 +939,13 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" +globals@^12.1.0: + version "12.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" + integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== + dependencies: + type-fest "^0.8.1" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -685,6 +972,11 @@ graceful-fs@^4.2.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" @@ -757,6 +1049,24 @@ ignore-walk@^3.0.1: dependencies: minimatch "^3.0.4" +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +import-fresh@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -784,7 +1094,7 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -inquirer@^7.1.0: +inquirer@^7.0.0, inquirer@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29" integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg== @@ -866,6 +1176,11 @@ is-extendable@^1.0.1: dependencies: is-plain-object "^2.0.4" +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + is-fullwidth-code-point@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" @@ -883,6 +1198,13 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -948,6 +1270,29 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + jsonfile@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" @@ -986,12 +1331,20 @@ later@1.2.0: resolved "https://registry.yarnpkg.com/later/-/later-1.2.0.tgz#f2cf6c4dd7956dd2f520adf0329836e9876bad0f" integrity sha1-8s9sTdeVbdL1IK3wMpg26YdrrQ8= +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + lodash-es@^4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== -lodash@^4.17.15: +lodash@^4.17.14, lodash@^4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -1171,6 +1524,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + ncp@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" @@ -1331,6 +1689,18 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -1366,6 +1736,13 @@ p-map@^1.1.1: resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -1386,6 +1763,11 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -1413,11 +1795,21 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -1426,6 +1818,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + qrcode-generator@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.4.tgz#63f771224854759329a99048806a53ed278740e7" @@ -1471,6 +1868,11 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regexpp@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" + integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -1486,6 +1888,11 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -1504,6 +1911,13 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +rimraf@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + rimraf@^2.2.8, rimraf@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" @@ -1599,6 +2013,11 @@ semver@^5.5.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== +semver@^7.2.1: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + serialize-error@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" @@ -1636,16 +2055,37 @@ shebang-command@^1.2.0: dependencies: shebang-regex "^1.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -1711,6 +2151,11 @@ split2@^3.0.0: dependencies: readable-stream "^3.0.0" +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + sqlite3-trans@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/sqlite3-trans/-/sqlite3-trans-1.2.2.tgz#faf268cc8d04dfd1a4854d64a70a229bdb50609f" @@ -1772,6 +2217,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + string-width@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff" @@ -1809,7 +2263,7 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.2.0: +strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== @@ -1828,11 +2282,23 @@ strip-eof@^1.0.0: resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= +strip-json-comments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" + integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" @@ -1840,6 +2306,16 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + tar@^4: version "4.4.6" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b" @@ -1868,6 +2344,11 @@ temptmp@^1.1.0: dependencies: del "^3.0.0" +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + thirty-two@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a" @@ -1940,11 +2421,23 @@ tweetnacl@^0.14.3: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + type-fest@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw== +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + union-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" @@ -1968,6 +2461,13 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" @@ -1998,6 +2498,11 @@ uuid@8.1.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg== +v8-compile-cache@^2.0.3: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" + integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== + walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" @@ -2012,6 +2517,13 @@ which@^1.2.9: dependencies: isexe "^2.0.0" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" @@ -2019,11 +2531,23 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + ws@^7.3.0: version "7.3.0" resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" From fb9d83ab6f24d75817256c540e06bf1fb61391e1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 11 Jun 2020 22:40:20 -0600 Subject: [PATCH 12/43] Tidy up config, fix a couple minor bugs --- core/config_loader.js | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/core/config_loader.js b/core/config_loader.js index a5eee3e0..c922cddd 100644 --- a/core/config_loader.js +++ b/core/config_loader.js @@ -1,6 +1,7 @@ // deps const paths = require('path'); const async = require('async'); +const moment = require('moment'); const _ = require('lodash'); const mapValuesDeep = require('deepdash/getMapValuesDeep')(_); @@ -55,7 +56,7 @@ module.exports = class ConfigLoader { config, (defaultVal, configVal, key, target, source) => { var path; - while (true) { + while (true) { // eslint-disable-line no-constant-condition if (!stack.length) { stack.push({source, path : []}); } @@ -118,7 +119,9 @@ module.exports = class ConfigLoader { case 'object' : try { value = JSON.parse(value); - } catch(e) { } + } catch(e) { + // ignored + } break; case 'date' : @@ -142,7 +145,7 @@ module.exports = class ConfigLoader { } _resolveEnvironmentVariable(spec) { - const [prefix, varName, type, array] = spec.split(':'); + const [, varName, type, array] = spec.split(':'); if (!varName) { return; } @@ -175,22 +178,16 @@ module.exports = class ConfigLoader { }); } - _configFileChanged({fileName, fileRoot, configCache}) { + _configFileChanged({fileName, fileRoot}) { const reCachedPath = paths.join(fileRoot, fileName); - configCache.getConfig(reCachedPath, (err, config) => { - if (err) { - return console.stdout(`Configuration ${reCachedPath} is invalid: ${err.message}`); // eslint-disable-line no-console - } - - if (this.configPaths.includes(reCachedPath)) { - this._reload(this.baseConfigPath, err => { - if (!err) { - const Events = require('./events.js'); - Events.emit(Events.getSystemEvents().ConfigChanged); - } - }); - } - }); + if (this.configPaths.includes(reCachedPath)) { + this._reload(this.baseConfigPath, err => { + if (!err) { + const Events = require('./events.js'); + Events.emit(Events.getSystemEvents().ConfigChanged); + } + }); + } } _resolveIncludes(configRoot, config, cb) { From 8808447343e9737285b1e1ae74f87feb51c29e46 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jun 2020 21:21:26 -0600 Subject: [PATCH 13/43] Fix hard coded config.hjson event in ConfigLoader * ConfigLoader init can now take onReload handler * Use onReload for config.hjson/system config event --- core/config.js | 6 ++++++ core/config_loader.js | 24 ++++++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/core/config.js b/core/config.js index 927d7bbf..791845b8 100644 --- a/core/config.js +++ b/core/config.js @@ -37,6 +37,12 @@ exports.Config = class Config extends ConfigLoader { } } }, + onReload : err => { + if (!err) { + const Events = require('./events.js'); + Events.emit(Events.getSystemEvents().ConfigChanged); + } + }, }; systemConfigInstance = new Config(options); diff --git a/core/config_loader.js b/core/config_loader.js index c922cddd..f8f3fe71 100644 --- a/core/config_loader.js +++ b/core/config_loader.js @@ -8,13 +8,26 @@ const mapValuesDeep = require('deepdash/getMapValuesDeep')(_); module.exports = class ConfigLoader { constructor( - { hotReload = true, defaultConfig = {}, defaultsCustomizer = null } = { hotReload : true, defaultConfig : {}, defaultsCustomizer : null } ) + { + hotReload = true, + defaultConfig = {}, + defaultsCustomizer = null, + onReload = null, + } = + { + hotReload : true, + defaultConfig : {}, + defaultsCustomizer : null, + onReload : null, + } + ) { this.current = {}; this.hotReload = hotReload; this.defaultConfig = defaultConfig; this.defaultsCustomizer = defaultsCustomizer; + this.onReload = onReload; } init(baseConfigPath, cb) { @@ -135,10 +148,6 @@ module.exports = class ConfigLoader { } } break; - - case 'regex' : - // :TODO: What flags to use, etc.? - break; } return value; @@ -182,9 +191,8 @@ module.exports = class ConfigLoader { const reCachedPath = paths.join(fileRoot, fileName); if (this.configPaths.includes(reCachedPath)) { this._reload(this.baseConfigPath, err => { - if (!err) { - const Events = require('./events.js'); - Events.emit(Events.getSystemEvents().ConfigChanged); + if (_.isFunction(this.onReload)) { + this.onReload(err, reCachedPath); } }); } From 683075f4ca014401ec025aabc19591d5b1d44dd2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jun 2020 21:22:38 -0600 Subject: [PATCH 14/43] Use new ConfigLoader for achievements --- core/achievement.js | 76 +++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/core/achievement.js b/core/achievement.js index d40acc18..87f71914 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -4,10 +4,8 @@ // ENiGMA½ const Events = require('./events.js'); const Config = require('./config.js').get; -const { - getConfigPath, - getFullConfig, -} = require('./config_util.js'); +const ConfigLoader = require('./config_loader'); +const { getConfigPath } = require('./config_util'); const UserDb = require('./database.js').dbs.user; const { getISOTimestampString @@ -29,13 +27,11 @@ const { const stringFormat = require('./string_format.js'); const StatLog = require('./stat_log.js'); const Log = require('./logger.js').log; -const ConfigCache = require('./config_cache.js'); // deps const _ = require('lodash'); const async = require('async'); const moment = require('moment'); -const paths = require('path'); exports.getAchievementsEarnedByUser = getAchievementsEarnedByUser; @@ -136,63 +132,61 @@ class UserStatAchievement extends Achievement { class Achievements { constructor(events) { this.events = events; + this.enabled = false; } getAchievementByTag(tag) { - return this.achievementConfig.achievements[tag]; + return this.config.get().achievements[tag]; } isEnabled() { - return !_.isUndefined(this.achievementConfig); + return this.enabled; } init(cb) { - let achievementConfigPath = _.get(Config(), 'general.achievementFile'); - if(!achievementConfigPath) { + const configPath = this._getConfigPath(); + if (!configPath) { Log.info('Achievements are not configured'); return cb(null); } - achievementConfigPath = getConfigPath(achievementConfigPath); // qualify - const configLoaded = (achievementConfig) => { - if(true !== achievementConfig.enabled) { + const configLoaded = () => { + if(true !== this.config.get().enabled) { Log.info('Achievements are not enabled'); + this.enabled = false; this.stopMonitoringUserStatEvents(); - delete this.achievementConfig; } else { Log.info('Achievements are enabled'); - this.achievementConfig = achievementConfig; + this.enabled = true; this.monitorUserStatEvents(); } }; - const changed = ( { fileName, fileRoot } ) => { - const reCachedPath = paths.join(fileRoot, fileName); - if(reCachedPath === achievementConfigPath) { - getFullConfig(achievementConfigPath, (err, achievementConfig) => { - if(err) { - return Log.error( { error : err.message }, 'Failed to reload achievement config from cache'); - } - configLoaded(achievementConfig); - }); - } + const configOptions = { + onReload : err => { + if (!err) { + configLoaded(); + } + }, }; - ConfigCache.getConfigWithOptions( - { - filePath : achievementConfigPath, - forceReCache : true, - callback : changed, - }, - (err, achievementConfig) => { - if(err) { - return cb(err); - } - - configLoaded(achievementConfig); - return cb(null); + this.config = new ConfigLoader(configOptions); + this.config.init(configPath, err => { + if (err) { + return cb(err); } - ); + + configLoaded(); + return cb(null); + }); + } + + _getConfigPath() { + const path = _.get(Config(), 'general.achievementFile'); + if(!path) { + return; + } + return getConfigPath(path); // qualify } loadAchievementHitCount(user, achievementTag, field, cb) { @@ -298,7 +292,7 @@ class Achievements { // :TODO: Make this code generic - find + return factory created object const achievementTags = Object.keys(_.pickBy( - _.get(this.achievementConfig, 'achievements', {}), + _.get(this.config.get(), 'achievements', {}), achievement => { if(false === achievement.enabled) { return false; @@ -498,7 +492,7 @@ class Achievements { const spec = _.get(info.details, `art.${name}`) || _.get(info.achievement, `art.${name}`) || - _.get(this.achievementConfig, `art.${name}`); + _.get(this.config.get(), `art.${name}`); if(!spec) { return callback(null); } From 1a96ad41d2dc58dd3465821cd11eae959702e42e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jun 2020 21:25:05 -0600 Subject: [PATCH 15/43] Fix ConfigLoader configPaths if no includes --- core/config_loader.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/config_loader.js b/core/config_loader.js index f8f3fe71..3844df1a 100644 --- a/core/config_loader.js +++ b/core/config_loader.js @@ -200,6 +200,7 @@ module.exports = class ConfigLoader { _resolveIncludes(configRoot, config, cb) { if (!Array.isArray(config.includes)) { + this.configPaths = [ this.baseConfigPath ]; return cb(null, config); } From 4d4be5d6a9ef205090c254632c6e538faa42a76e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 17 Jun 2020 00:10:51 -0600 Subject: [PATCH 16/43] Major progress on revamp * Deprecated explicit prompt.hjson/general.promptFile, etc.: menu.hjson can simply include any number of files * All menus and themes, their events, etc. are managed by ThemeManager allowing includes, refs, etc. and much cleaner code --- WHATSNEW.md | 2 + core/achievement.js | 7 +- core/bbs.js | 12 +- core/client.js | 24 +- core/config_default.js | 1 - core/config_util.js | 56 +-- core/menu_util.js | 5 +- core/theme.js | 593 ++++++++++++-------------- docs/_includes/nav.md | 1 - docs/art/themes.md | 4 +- docs/configuration/colour-codes.md | 8 +- docs/configuration/creating-config.md | 2 +- docs/configuration/hjson.md | 3 +- docs/configuration/menu-hjson.md | 5 +- docs/configuration/prompt-hjson.md | 8 - 15 files changed, 331 insertions(+), 400 deletions(-) delete mode 100644 docs/configuration/prompt-hjson.md diff --git a/WHATSNEW.md b/WHATSNEW.md index 35c26086..21d43083 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -4,6 +4,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For ## 0.0.12-beta * The `master` branch has become mainline. What this means to users is `git pull` will always give you the latest and greatest. Make sure to read [Updating](/docs/admin/updating.md) and keep an eye on `WHATSNEW.md` (this file) and [UPGRADE](UPGRADE.md)! See also [ticket #276](https://github.com/NuSkooler/enigma-bbs/issues/276). * The default configuration has been moved to [config_default.js](/core/config_default.js). +* A full configuration revamp has taken place. Configuration files such as `config.hjson`, `menu.hjson`, and `theme.hjson` can now utilize includes via the `include` directive, reference 'self' sections using `@reference:` and import environment variables with `@environment`. +* An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `include`ed in `menu.hjson`. ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/core/achievement.js b/core/achievement.js index 87f71914..3c58d4d3 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -162,15 +162,14 @@ class Achievements { } }; - const configOptions = { + this.config = new ConfigLoader({ onReload : err => { if (!err) { configLoaded(); } - }, - }; + } + }); - this.config = new ConfigLoader(configOptions); this.config.init(configPath, err => { if (err) { return cb(err); diff --git a/core/bbs.js b/core/bbs.js index 0bf8f8aa..074a742b 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -212,15 +212,9 @@ function initialize(cb) { function initStatLog(callback) { return require('./stat_log.js').init(callback); }, - function initConfigs(callback) { - return require('./config_util.js').init(callback); - }, - function initThemes(callback) { - // Have to pull in here so it's after Config init - require('./theme.js').initAvailableThemes( (err, themeCount) => { - logger.log.info({ themeCount }, 'Themes initialized'); - return callback(err); - }); + function initMenusAndThemes(callback) { + const { ThemeManager } = require('./theme'); + return ThemeManager.create(callback); }, function loadSysOpInformation(callback) { // diff --git a/core/client.js b/core/client.js index b8b08c34..8d17f953 100644 --- a/core/client.js +++ b/core/client.js @@ -83,7 +83,7 @@ function Client(/*input, output*/) { const self = this; this.user = new User(); - this.currentTheme = { info : { name : 'N/A', description : 'None' } }; + this.currentThemeConfig = { info : { name : 'N/A', description : 'None' } }; this.lastActivityTime = Date.now(); this.menuStack = new MenuStack(this); this.acs = new ACS( { client : this, user : this.user } ); @@ -94,6 +94,26 @@ function Client(/*input, output*/) { this.mciCache = {}; }; + Object.defineProperty(this, 'currentTheme', { + get : () => { + if (this.currentThemeConfig) { + return this.currentThemeConfig.get(); + } else { + return { + info : { + name : 'N/A', + author : 'N/A', + description : 'N/A', + group : 'N/A', + } + }; + } + }, + set : (theme) => { + this.currentThemeConfig = theme; + } + }); + Object.defineProperty(this, 'node', { get : function() { return self.session.id; @@ -120,7 +140,7 @@ function Client(/*input, output*/) { this.themeChangedListener = function( { themeId } ) { if(_.get(self.currentTheme, 'info.themeId') === themeId) { - self.currentTheme = require('./theme.js').getAvailableThemes().get(themeId); + self.currentThemeConfig = require('./theme.js').getAvailableThemes().get(themeId); } }; diff --git a/core/config_default.js b/core/config_default.js index 063f3899..82d3eb1d 100644 --- a/core/config_default.js +++ b/core/config_default.js @@ -14,7 +14,6 @@ module.exports = () => { closedSystem : false, // is the system closed to new users? menuFile : 'menu.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path - promptFile : 'prompt.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path achievementFile : 'achievements.hjson', }, diff --git a/core/config_util.js b/core/config_util.js index d64c7a24..63e46f62 100644 --- a/core/config_util.js +++ b/core/config_util.js @@ -1,17 +1,12 @@ /* jslint node: true */ 'use strict'; -const Config = require('./config.js').get; -const ConfigCache = require('./config_cache.js'); -const Events = require('./events.js'); +const Config = require('./config.js').get; // deps -const paths = require('path'); -const async = require('async'); +const paths = require('path'); -exports.init = init; -exports.getConfigPath = getConfigPath; -exports.getFullConfig = getFullConfig; +exports.getConfigPath = getConfigPath; function getConfigPath(filePath) { // |filePath| is assumed to be in the config path if it's only a file name @@ -20,48 +15,3 @@ function getConfigPath(filePath) { } return filePath; } - -function init(cb) { - // pre-cache menu.hjson and prompt.hjson + establish events - const changed = ( { fileName, fileRoot } ) => { - const reCachedPath = paths.join(fileRoot, fileName); - if(reCachedPath === getConfigPath(Config().general.menuFile)) { - Events.emit(Events.getSystemEvents().MenusChanged); - } else if(reCachedPath === getConfigPath(Config().general.promptFile)) { - Events.emit(Events.getSystemEvents().PromptsChanged); - } - }; - - const config = Config(); - async.series( - [ - function menu(callback) { - return ConfigCache.getConfigWithOptions( - { - filePath : getConfigPath(config.general.menuFile), - callback : changed, - }, - callback - ); - }, - function prompt(callback) { - return ConfigCache.getConfigWithOptions( - { - filePath : getConfigPath(config.general.promptFile), - callback : changed, - }, - callback - ); - } - ], - err => { - return cb(err); - } - ); -} - -function getFullConfig(filePath, cb) { - ConfigCache.getConfig(getConfigPath(filePath), (err, config) => { - return cb(err, config); - }); -} diff --git a/core/menu_util.js b/core/menu_util.js index a429415b..923d907d 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -24,10 +24,11 @@ function getMenuConfig(client, name, cb) { async.waterfall( [ function locateMenuConfig(callback) { - if(_.has(client.currentTheme, [ 'menus', name ])) { - const menuConfig = client.currentTheme.menus[name]; + const menuConfig = _.get(client.currentTheme, [ 'menus', name ]); + if (menuConfig) { return callback(null, menuConfig); } + return callback(Errors.DoesNotExist(`No menu entry for "${name}"`)); }, function locatePromptConfig(menuConfig, callback) { diff --git a/core/theme.js b/core/theme.js index c477f92e..557377b7 100644 --- a/core/theme.js +++ b/core/theme.js @@ -5,16 +5,16 @@ const Config = require('./config.js').get; const art = require('./art.js'); const ansi = require('./ansi_term.js'); const Log = require('./logger.js').log; -const ConfigCache = require('./config_cache.js'); -const getFullConfig = require('./config_util.js').getFullConfig; const asset = require('./asset.js'); const ViewController = require('./view_controller.js').ViewController; const Errors = require('./enig_error.js').Errors; -const ErrorReasons = require('./enig_error.js').ErrorReasons; const Events = require('./events.js'); const AnsiPrep = require('./ansi_prep.js'); const UserProps = require('./user_property.js'); +const ConfigLoader = require('./config_loader'); +const { getConfigPath } = require('./config_util'); + // deps const fs = require('graceful-fs'); const paths = require('path'); @@ -26,213 +26,262 @@ exports.getThemeArt = getThemeArt; exports.getAvailableThemes = getAvailableThemes; exports.getRandomTheme = getRandomTheme; exports.setClientTheme = setClientTheme; -exports.initAvailableThemes = initAvailableThemes; exports.displayPreparedArt = displayPreparedArt; exports.displayThemeArt = displayThemeArt; exports.displayThemedPause = displayThemedPause; exports.displayThemedPrompt = displayThemedPrompt; exports.displayThemedAsset = displayThemedAsset; -function refreshThemeHelpers(theme) { - // - // Create some handy helpers - // - theme.helpers = { - getPasswordChar : function() { - let pwChar = _.get( - theme, - 'customization.defaults.passwordChar', - Config().theme.passwordChar - ); +// global instance of ThemeManager; see ThemeManager.create() +let themeManagerInstance; - if(_.isString(pwChar)) { - pwChar = pwChar.substr(0, 1); - } else if(_.isNumber(pwChar)) { - pwChar = String.fromCharCode(pwChar); +exports.ThemeManager = class ThemeManager { + constructor() { + this.availableThemes = new Map(); + } + + static create(cb) { + themeManagerInstance = new ThemeManager(); + themeManagerInstance.init(err => { + if (!err) { + themeManagerInstance.getAvailableThemes().forEach( (themeConfig, themeId) => { + const { name, author, group } = themeConfig.get().info; + Log.info( + { themeId, themeName : name, author, group }, + 'Theme loaded' + ); + }); } - return pwChar; - }, - getDateFormat : function(style = 'short') { - const format = Config().theme.dateFormat[style] || 'MM/DD/YYYY'; - return _.get(theme, `customization.defaults.dateFormat.${style}`, format); - }, - getTimeFormat : function(style = 'short') { - const format = Config().theme.timeFormat[style] || 'h:mm a'; - return _.get(theme, `customization.defaults.timeFormat.${style}`, format); - }, - getDateTimeFormat : function(style = 'short') { - const format = Config().theme.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; - return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format); - } - }; -} - -function loadTheme(themeId, cb) { - const path = paths.join(Config().paths.themes, themeId, 'theme.hjson'); - - const changed = ( { fileName, fileRoot } ) => { - const reCachedPath = paths.join(fileRoot, fileName); - if(reCachedPath === path) { - reloadTheme(themeId); - } - }; - - const getOpts = { - filePath : path, - forceReCache : true, - callback : changed, - }; - - ConfigCache.getConfigWithOptions(getOpts, (err, theme) => { - if(err) { return cb(err); - } + }); + } + getAvailableThemes() { + return this.availableThemes; + } + + init(cb) { + this.menuConfig = new ConfigLoader({ + onReload : err => { + if (!err) { + // menu.hjson/includes have changed; this could affect + // all themes, so they must be reloaded + Events.emit(Events.getSystemEvents().MenusChanged); + + this._reloadAllThemes(); + } + }, + }); + + this.menuConfig.init(getConfigPath(Config().general.menuFile), err => { + if (err) { + return cb(err); + } + + return this._loadThemes(cb); + }); + } + + _loadThemes(cb) { + const themeDir = Config().paths.themes; + + fs.readdir(themeDir, (err, files) => { + if (err) { + return cb(err); + } + + async.filter(files, (filename, nextFilename) => { + const fullPath = paths.join(themeDir, filename); + fs.stat(fullPath, (err, stats) => { + if (err) { + return nextFilename(err); + } + + return nextFilename(null, stats.isDirectory()); + }); + }, + (err, themeIds) => { + if (err) { + return cb(err); + } + + async.each(themeIds, (themeId, nextThemeId) => { + return this._loadTheme(themeId, nextThemeId); + }, + err => { + return cb(err); + }); + }); + }); + } + + _loadTheme(themeId, cb) { + const themeConfig = new ConfigLoader({ + onReload : err => { + if (!err) { + // this particular theme has changed + this._themeLoaded(themeId, err => { + if (!err) { + Events.emit( + Events.getSystemEvents().ThemeChanged, + { themeId } + ); + } + }); + } + } + }); + + const themeConfigPath = paths.join(Config().paths.themes, themeId, 'theme.hjson'); + + themeConfig.init(themeConfigPath, err => { + if (err) { + return cb(err); + } + + this._themeLoaded(themeId, themeConfig); + return cb(null); + }); + } + + _themeLoaded(themeId, themeConfig) { + const theme = themeConfig.get(); + + // do some basic validation + // :TODO: schema validation here if(!_.isObject(theme.info) || !_.isString(theme.info.name) || !_.isString(theme.info.author)) { - return cb(Errors.Invalid('Invalid or missing "info" section')); + return Log.warn({ themeId }, 'Theme contains invalid or missing "info" section'); } if(false === _.get(theme, 'info.enabled')) { - return cb(Errors.General('Theme is not enabled', ErrorReasons.ErrNotEnabled)); + Log.info({ themeId }, 'Theme is disabled'); + return this.availableThemes.delete(themeId); } - refreshThemeHelpers(theme); + // merge menu.hjson+theme.hjson/etc. to the final usable theme + this._finalizeTheme(themeConfig); - return cb(null, theme, path); - }); -} + // Theme is valid and enabled; update it in available themes + this.availableThemes.set(themeId, themeConfig); -const availableThemes = new Map(); - -const IMMUTABLE_MCI_PROPERTIES = [ - 'maxLength', 'argName', 'submit', 'validate' -]; - -function getMergedTheme(menuConfig, promptConfig, theme) { - assert(_.isObject(menuConfig)); - assert(_.isObject(theme)); - - // :TODO: merge in defaults (customization.defaults{} ) - // :TODO: apply generic stuff, e.g. "VM" (vs "VM1") - - // - // Create a *clone* of menuConfig (menu.hjson) then bring in - // promptConfig (prompt.hjson) - // - const mergedTheme = _.cloneDeep(menuConfig); - - if(_.isObject(promptConfig.prompts)) { - mergedTheme.prompts = _.cloneDeep(promptConfig.prompts); + Events.emit( + Events.getSystemEvents().ThemeChanged, + { themeId } + ); } - // - // Add in data we won't be altering directly from the theme - // - mergedTheme.info = theme.info; - mergedTheme.helpers = theme.helpers; - mergedTheme.achievements = _.get(theme, 'customization.achievements'); + _finalizeTheme(themeConfig) { + // These TODOs are left over from the old system - decide what/if to do with them: + // :TODO: merge in defaults (customization.defaults{} ) + // :TODO: apply generic stuff, e.g. "VM" (vs "VM1") - // - // merge customizer to disallow immutable MCI properties - // - const mciCustomizer = function(objVal, srcVal, key) { - return IMMUTABLE_MCI_PROPERTIES.indexOf(key) > -1 ? objVal : srcVal; - }; + // start out with menu.hjson + const mergedTheme = _.cloneDeep(this.menuConfig.get()); - function getFormKeys(fromObj) { - // remove all non-numbers - return _.remove(_.keys(fromObj), k => !isNaN(k)); - } + const theme = themeConfig.get(); - function mergeMciProperties(dest, src) { - Object.keys(src).forEach(mci => { - if(dest[mci]) { - _.mergeWith(dest[mci], src[mci], mciCustomizer); - } else { - // theme contains MCI not in menu; bring in as-is - dest[mci] = src[mci]; - } - }); - } + // some data brought directly over + mergedTheme.info = theme.info; + mergedTheme.achievements = _.get(theme, 'customization.achievements'); - function applyThemeMciBlock(dest, src, formKey) { - if(_.isObject(src.mci)) { - mergeMciProperties(dest, src.mci); - } else { - if(_.has(src, [ formKey, 'mci' ])) { - mergeMciProperties(dest, src[formKey].mci); - } - } - } + // Create some helpers for this theme + this._setThemeHelpers(mergedTheme); - // - // menu.hjson can have a couple different structures: - // 1) Explicit declaration of expected MCI code(s) under 'form:' before a 'mci' block - // (this allows multiple layout types defined by one menu for example) - // - // 2) Non-explicit declaration: 'mci' directly under 'form:' - // - // theme.hjson has it's own mix: - // 1) Explicit: Form ID before 'mci' (generally used where there are > 1 forms) - // - // 2) Non-explicit: 'mci' directly under an entry - // - // Additionally, #1 or #2 may be under an explicit key of MCI code(s) to match up - // with menu.hjson in #1. - // - // * When theming an explicit menu.hjson entry (1), we will use a matching explicit - // entry with a matching MCI code(s) key in theme.hjson (e.g. menu="ETVM"/theme="ETVM" - // and fall back to generic if a match is not found. - // - // * If theme.hjson provides form ID's, use them. Otherwise, we'll apply directly assuming - // there is a generic 'mci' block. - // - function applyToForm(form, menuTheme, formKey) { - if(_.isObject(form.mci)) { - // non-explicit: no MCI code(s) key assumed since we found 'mci' directly under form ID - applyThemeMciBlock(form.mci, menuTheme, formKey); - } else { - // remove anything not uppercase - const menuMciCodeKeys = _.remove(_.keys(form), k => k === k.toUpperCase()); + // merge customizer to disallow immutable MCI properties + const ImmutableMCIProperties = [ + 'maxLength', 'argName', 'submit', 'validate' + ]; - menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) { - let applyFrom; - if(_.has(menuTheme, [ mciKey, 'mci' ])) { - applyFrom = menuTheme[mciKey]; + const mciCustomizer = (objVal, srcVal, key) => { + return ImmutableMCIProperties.indexOf(key) > -1 ? objVal : srcVal; + }; + + const getFormKeys = (obj) => { + // remove all non-numbers + return _.remove(Object.keys(obj), k => !isNaN(k)); + }; + + const mergeMciProperties = (dst, src) => { + Object.keys(src).forEach(mci => { + if (dst[mci]) { + _.mergeWith(dst[mci], src[mci], mciCustomizer); } else { - applyFrom = menuTheme; + // theme contains a MCI that's not found in menu + dst[mci] = src[mci]; } - - applyThemeMciBlock(form[mciKey].mci, applyFrom, formKey); }); - } - } + }; - [ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) { - _.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) { - let createdFormSection = false; - const mergedThemeMenu = mergedTheme[sectionName][menuName]; + const applyThemeMciBlock = (dst, src, formKey) => { + if(_.isObject(src.mci)) { + mergeMciProperties(dst, src.mci); + } else if (_.has(src, [ formKey, 'mci' ])) { + mergeMciProperties(dst, src[formKey].mci); + } + }; - if(_.has(theme, [ 'customization', sectionName, menuName ])) { - const menuTheme = theme.customization[sectionName][menuName]; + // + // menu.hjson can have a couple different structures: + // 1) Explicit declaration of expected MCI code(s) under 'form:' before a 'mci' block + // (this allows multiple layout types defined by one menu for example) + // + // 2) Non-explicit declaration: 'mci' directly under 'form:' + // + // theme.hjson has it's own mix: + // 1) Explicit: Form ID before 'mci' (generally used where there are > 1 forms) + // + // 2) Non-explicit: 'mci' directly under an entry + // + // Additionally, #1 or #2 may be under an explicit key of MCI code(s) to match up + // with menu.hjson in #1. + // + // * When theming an explicit menu.hjson entry (1), we will use a matching explicit + // entry with a matching MCI code(s) key in theme.hjson (e.g. menu="ETVM"/theme="ETVM" + // and fall back to generic if a match is not found. + // + // * If theme.hjson provides form ID's, use them. Otherwise, we'll apply directly assuming + // there is a generic 'mci' block. + // + const applyToForm = (form, menuTheme, formKey) => { + if (_.isObject(form.mci)) { + // non-explicit: no MCI code(s) key assumed since we found 'mci' directly under form ID + applyThemeMciBlock(form.mci, menuTheme, formKey); + } else { + // remove anything not uppercase + const menuMciCodeKeys = _.remove(_.keys(form), k => k === k.toUpperCase()); - // config block is direct assign/overwrite - // :TODO: should probably be _.merge() - if(menuTheme.config) { - mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config); - } + menuMciCodeKeys.forEach(mciKey => { + const src = _.has(menuTheme, [ mciKey, 'mci' ]) ? + menuTheme[mciKey] : + menuTheme; - if('menus' === sectionName) { - if(_.isObject(mergedThemeMenu.form)) { - getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) { - applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey); - }); - } else { - if(_.isObject(menuTheme.mci)) { + applyThemeMciBlock(form[mciKey].mci, src, formKey); + }); + } + }; + + [ 'menus', 'prompts'].forEach(sectionName => { + Object.keys(mergedTheme[sectionName]).forEach(entryName => { + let createdFormSection = false; + const mergedThemeMenu = mergedTheme[sectionName][entryName]; + + const menuTheme = _.get(theme, [ 'customization', sectionName, entryName ]); + if (menuTheme) { + if (menuTheme.config) { + // :TODO: should this be _.merge() ? + mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config); + } + + if('menus' === sectionName) { + if(_.isObject(mergedThemeMenu.form)) { + getFormKeys(mergedThemeMenu.form).forEach(formKey => { + applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey); + }); + } else if(_.isObject(menuTheme.mci)) { // // Not specified at menu level means we apply anything from the // theme to form.0.mci{} @@ -241,158 +290,84 @@ function getMergedTheme(menuConfig, promptConfig, theme) { mergeMciProperties(mergedThemeMenu.form[0], menuTheme); createdFormSection = true; } + } else if('prompts' === sectionName) { + // no 'form' or form keys for prompts -- direct to mci + applyToForm(mergedThemeMenu, menuTheme); } - } else if('prompts' === sectionName) { - // no 'form' or form keys for prompts -- direct to mci - applyToForm(mergedThemeMenu, menuTheme); } - } - // - // Finished merging for this menu/prompt - // - // If the following conditions are true, set runtime.autoNext to true: - // * This is a menu - // * There is/was no explicit 'form' section - // * There is no 'prompt' specified - // - if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) && - (createdFormSection || !_.isObject(mergedThemeMenu.form))) - { - mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } ); - } + // + // Finished merging for this menu/prompt + // + // If the following conditions are true, set runtime.autoNext to true: + // * This is a menu + // * There is/was no explicit 'form' section + // * There is no 'prompt' specified + // + if('menus' === sectionName && + !_.isString(mergedThemeMenu.prompt) && + (createdFormSection || !_.isObject(mergedThemeMenu.form))) + { + mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } ); + } + }); }); - }); + themeConfig.current = mergedTheme; + } - return mergedTheme; -} + _setThemeHelpers(theme) { + theme.helpers = { + getPasswordChar : function() { + let pwChar = _.get( + theme, + 'customization.defaults.passwordChar', + Config().theme.passwordChar + ); -function reloadTheme(themeId) { - const config = Config(); - async.waterfall( - [ - function loadMenuConfig(callback) { - getFullConfig(config.general.menuFile, (err, menuConfig) => { - return callback(err, menuConfig); - }); + if(_.isString(pwChar)) { + pwChar = pwChar.substr(0, 1); + } else if(_.isNumber(pwChar)) { + pwChar = String.fromCharCode(pwChar); + } + + return pwChar; }, - function loadPromptConfig(menuConfig, callback) { - getFullConfig(config.general.promptFile, (err, promptConfig) => { - return callback(err, menuConfig, promptConfig); - }); + getDateFormat : function(style = 'short') { + const format = Config().theme.dateFormat[style] || 'MM/DD/YYYY'; + return _.get(theme, `customization.defaults.dateFormat.${style}`, format); }, - function loadIt(menuConfig, promptConfig, callback) { - loadTheme(themeId, (err, theme) => { - if(err) { - if(ErrorReasons.NotEnabled !== err.reasonCode) { - Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme'); - return; - } - return callback(err); - } - - Object.assign(theme.info, { themeId } ); - availableThemes.set(themeId, getMergedTheme(menuConfig, promptConfig, theme)); - - Events.emit( - Events.getSystemEvents().ThemeChanged, - { themeId } - ); - - return callback(null, theme); - }); + getTimeFormat : function(style = 'short') { + const format = Config().theme.timeFormat[style] || 'h:mm a'; + return _.get(theme, `customization.defaults.timeFormat.${style}`, format); + }, + getDateTimeFormat : function(style = 'short') { + const format = Config().theme.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; + return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format); } - ], - (err, theme) => { - if(err) { - Log.warn( { themeId, error : err.message }, 'Failed to reload theme'); - } else { - Log.debug( { info : theme.info }, 'Theme recached' ); - } - } - ); -} + }; + } -function reloadAllThemes() -{ - async.each([ ...availableThemes.keys() ], themeId => reloadTheme(themeId)); -} - -function initAvailableThemes(cb) { - const config = Config(); - async.waterfall( - [ - function loadMenuConfig(callback) { - getFullConfig(config.general.menuFile, (err, menuConfig) => { - return callback(err, menuConfig); - }); - }, - function loadPromptConfig(menuConfig, callback) { - getFullConfig(config.general.promptFile, (err, promptConfig) => { - return callback(err, menuConfig, promptConfig); - }); - }, - function getThemeDirectories(menuConfig, promptConfig, callback) { - fs.readdir(config.paths.themes, (err, files) => { - if(err) { - return callback(err); - } - - return callback( - null, - menuConfig, - promptConfig, - files.filter( f => { - // sync normally not allowed -- initAvailableThemes() is a startup-only method, however - return fs.statSync(paths.join(config.paths.themes, f)).isDirectory(); - }) - ); - }); - }, - function populateAvailable(menuConfig, promptConfig, themeDirectories, callback) { - async.each(themeDirectories, (themeId, nextThemeDir) => { // theme dir = theme ID - loadTheme(themeId, (err, theme) => { - if(err) { - if(ErrorReasons.NotEnabled !== err.reasonCode) { - Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme'); - } - - return nextThemeDir(null); // try next - } - - Object.assign(theme.info, { themeId } ); - availableThemes.set(themeId, getMergedTheme(menuConfig, promptConfig, theme)); - return nextThemeDir(null); - }); - }, err => { - return callback(err); - }); - }, - function initEvents(callback) { - Events.on(Events.getSystemEvents().MenusChanged, () => { - return reloadAllThemes(); - }); - Events.on(Events.getSystemEvents().PromptsChanged, () => { - return reloadAllThemes(); - }); - - return callback(null); - } - ], - err => { - return cb(err, availableThemes.size); - } - ); -} + _reloadAllThemes() { + async.each([ ...this.availableThemes.keys() ], (themeId, nextThemeId) => { + this._loadTheme(themeId, err => { + if (!err) { + Log.info({ themeId }, 'Theme reloaded'); + } + return nextThemeId(null); // always proceed + }); + }); + } +}; function getAvailableThemes() { - return availableThemes; + return themeManagerInstance.getAvailableThemes(); } function getRandomTheme() { - if(availableThemes.size > 0) { - const themeIds = [ ...availableThemes.keys() ]; + const avail = getAvailableThemes(); + if(avail.size > 0) { + const themeIds = [ ...avail.keys() ]; return themeIds[Math.floor(Math.random() * themeIds.length)]; } } diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 6b335617..941637c3 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -16,7 +16,6 @@ - [System Configuration]({{ site.baseurl }}{% link configuration/config-hjson.md %}) - [HJSON Config Files]({{ site.baseurl }}{% link configuration/hjson.md %}) - [Menus]({{ site.baseurl }}{% link configuration/menu-hjson.md %}) - - [Prompts]({{ site.baseurl }}{% link configuration/prompt-hjson.md %}) - [Directory Structure]({{ site.baseurl }}{% link configuration/directory-structure.md %}) - [Archivers]({{ site.baseurl }}{% link configuration/archivers.md %}) - [File Transfer Protocols]({{ site.baseurl }}{% link configuration/file-transfer-protocols.md %}) diff --git a/docs/art/themes.md b/docs/art/themes.md index 577a95fe..4da7c310 100644 --- a/docs/art/themes.md +++ b/docs/art/themes.md @@ -38,7 +38,7 @@ The `customization` block in is itself broken up into major parts: |-------------|---------------------------------------------------| | `defaults` | Default values to use when this theme is active. These values override system defaults, but can still be overridden themselves in specific areas of your theme. | | `menus` | The bulk of what you theme in the system will be here. Any menu (that is, anything you find in `menu.hjson`) can be tweaked. | -| `prompts` | Similar to `menus`, this file themes prompts found in `prompts.hjson`. | +| `prompts` | Similar to `menus`, this section themes `prompts`. | #### Defaults | Item | Description | @@ -46,7 +46,7 @@ The `customization` block in is itself broken up into major parts: | `passwordChar` | Character to display in password fields. Defaults to `*` | | `dateFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for dates. | | `timeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for times. | -| `dateTimeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for date/time combinations. | +| `dateTimeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for date/time combinations. | Example: ```hjson diff --git a/docs/configuration/colour-codes.md b/docs/configuration/colour-codes.md index 9ad9abdd..e2355f01 100644 --- a/docs/configuration/colour-codes.md +++ b/docs/configuration/colour-codes.md @@ -2,9 +2,7 @@ layout: page title: Colour Codes --- -ENiGMA½ supports Renegade-style pipe colour codes for formatting strings. You'll see them used in [`config.hjson`](config-hjson), -[`prompt.hjson`](prompt-hjson), [`menu.hjson`](menu-hjson), and can also be used in places like the oneliner, rumour mod, -full screen editor etc. +ENiGMA½ supports Renegade-style pipe colour codes for formatting strings. You'll see them used throughout your configuration, and can also be used in places like the oneliner, rumour mod, full screen editor etc. ## Usage When ENiGMA½ encounters colour codes in strings, they'll be processed in order and combined where possible. @@ -18,8 +16,8 @@ For example: ## Colour Code Reference -:warning: Colour codes |24 to |31 are considered "blinking" or "iCE" colour codes. On terminals that support them they'll +:warning: Colour codes |24 to |31 are considered "blinking" or "iCE" colour codes. On terminals that support them they'll be shown as the correct colours - for terminals that don't, or are that are set to "blinking" mode - they'll blink! -![Regegade style colour codes](../assets/images/colour-codes.png "Colour Codes") +![Renegade style colour codes](../assets/images/colour-codes.png "Colour Codes") diff --git a/docs/configuration/creating-config.md b/docs/configuration/creating-config.md index 5d845d5e..068ad5fa 100644 --- a/docs/configuration/creating-config.md +++ b/docs/configuration/creating-config.md @@ -10,5 +10,5 @@ Your initial configuration skeleton can be created using the `oputil.js` command ./oputil.js config new ``` -You will be asked a series of questions to create an initial configuration, which will be saved to `/enigma-bbs-install-path/config/config.hjson`. This will also produce `config/-menu.hjson` and `config/-prompt.hjson` files (where `` is replaced by the name you provided in the steps above). See [Menu HJSON](menu-hjson.md) and [Prompt HJSON](prompt-hjson.md) for more information. +You will be asked a series of questions to create an initial configuration, which will be saved to `/enigma-bbs-install-path/config/config.hjson`. This will also produce `config/-menu.hjson` and `config/-prompt.hjson` files (where `` is replaced by the name you provided in the steps above). See [Menu HJSON](menu-hjson.md) for more information. diff --git a/docs/configuration/hjson.md b/docs/configuration/hjson.md index 74b6eb01..e812479f 100644 --- a/docs/configuration/hjson.md +++ b/docs/configuration/hjson.md @@ -3,7 +3,7 @@ layout: page title: HJSON Config Files --- ## JSON for Humans! -HJSON is the configuration file format used by ENiGMA½ for [System Configuration](config-hjson.md), [Menus](menu-hjson.md), [Prompts](prompt-hjson.md), etc. [HJSON](https://hjson.org/) is is [JSON](https://json.org/) for humans! +HJSON is the configuration file format used by ENiGMA½ for [System Configuration](config-hjson.md), [Menus](menu-hjson.md), etc. [HJSON](https://hjson.org/) is is [JSON](https://json.org/) for humans! For those completely unfamiliar, JSON stands for JavaScript Object Notation. But don't let that scare you! JSON is simply a text file format with a bit of structure ― kind of like a fancier INI file. HJSON on the other hand as mentioned previously, is JSON for humans. That is, it has the following features and more: @@ -18,7 +18,6 @@ Through the documentation, some terms regarding HJSON and configuration files wi * `config.hjson`: Refers to `/path/to/enigma-bbs/config/config.hjson`. See [System Configuration](config-hjson.md). * `menu.hjson`: Refers to `/path/to/enigma-bbs/config/-menu.hjson`. See [Menus](menu-hjson.md). -* `prompt.hjson`: Refers to `/path/to/enigma-bbs/config/-prompt.hjson`. See [Prompts](prompt-hjson.md). * Configuration *key*: Elements in HJSON are name-value pairs where the name is the *key*. For example, provided `foo: bar`, `foo` is the key. * Configuration *section* or *block* (also commonly called an "Object" in code): This is referring to a section in a HJSON file that starts with a *key*. For example: ```hjson diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 8ddda2d8..d8d21af8 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -21,7 +21,7 @@ Below is a table of **common** menu entry members. These members apply to most e | `desc` | A friendly description that can be found in places such as "Who's Online" or wherever the `%MD` MCI code is used. | | `art` | An art file *spec*. See [General Art Information](/docs/art/general.md). | | `next` | Specifies the next menu entry to go to next. Can be explicit or an array of possibilities dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. | -| `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in `prompt.hjson`. | +| `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in the `prompts` section. See **Prompts** for more information. | | `submit` | Defines a submit handler when using `prompt`. | `form` | An object defining one or more *forms* available on this menu. | | `module` | Sets the module name to use for this menu. See **Menu Modules** below. | @@ -183,6 +183,9 @@ In the above entry, you'll notice `form`. This defines a form(s) object. In this * The `submit` object tells the system to attempt to apply provided match entries from any view ID (`*`). * Upon submit, the first match will be executed. For example, if the user selects "login", the first entry with a value of `{ matrixSubmit: 0 }` will match (due to 0 being the first index in the list and `matrixSubmit` being the arg name in question) causing `action` of `@menu:login` to be executed (go to `login` menu). +## Prompts +TODO: describe the "prompts" section, default setup, etc. + ## ACS Checks Menu modules can check user ACS in order to restrict areas and perform flow control. See [ACS](acs.md) for available ACS syntax. diff --git a/docs/configuration/prompt-hjson.md b/docs/configuration/prompt-hjson.md deleted file mode 100644 index 993e5b8e..00000000 --- a/docs/configuration/prompt-hjson.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -layout: page -title: prompt.hjson ---- -:zap: This page is to describe general information the `prompt.hjson` file. It -needs fleshing out, please submit a PR if you'd like to help! - -See [HJSON General Information](hjson.md) for more information. From a457296b391e6fdeec7f679c4149e6fd8388a390 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 17 Jun 2020 19:45:18 -0600 Subject: [PATCH 17/43] * Remove 'date', 'datetime', and 'time' in favor of just 'timestamp' for env var directive * Allow "1" === true for boolean types --- core/config_loader.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/config_loader.js b/core/config_loader.js index 3844df1a..768cbe1d 100644 --- a/core/config_loader.js +++ b/core/config_loader.js @@ -117,7 +117,7 @@ module.exports = class ConfigLoader { switch (type) { case 'bool' : case 'boolean' : - value = 'true' === value.toLowerCase(); + value = ('1' === value || 'true' === value.toLowerCase()); break; case 'number' : @@ -137,9 +137,6 @@ module.exports = class ConfigLoader { } break; - case 'date' : - case 'time' : - case 'datetime' : case 'timestamp' : { const m = moment(value); From 99c053f4faa309dee6438fb7377b175115c45f0e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 17 Jun 2020 19:46:18 -0600 Subject: [PATCH 18/43] Docs on configuration files --- WHATSNEW.md | 4 +- core/system_events.js | 1 - docs/_includes/nav.md | 1 + docs/configuration/config-files.md | 99 ++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 docs/configuration/config-files.md diff --git a/WHATSNEW.md b/WHATSNEW.md index 21d43083..27b8dfa2 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -4,8 +4,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For ## 0.0.12-beta * The `master` branch has become mainline. What this means to users is `git pull` will always give you the latest and greatest. Make sure to read [Updating](/docs/admin/updating.md) and keep an eye on `WHATSNEW.md` (this file) and [UPGRADE](UPGRADE.md)! See also [ticket #276](https://github.com/NuSkooler/enigma-bbs/issues/276). * The default configuration has been moved to [config_default.js](/core/config_default.js). -* A full configuration revamp has taken place. Configuration files such as `config.hjson`, `menu.hjson`, and `theme.hjson` can now utilize includes via the `include` directive, reference 'self' sections using `@reference:` and import environment variables with `@environment`. -* An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `include`ed in `menu.hjson`. +* A full configuration revamp has taken place. Configuration files such as `config.hjson`, `menu.hjson`, and `theme.hjson` can now utilize includes via the `includes` directive, reference 'self' sections using `@reference:` and import environment variables with `@environment`. +* An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `includes`ed in `menu.hjson`. ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/core/system_events.js b/core/system_events.js index 129dacfd..00f242ca 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -9,7 +9,6 @@ module.exports = { ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // (theme.hjson): { themeId } ConfigChanged : 'codes.l33t.enigma.system.config_changed', // (config.hjson) MenusChanged : 'codes.l33t.enigma.system.menus_changed', // (menu.hjson) - PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', // (prompt.hjson) // User - includes { user, ...} NewUser : 'codes.l33t.enigma.system.user_new', // { ... } diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 941637c3..1f8e27c7 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -13,6 +13,7 @@ - Configuration - [Creating Config Files]({{ site.baseurl }}{% link configuration/creating-config.md %}) - [SysOp Setup]({{ site.baseurl }}{% link configuration/sysop-setup.md %}) + - [Configuration Files]({{ site.baseurl }}{% link configuration/config-files.md %}) - [System Configuration]({{ site.baseurl }}{% link configuration/config-hjson.md %}) - [HJSON Config Files]({{ site.baseurl }}{% link configuration/hjson.md %}) - [Menus]({{ site.baseurl }}{% link configuration/menu-hjson.md %}) diff --git a/docs/configuration/config-files.md b/docs/configuration/config-files.md new file mode 100644 index 00000000..ae625d2a --- /dev/null +++ b/docs/configuration/config-files.md @@ -0,0 +1,99 @@ +--- +layout: page +title: Configuration Files +--- +## General Information +ENiGMA½ configuration files such as the [system config](config-hjson.md), [menus](menu-hjson.md) and [themes](../art/themes.md) are formatted in the [HJSON format](hjson.md). + +## Hot-Reload +Nearly all of ENiGMA½'s configuration can be hot-reloaded. That is, a live system can have it's configuration modified and it will be loaded in place. + +## Common Directives +### Includes +Most configuration files offer an `includes` directive that allows users to break up large configuration files into smaller and organized parts. For example, consider a system with many menus/screens. Instead of a single `menu.hjson`, the SysOp may break this into `message-base.hjson`, `file-base.hjson`, etc. + +The `includes` directive may be used the top-level scope of a configuration file: +```hjson +{ + // menu.hjson + + includes: [ + message-base.hjson + file-base.hjson + ] + + menus: { + someMenu: { + // ... + } + } +} +``` + +### References +Often times in a configuration you will find that you're repeating yourself quite a bit. ENiGMA½ provides an `@reference` that can help with this in the form of `@reference:dot.path.to.section`. + +Consider `actionKeys` in a menu. Often times you may show a screen and the user presses `Q` or `ESC` to fall back to the previous. Instead of repeating this in many menus, a generic recyclable block can be utilized: + +```hjson +{ + // note that 'recycle' here is arbitrary; + // only 'menus' and 'prompts' is reserved at this level. + recycle: { + prevMenu: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + + menus: { + someMenu: { + // ... + form: { + 0: { + // ... + actionKeys: @reference:recycle.prevMenu + } + } + } + } +} +``` + +:information_source: An unresolved @reference will be left intact. + +### Environment Variables +Especially in a container environment such as [Docker](/docs/installation/docker.md), environment variable access in configuration files can become very handy. ENiGMA½ provides a flexible way to access variables using the `@environment` directive. The most basic form of `@environment:VAR_NAME` produces a string value. Additionally a `:type` suffix can be supplied to coerece the value to a particular type. Variables pointing to a comma separated list can be turned to arrays using an additional `:array` suffix. + +Below is a table of the various forms: +| Form | Variable Value | Produces | +|------|----------------|----------| +| `@environment:SOME_VAR` | "Foo" | `"Foo"` (without quotes) | +| `@environment:SOME_VAR` | "123" | `"123"` (without quotes) | +| `@environment:SOME_VAR:string` | "Bar" | `"Bar"` (without quotes) | +| `@environment:SOME_VAR:string:array` | "Foo,Bar" | `[ 'Foo', 'Bar' ]` | +| `@environment:SOME_VAR:boolean` | "1" | `true` | +| `@environment:SOME_VAR:boolean` | "True" | `true` | +| `@environment:SOME_VAR:boolean` | "false" | `false` | +| `@environment:SOME_VAR:boolean` | "cat" | `false` | +| `@environment:SOME_VAR:boolean:array` | "True,false,TRUE" | `[ true, false, true ]` | +| `@environment:SOME_VAR:number` | "123" | `123` | +| `@environment:SOME_VAR:number:array` | "123,456" | `[ 123, 456 ]` | +| `@environment:SOME_VAR:number` | "kitten" | (invalid) | +| `@environment:SOME_VAR:object` | '{"a":"b"}' | `{ 'a' : 'b' }` | +| `@environment:SOME_VAR:object:array` | '{"a":"b"},{"c":"d"}' | `[ { 'a' : 'b' }, { 'c' : 'd' } ]` | +| `@environment:SOME_VAR:timestamp` | "2020-01-05" | A [moment](https://momentjs.com/) object representing 2020-01-05 | +| `@environment:SOME_VAR:timestamp:array` | "2020-01-05,2016-05-16T01:15:37'" | An array of [moment](https://momentjs.com/) objects representing 2020-01-05 and 2016-05-16T01:15:37 | + +:information_source: `bool` may be used as an alias to `boolean`. + +:information_source: `timestamp` values can be in any form that [moment can parse](https://momentjs.com/docs/#/parsing/). + +:information_source: An unresolved or invalid @environment will be left intact. + +## See Also +* [System Configuration](config-hjson.md) +* [Menu Configuration](menu-hjson.md) +* [The HJSON Format](hjson.md) From e1091ea115499c204816c38df13263e3aa9cf895 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 17 Jun 2020 19:49:35 -0600 Subject: [PATCH 19/43] Fix indent in doc --- docs/configuration/config-files.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/configuration/config-files.md b/docs/configuration/config-files.md index ae625d2a..61f032aa 100644 --- a/docs/configuration/config-files.md +++ b/docs/configuration/config-files.md @@ -40,13 +40,13 @@ Consider `actionKeys` in a menu. Often times you may show a screen and the user // note that 'recycle' here is arbitrary; // only 'menus' and 'prompts' is reserved at this level. recycle: { - prevMenu: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } + prevMenu: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } menus: { someMenu: { From c1d082ef804d9726c89e3b5043dce95dd08a0e54 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 17 Jun 2020 19:53:23 -0600 Subject: [PATCH 20/43] Add environment example --- docs/configuration/config-files.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/configuration/config-files.md b/docs/configuration/config-files.md index 61f032aa..67806fd7 100644 --- a/docs/configuration/config-files.md +++ b/docs/configuration/config-files.md @@ -93,6 +93,24 @@ Below is a table of the various forms: :information_source: An unresolved or invalid @environment will be left intact. +Consider the following fragment: +```hjson +{ + foo: { + bar: @environment:BAR_VAR:number + } +} +``` + +If the environment has `BAR_VAR=1337`, this would produce: +```hjson +{ + foo: { + bar: 1337 + } +} +``` + ## See Also * [System Configuration](config-hjson.md) * [Menu Configuration](menu-hjson.md) From 5c99cd6cb50eddec504943cb40c8d7ab3ab9ecc1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 17 Jun 2020 22:04:30 -0600 Subject: [PATCH 21/43] Better docs --- docs/configuration/config-files.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/configuration/config-files.md b/docs/configuration/config-files.md index 67806fd7..e889452d 100644 --- a/docs/configuration/config-files.md +++ b/docs/configuration/config-files.md @@ -14,16 +14,25 @@ Most configuration files offer an `includes` directive that allows users to brea The `includes` directive may be used the top-level scope of a configuration file: ```hjson +// menu.hjson { - // menu.hjson - includes: [ message-base.hjson file-base.hjson ] menus: { - someMenu: { + someOtherMenu: { + // ... + } + } +} +``` +```hjson +// message-base.hjson +{ + menus: { + someMessageMenu: { // ... } } @@ -33,7 +42,7 @@ The `includes` directive may be used the top-level scope of a configuration file ### References Often times in a configuration you will find that you're repeating yourself quite a bit. ENiGMA½ provides an `@reference` that can help with this in the form of `@reference:dot.path.to.section`. -Consider `actionKeys` in a menu. Often times you may show a screen and the user presses `Q` or `ESC` to fall back to the previous. Instead of repeating this in many menus, a generic recyclable block can be utilized: +Consider `actionKeys` in a menu. Often times you may show a screen and the user presses `Q` or `ESC` to fall back to the previous. Instead of repeating this in many menus, a generic block can be referenced: ```hjson { @@ -50,10 +59,8 @@ Consider `actionKeys` in a menu. Often times you may show a screen and the user menus: { someMenu: { - // ... form: { 0: { - // ... actionKeys: @reference:recycle.prevMenu } } @@ -62,7 +69,7 @@ Consider `actionKeys` in a menu. Often times you may show a screen and the user } ``` -:information_source: An unresolved @reference will be left intact. +:information_source: An unresolved `@reference` will be left intact. ### Environment Variables Especially in a container environment such as [Docker](/docs/installation/docker.md), environment variable access in configuration files can become very handy. ENiGMA½ provides a flexible way to access variables using the `@environment` directive. The most basic form of `@environment:VAR_NAME` produces a string value. Additionally a `:type` suffix can be supplied to coerece the value to a particular type. Variables pointing to a comma separated list can be turned to arrays using an additional `:array` suffix. @@ -91,7 +98,7 @@ Below is a table of the various forms: :information_source: `timestamp` values can be in any form that [moment can parse](https://momentjs.com/docs/#/parsing/). -:information_source: An unresolved or invalid @environment will be left intact. +:information_source: An unresolved or invalid `@environment` will be left intact. Consider the following fragment: ```hjson From ce15511cf1315c188b3e2d00326402935b1f4283 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 18 Jun 2020 20:46:24 -0600 Subject: [PATCH 22/43] Fix missing themeId --- core/theme.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/theme.js b/core/theme.js index 557377b7..db9561f1 100644 --- a/core/theme.js +++ b/core/theme.js @@ -163,7 +163,7 @@ exports.ThemeManager = class ThemeManager { } // merge menu.hjson+theme.hjson/etc. to the final usable theme - this._finalizeTheme(themeConfig); + this._finalizeTheme(themeId, themeConfig); // Theme is valid and enabled; update it in available themes this.availableThemes.set(themeId, themeConfig); @@ -174,7 +174,7 @@ exports.ThemeManager = class ThemeManager { ); } - _finalizeTheme(themeConfig) { + _finalizeTheme(themeId, themeConfig) { // These TODOs are left over from the old system - decide what/if to do with them: // :TODO: merge in defaults (customization.defaults{} ) // :TODO: apply generic stuff, e.g. "VM" (vs "VM1") @@ -185,7 +185,7 @@ exports.ThemeManager = class ThemeManager { const theme = themeConfig.get(); // some data brought directly over - mergedTheme.info = theme.info; + mergedTheme.info = Object.assign({}, theme.info, { themeId }); mergedTheme.achievements = _.get(theme, 'customization.achievements'); // Create some helpers for this theme From 20bbbd2f591cc742d3f5ec1a0f6246a2a5df1093 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 18 Jun 2020 21:29:24 -0600 Subject: [PATCH 23/43] Nicer handling of config file errors at startup --- core/bbs.js | 13 +++++++++++-- core/config.js | 3 +-- core/config_loader.js | 3 +++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index 074a742b..87289e94 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -54,6 +54,8 @@ function printVersionAndExit() { } function main() { + let errorDisplayed = false; + async.waterfall( [ function processArgs(callback) { @@ -87,7 +89,14 @@ function main() { configPathSupplied = null; // make non-fatal; we'll go with defaults } } else { - console.error(err.message); + errorDisplayed = true; + console.error(`Configuration error: ${err.message}`); // eslint-disable-line no-console + if (err.hint) { + console.error(`Hint: ${err.hint}`); + } + if (err.configPath) { + console.error(`Note: ${err.configPath}`); + } } } return callback(err); @@ -114,7 +123,7 @@ function main() { }); } - if(err) { + if(err && !errorDisplayed) { console.error('Error initializing: ' + util.inspect(err)); } } diff --git a/core/config.js b/core/config.js index 791845b8..8c77de0c 100644 --- a/core/config.js +++ b/core/config.js @@ -48,7 +48,6 @@ exports.Config = class Config extends ConfigLoader { systemConfigInstance = new Config(options); systemConfigInstance.init(baseConfigPath, err => { if (err) { - console.stdout(`Configuration ${baseConfigPath} error: ${err.message}`); // eslint-disable-line no-console return cb(err); } @@ -56,7 +55,7 @@ exports.Config = class Config extends ConfigLoader { // instance we just created exports.get = systemConfigInstance.get.bind(systemConfigInstance); - return cb(err); + return cb(null); }); } diff --git a/core/config_loader.js b/core/config_loader.js index 768cbe1d..da3de0d9 100644 --- a/core/config_loader.js +++ b/core/config_loader.js @@ -180,6 +180,9 @@ module.exports = class ConfigLoader { }; ConfigCache.getConfigWithOptions(options, (err, config) => { + if (err) { + err.configPath = options.filePath; + } return cb(err, config); }); } From 5ab927d2f52a40185a9127ad5962da320108122f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 29 Jun 2020 22:26:35 -0600 Subject: [PATCH 24/43] Better getConfigPath() --- core/config_util.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/config_util.js b/core/config_util.js index 63e46f62..714055dd 100644 --- a/core/config_util.js +++ b/core/config_util.js @@ -9,9 +9,9 @@ const paths = require('path'); exports.getConfigPath = getConfigPath; function getConfigPath(filePath) { - // |filePath| is assumed to be in the config path if it's only a file name - if('.' === paths.dirname(filePath)) { - filePath = paths.join(Config().paths.config, filePath); + if (paths.isAbsolute(filePath)) { + return filePath; } - return filePath; + + return paths.join(Config().paths.config, filePath); } From 0b78b42f2fd7e883f94206e71e2e27138e279c23 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 29 Jun 2020 22:27:13 -0600 Subject: [PATCH 25/43] Fix DoesNotExist error --- core/menu_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/menu_util.js b/core/menu_util.js index 923d907d..0943fe22 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -37,7 +37,7 @@ function getMenuConfig(client, name, cb) { menuConfig.promptConfig = client.currentTheme.prompts[menuConfig.prompt]; return callback(null, menuConfig); } - return callback(Error.DoesNotExist(`No prompt entry for "${menuConfig.prompt}"`)); + return callback(Errors.DoesNotExist(`No prompt entry for "${menuConfig.prompt}"`)); } return callback(null, menuConfig); } From b36191392923384f473b4fd89be79cce676424af Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 29 Jun 2020 22:27:31 -0600 Subject: [PATCH 26/43] Fix getConfigPath() --- core/oputil/oputil_common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/oputil/oputil_common.js b/core/oputil/oputil_common.js index ae63fa2a..ff6e348b 100644 --- a/core/oputil/oputil_common.js +++ b/core/oputil/oputil_common.js @@ -64,7 +64,7 @@ function getDefaultConfigPath() { } function getConfigPath() { - const baseConfigPath = argv.config ? argv.config : config.getDefaultPath(); + const baseConfigPath = argv.config ? argv.config : config.Config.getDefaultPath(); return baseConfigPath + 'config.hjson'; } From 72564b7db6d2b704932b233354c4f46827e1e690 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 29 Jun 2020 22:59:27 -0600 Subject: [PATCH 27/43] Fix theme getter --- core/user_config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/user_config.js b/core/user_config.js index ca3b20ff..4ccd7017 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -171,7 +171,7 @@ exports.getModule = class UserConfigModule extends MenuModule { }, function prepareAvailableThemes(callback) { self.availThemeInfo = _.sortBy([...theme.getAvailableThemes()].map(entry => { - const theme = entry[1]; + const theme = entry[1].get(); return { themeId : theme.info.themeId, name : theme.info.name, From c849a1dc6d7265a5183c1bc33a51bfb2d2dbac48 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 5 Jul 2020 14:05:45 -0600 Subject: [PATCH 28/43] Update Node version in template --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index a8e6a08e..b6d1464a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,7 +3,7 @@ For :bug: bug reports, please fill out the information below plus any additional **Short problem description** **Environment** -- [ ] I am using Node.js v10.x LTS or higher +- [ ] I am using Node.js v12.x LTS or higher - [ ] `npm install` or `yarn` reports success - Actual Node.js version (`node --version`): - Operating system (`uname -a` on *nix systems): From 35ef2db4463733c6b087a2e64d45a92af107f07b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 5 Jul 2020 14:06:10 -0600 Subject: [PATCH 29/43] Fix theme reload bug --- core/theme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/theme.js b/core/theme.js index db9561f1..f763614d 100644 --- a/core/theme.js +++ b/core/theme.js @@ -121,7 +121,7 @@ exports.ThemeManager = class ThemeManager { onReload : err => { if (!err) { // this particular theme has changed - this._themeLoaded(themeId, err => { + this._themeLoaded(themeId, themeConfig, err => { if (!err) { Events.emit( Events.getSystemEvents().ThemeChanged, From 965e845834b04c8ad7b68ba4c482af27cf40147e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 5 Jul 2020 14:06:34 -0600 Subject: [PATCH 30/43] Major work on breaking up menus --- art/themes/luciano_blocktronics/theme.hjson | 70 +- core/oputil/oputil_config.js | 52 +- misc/config_template.in.hjson | 8 +- misc/menu_template.in.hjson | 4421 ------------------- misc/menu_templates/doors.in.hjson | 132 + misc/menu_templates/file_base.in.hjson | 918 ++++ misc/menu_templates/login.in.hjson | 607 +++ misc/menu_templates/main.in.hjson | 1024 +++++ misc/menu_templates/message_base.in.hjson | 816 ++++ misc/menu_templates/new_user.in.hjson | 362 ++ misc/menu_templates/private_mail.in.hjson | 197 + misc/prompt_template.in.hjson | 152 +- 12 files changed, 4147 insertions(+), 4612 deletions(-) delete mode 100644 misc/menu_template.in.hjson create mode 100644 misc/menu_templates/doors.in.hjson create mode 100644 misc/menu_templates/file_base.in.hjson create mode 100644 misc/menu_templates/login.in.hjson create mode 100644 misc/menu_templates/main.in.hjson create mode 100644 misc/menu_templates/message_base.in.hjson create mode 100644 misc/menu_templates/new_user.in.hjson create mode 100644 misc/menu_templates/private_mail.in.hjson diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index d812a9c3..4045a549 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -4,6 +4,11 @@ author: Luciano Ayres group: blocktronics enabled: true + + // + // Also check out Luciano's ANSIGARDEN: + // http://www.ansigarden.com/ + // } customization: { @@ -32,7 +37,7 @@ ET2: { width: 23 } ET5: { width: 23 } ET6: { width: 23 } - + ET7: { width: 23 } ET8: { width: 23 } ET9: { width: 23 } @@ -43,14 +48,14 @@ } } } - + newUserApplicationSsh: { mci: { ET1: { width: 23 } ET2: { width: 23 } ET5: { width: 23 } ET6: { width: 23 } - + ET7: { width: 23 } ET8: { width: 23 } ET9: { width: 23 } @@ -97,7 +102,7 @@ } TM2: { focusTextStyle: first lower - } + } } } 1: { @@ -227,7 +232,7 @@ } TM2: { focusTextStyle: first lower - } + } } } 1: { @@ -241,8 +246,8 @@ } } - messageAreaMessageList: { - config: { + messageBaseMessageList: { + config: { dateTimeFormat: ddd MMM Do allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}" } @@ -256,7 +261,7 @@ } } - messageAreaChangeCurrentConference: { + messageBaseChangeCurrentConference: { mci: { VM1: { width: 26 @@ -267,7 +272,7 @@ } } - messageAreaChangeCurrentArea: { + messageBaseChangeCurrentArea: { mci: { VM1: { width: 26 @@ -278,7 +283,7 @@ } } - messageAreaSetNewScanDate: { + messageBaseSetNewScanDate: { mci: { SM2: { width: 54 @@ -299,7 +304,7 @@ } } - mailMenuCreateMessage: { + privateMailMenuCreateMessage: { 0: { mci: { TL1: { width: 19, textOverflow: "..." } @@ -314,7 +319,7 @@ } } - mailMenuInbox: { + privateMailMenuInbox: { config: { dateTimeFormat: ddd MMM Do allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}" @@ -344,7 +349,7 @@ TM2: { focusTextStyle: upper items: [ "yes", "no" ] - } + } } } 1: { @@ -375,7 +380,7 @@ TL6: { width: 28 } TL7: { width: 28 } TL8: { width: 28 } - TL9: { width: 28 } + TL9: { width: 28 } } }, 1: { @@ -500,7 +505,7 @@ } } - messageAreaSearchMessageList: { + messageBaseSearchMessageList: { config: { allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}" // Fri Sep 25th @@ -516,7 +521,7 @@ } } - messageAreaMyMessagesList: { + messageBaseMyMessagesList: { config: { // Fri Sep 25th dateTimeFormat: ddd MMM Do @@ -531,22 +536,23 @@ } } + // The 'msg_list' module looks for this entry by default messageAreaViewPost: { 0: { mci: { - TL1: { + TL1: { width: 19 - textOverflow: ... + textOverflow: ... } TL2: { width: 19 textOverflow: ... } - TL3: { + TL3: { width: 19 textOverflow: ... } - TL5: { + TL5: { width: 19 textOverflow: ... } @@ -566,7 +572,7 @@ } } - messageAreaNewPost: { + messageBaseNewPost: { 0: { mci: { TL1: { width: 19, textOverflow: "..." } @@ -582,7 +588,7 @@ } } - messageAreaReplyPost: { + messageBaseReplyPost: { 0: { mci: { TL1: { width: 19, textOverflow: "..." } @@ -664,7 +670,7 @@ } newScanMessageList: { - config: { + config: { dateTimeFormat: ddd MMM Do allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}" } @@ -693,7 +699,7 @@ config: { hashTagsSep: "|08, |07" browseInfoFormat10: "|00|10{fileName:<.44} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}" - browseInfoFormat11: "|00|15{areaName}" + browseInfoFormat11: "|00|15{areaName}" browseInfoFormat12: "|00|07{hashTags}" browseInfoFormat13: "|00|07{estReleaseYear}" browseInfoFormat14: "|00|07{dlCount}" @@ -706,7 +712,7 @@ isQueuedIndicator: "|00|10YES" isNotQueuedIndicator: "|00|07no" - + userRatingTicked: "|00|15*" userRatingUnticked: "|00|07-" @@ -749,7 +755,7 @@ TL13: { width: 21 } TL14: { width: 21 } TL15: { width: 21 } - TL16: { width: 21 } + TL16: { width: 21 } TL17: { width: 73 } } @@ -793,7 +799,7 @@ config: { hashTagsSep: "|08, |07" browseInfoFormat10: "|00|10{fileName:<44} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}" - browseInfoFormat11: "|00|15{areaName}" + browseInfoFormat11: "|00|15{areaName}" browseInfoFormat12: "|00|07{hashTags}" browseInfoFormat13: "|00|07{estReleaseYear}" browseInfoFormat14: "|00|07{dlCount}" @@ -806,7 +812,7 @@ isQueuedIndicator: "|00|10YES" isNotQueuedIndicator: "|00|07no" - + userRatingTicked: "|00|15*" userRatingUnticked: "|00|07-" @@ -849,7 +855,7 @@ TL13: { width: 21 } TL14: { width: 21 } TL15: { width: 21 } - TL16: { width: 21 } + TL16: { width: 21 } TL17: { width: 73 } } @@ -901,7 +907,7 @@ } } } - } + } fileBaseSearch: { mci: { @@ -977,7 +983,7 @@ } } - fileAreaFilterEditor: { + fileBaseFilterEditor: { mci: { ET1: { width: 26 @@ -1086,7 +1092,7 @@ mci: { TL1: { width: 48 } TL2: { width: 48 } - TL3: { width: 48 } + TL3: { width: 48 } MT4: { height: 6 width: 68 diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index 4548a910..28c065d4 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -227,29 +227,47 @@ function buildNewConfig() { if(err) { return; } - const bn = sanatizeFilename(config.general.boardName) + const boardName = sanatizeFilename(config.general.boardName) .replace(/[^a-z0-9_-]/ig, '_') .replace(/_+/g, '_') .toLowerCase(); - const menuFile = `${bn}-menu.hjson`; - copyFileSyncSilent( - paths.join(__dirname, '../../misc/menu_template.in.hjson'), - paths.join(__dirname, '../../config/', menuFile), - fs.constants.COPYFILE_EXCL - ); + const menuFile = `menus/${boardName}-main.hjson`; - const promptFile = `${bn}-prompt.hjson`; - copyFileSyncSilent( - paths.join(__dirname, '../../misc/prompt_template.in.hjson'), - paths.join(__dirname, '../../config/', promptFile), - fs.constants.COPYFILE_EXCL - ); + const mainTemplate = hjson.rt.parse(fs.readFileSync(paths.join(__dirname, '../../misc/menu_templates/main.in.hjson'), 'utf8')); - config.general.menuFile = menuFile; - config.general.promptFile = promptFile; + const includeFiles = [ + 'message_base.in.hjson', + 'private_mail.in.hjson', + 'login.in.hjson', + 'new_user.in.hjson', + 'doors.in.hjson', + 'file_base.in.hjson', + ]; - if(writeConfig(config, configPath)) { - console.info('Configuration generated'); + includeFiles.forEach(incFile => { + const outName = `${boardName}-${incFile.replace('.in', '')}`; + copyFileSyncSilent( + paths.join(__dirname, '../../misc/menu_templates', incFile), + paths.join(__dirname, '../../config/menus', outName), + fs.constants.COPYFILE_EXCL + ); + }); + + mainTemplate.includes = includeFiles.map(incFile => { + return `${boardName}-${incFile.replace('.in', '')}`; + }); + + if (writeConfig( + mainTemplate, + paths.join(__dirname, '../../config/menus', `${boardName}-main.hjson`))) + { + config.general.menuFile = menuFile; + + if(writeConfig(config, configPath)) { + console.info('Configuration generated'); + } else { + console.error('Failed writing configuration'); + } } else { console.error('Failed writing configuration'); } diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index abcdc09a..1db5406d 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -369,7 +369,7 @@ storageTags: { // // Example storage tag: "super_l33t_warez": - // super_l33t_warez: "/path/to/super/l33t/warez" + // super_l33t_warez: /path/to/super/l33t/warez // } @@ -377,10 +377,10 @@ // // Example area with the areaTag of "an_example_area": // an_example_area: { - // name: "Example File Area" - // desc: "It's just an example, yo!" + // name: Example File Area + // desc: It's just an example, yo! // storageTags: [ - // "super_l33t_warez" + // super_l33t_warez // ] // } // diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson deleted file mode 100644 index 57a3f761..00000000 --- a/misc/menu_template.in.hjson +++ /dev/null @@ -1,4421 +0,0 @@ -{ - /* - ./\/\.' ENiGMA½ Menu Configuration -/--/-------- - -- - - - _____________________ _____ ____________________ __________\_ / - \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! - // __|___// | \// |// | \// | | \// \ /___ /_____ - /____ _____| __________ ___|__| ____| \ / _____ \ - ---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ - /__ _\ - <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ - - *-----------------------------------------------------------------------------* - - General Information - ------------------------------- - - - This configuration is in HJSON (http://hjson.org/) format. Strict to-spec - JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. - - See http://hjson.org/ for more information and syntax. - - Various editors and IDEs such as Sublime Text 3, Visual Studio Code, and so - on have syntax highlighting for the HJSON format which are highly recommended. - - ------------------------------- -- - - - Menu Configuration - ------------------------------- - - - ENiGMA½ makes no assumptions about specific menu types (main, doors, etc.), - but instead allows full customization of all menus throughout the system. - Some menus such as a main menu are considered "standard" while others are - backed by a specific module. SysOps can tweak various settings about these - modules (look & feel, keyboard interation, and so on) or even fully replace - the module with something else. - - This file starts out as an example setup. Look at the examples, change - settings, menu ordering/flow, add/remove menus, implement ACS control, - etc.! - - Remember you can *live edit* this file. That is, make a change and save - while you're logged into the system and it will take effect on the next - menu change or screen refresh. - - Please see RTFM ...er, uh... see the documentation for more information, and - don't be shy to ask for help: - - BBS : Xibalba @ xibalba.l33t.codes - FTN : BBS Discussion on fsxNet - IRC : #enigma-bbs / FreeNode - Email : bryan@l33t.codes - */ - menus: { - // - // Send telnet connections to matrix where users can login, apply, etc. - // - telnetConnected: { - art: CONNECT - next: matrix - config: { nextTimeout: 1500 } - } - - // - // SSH connections are pre-authenticated via the SSH server itself. - // Jump directly to either the 2FA/OTP auth or the login sequence - // depending on user ACS. - // - sshConnected: { - art: CONNECT - next: [ - { - acs: AR2 - next: loginTwoFactorAuthOTPLoop - } - { - next: fullLoginSequenceLoginArt - } - ] - config: { nextTimeout: 1500 } - } - - // - // Another SSH specialization: If the user logs in with a new user - // name (e.g. "new", "apply", ...) they will be directed to the - // application process. - // - sshConnectedNewUser: { - art: CONNECT - next: newUserApplicationPreSsh - config: { nextTimeout: 1500 } - } - - // Ye ol' standard matrix - matrix: { - art: matrix - form: { - 0: { - VM: { - mci: { - VM1: { - submit: true - focus: true - argName: navSelect - items: [ - { - text: login - data: login - } - { - text: apply - data: apply - } - - // - // To enable the forgot password option, you'll need to have - // the web server & email configured. Once that is in place, - // uncomment the section below. - // - // See docs for more information - // - /* - { - text: forgot pass - data: forgot - } - */ - { - text: log off - data: logoff - } - ] - } - } - submit: { - *: [ - { - value: { navSelect: "login" } - action: @menu:login - } - { - value: { navSelect: "apply" } - action: @menu:newUserApplicationPre - } - { - value: { navSelect: "forgot" } - action: @menu:forgotPassword - } - { - value: { navSelect: "logoff" } - action: @menu:logoff - } - ] - } - } - } - } - } - - login: { - art: USERLOG - next: [ - { - // - // Users with 2FA/OTP enabled *must* go through - // an additional OTP authentication step - // - acs: AR2 - next: loginTwoFactorAuthOTPLoop - } - { - // ...everyone else can carry on as per usual - next: fullLoginSequenceLoginArt - } - ] - config: { - tooNodeMenu: loginAttemptTooNode - inactive: loginAttemptAccountInactive - disabled: loginAttemptAccountDisabled - locked: loginAttemptAccountLocked - } - form: { - 0: { - mci: { - ET1: { - maxLength: @config:users.usernameMax - argName: username - focus: true - } - ET2: { - password: true - maxLength: @config:users.passwordMax - argName: password - submit: true - } - } - submit: { - *: [ - { - value: { password: null } - action: @systemMethod:login - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - loginAttemptTooNode: { - art: TOONODE - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - loginAttemptAccountLocked: { - art: ACCOUNTLOCKED - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - loginAttemptAccountDisabled: { - art: ACCOUNTDISABLED - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - loginAttemptAccountInactive: { - art: ACCOUNTINACTIVE - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - // - // Empty menu to catch us in a 2FA/OTP auth loop - // until the user either authenticates successfully - // or the system boots them. - // - loginTwoFactorAuthOTPLoop: { - next: loginTwoFactorAuthOTP - } - - loginTwoFactorAuthOTP: { - art: 2FAOTP - next: fullLoginSequenceLoginArt - form: { - 0: { - mci: { - ET1: { - argName: token - focus: true - submit: true - } - } - submit: { - *: [ - { - value: { token: null } - action: @systemMethod:login2FA_OTP - } - ] - } - actionKeys: [ - { - // no turning back at this point... - keys: [ "escape" ] - action: @systemMethod:logoff - } - ] - } - } - } - - forgotPassword: { - desc: Forgot password - prompt: forgotPasswordPrompt - submit: [ - { - value: { username: null } - action: @systemMethod:sendForgotPasswordEmail - extraArgs: { next: "forgotPasswordSubmitted" } - } - ] - } - - forgotPasswordSubmitted: { - desc: Forgot password - art: FORGOTPWSENT - config: { - cls: true - pause: true - } - next: @systemMethod:logoff - } - - // :TODO: Prompt Yes/No for logoff confirm - fullLogoffSequence: { - desc: Logging Off - prompt: logoffConfirmation - submit: [ - { - value: { promptValue: 0 } - action: @menu:fullLogoffSequencePreAd - } - { - value: { promptValue: 1 } - action: @systemMethod:prevMenu - } - ] - } - - fullLogoffSequencePreAd: { - art: PRELOGAD - desc: Logging Off - next: fullLogoffSequenceRandomBoardAd - config: { - cls: true - nextTimeout: 1500 - } - } - - fullLogoffSequenceRandomBoardAd: { - art: OTHRBBS - desc: Logging Off - next: logoff - config: { - baudRate: 57600 - pause: true - cls: true - } - } - - logoff: { - art: LOGOFF - desc: Logging Off - next: @systemMethod:logoff - } - - // A quick preamble - defaults to warning about broken terminals - newUserApplicationPre: { - art: NEWUSER1 - next: newUserApplication - desc: Applying - config: { - pause: true - cls: true - menuFlags: [ "noHistory" ] - } - } - - newUserApplication: { - module: nua - art: NUA - next: [ - { - // Initial SysOp does not send feedback to themselves - acs: ID1 - next: fullLoginSequenceLoginArt - } - { - // ...everyone else does - next: newUserFeedbackToSysOpPreamble - } - ] - form: { - 0: { - mci: { - ET1: { - focus: true - argName: username - maxLength: @config:users.usernameMax - validate: @systemMethod:validateUserNameAvail - } - ET2: { - argName: realName - maxLength: @config:users.realNameMax - validate: @systemMethod:validateNonEmpty - } - MET3: { - argName: birthdate - maskPattern: "####/##/##" - validate: @systemMethod:validateBirthdate - } - ME4: { - argName: sex - maskPattern: A - textStyle: upper - validate: @systemMethod:validateNonEmpty - } - ET5: { - argName: location - maxLength: @config:users.locationMax - validate: @systemMethod:validateNonEmpty - } - ET6: { - argName: affils - maxLength: @config:users.affilsMax - } - ET7: { - argName: email - maxLength: @config:users.emailMax - validate: @systemMethod:validateEmailAvail - } - ET8: { - argName: web - maxLength: @config:users.webMax - } - ET9: { - argName: password - password: true - maxLength: @config:users.passwordMax - validate: @systemMethod:validatePasswordSpec - } - ET10: { - argName: passwordConfirm - password: true - maxLength: @config:users.passwordMax - validate: @method:validatePassConfirmMatch - } - TM12: { - argName: submission - items: [ "apply", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { "submission" : 0 } - action: @method:submitApplication - extraArgs: { - inactive: userNeedsActivated - error: newUserCreateError - } - } - { - value: { "submission" : 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - // A quick preamble - defaults to warning about broken terminals (SSH version) - newUserApplicationPreSsh: { - art: NEWUSER1 - next: newUserApplicationSsh - desc: Applying - config: { - pause: true - cls: true - menuFlags: [ "noHistory" ] - } - } - - // - // SSH specialization of NUA - // Canceling this form logs off vs falling back to matrix - // - newUserApplicationSsh: { - module: nua - art: NUA - fallback: logoff - next: newUserFeedbackToSysOpPreamble - form: { - 0: { - mci: { - ET1: { - focus: true - argName: username - maxLength: @config:users.usernameMax - validate: @systemMethod:validateUserNameAvail - } - ET2: { - argName: realName - maxLength: @config:users.realNameMax - validate: @systemMethod:validateNonEmpty - } - MET3: { - argName: birthdate - maskPattern: "####/##/##" - validate: @systemMethod:validateBirthdate - } - ME4: { - argName: sex - maskPattern: A - textStyle: upper - validate: @systemMethod:validateNonEmpty - } - ET5: { - argName: location - maxLength: @config:users.locationMax - validate: @systemMethod:validateNonEmpty - } - ET6: { - argName: affils - maxLength: @config:users.affilsMax - } - ET7: { - argName: email - maxLength: @config:users.emailMax - validate: @systemMethod:validateEmailAvail - } - ET8: { - argName: web - maxLength: @config:users.webMax - } - ET9: { - argName: password - password: true - maxLength: @config:users.passwordMax - validate: @systemMethod:validatePasswordSpec - } - ET10: { - argName: passwordConfirm - password: true - maxLength: @config:users.passwordMax - validate: @method:validatePassConfirmMatch - } - TM12: { - argName: submission - items: [ "apply", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { "submission" : 0 } - action: @method:submitApplication - extraArgs: { - inactive: userNeedsActivated - error: newUserCreateError - } - } - { - value: { "submission" : 1 } - action: @systemMethod:logoff - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:logoff - } - ] - } - } - } - - newUserFeedbackToSysOpPreamble: { - art: LETTER - config: { pause: true } - next: newUserFeedbackToSysOp - } - - newUserFeedbackToSysOp: { - desc: Feedback to SysOp - module: msg_area_post_fse - next: [ - { - acs: AS2 - next: fullLoginSequenceLoginArt - } - { - next: newUserInactiveDone - } - ] - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - }, - editorMode: edit - editorType: email - messageAreaTag: private_mail - toUserId: 1 /* always to +op */ - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - text: @sysStat:sysop_username - maxLength: 36 - // :TODO: readOnly: true - } - ET3: { - argName: subject - maxLength: 72 - submit: true - text: New user feedback - validate: @systemMethod:validateMessageSubject - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - } - 1: { - mci: { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { value: "message", action: "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - }, - 2: { - TLTL: { - mci: { - TL1: { - width: 5 - } - TL2: { - width: 4 - } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - items: [ "save", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - } - } - } - - newUserInactiveDone: { - desc: Finished with NUA - art: DONE - config: { pause: true } - next: @menu:logoff - } - - fullLoginSequenceLoginArt: { - desc: Logging In - art: WELCOME - config: { pause: true } - next: fullLoginSequenceLastCallers - } - - fullLoginSequenceLastCallers: { - desc: Last Callers - module: last_callers - art: LASTCALL - config: { - pause: true - font: cp437 - } - next: fullLoginSequenceWhosOnline - } - fullLoginSequenceWhosOnline: { - desc: Who's Online - module: whos_online - art: WHOSON - config: { pause: true } - next: fullLoginSequenceOnelinerz - } - - fullLoginSequenceOnelinerz: { - desc: Viewing Onelinerz - module: onelinerz - next: [ - { - // calls >= 2 - acs: NC2 - next: fullLoginSequenceNewScanConfirm - } - { - // new users - skip new scan - next: fullLoginSequenceUserStats - } - ] - config: { - cls: true - art: { - view: ONELINER - add: ONEADD - } - } - form: { - 0: { - mci: { - VM1: { - focus: false - height: 10 - } - TM2: { - argName: addOrExit - items: [ "yeah!", "nah" ] - "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } - submit: true - focus: true - } - } - submit: { - *: [ - { - value: { addOrExit: 0 } - action: @method:viewAddScreen - } - { - value: { addOrExit: null } - action: @systemMethod:nextMenu - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:nextMenu - } - ] - }, - 1: { - mci: { - ET1: { - focus: true - maxLength: 70 - argName: oneliner - } - TL2: { - width: 60 - } - TM3: { - argName: addOrCancel - items: [ "add", "cancel" ] - "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } - submit: true - } - } - - submit: { - *: [ - { - value: { addOrCancel: 0 } - action: @method:addEntry - } - { - value: { addOrCancel: 1 } - action: @method:cancelAdd - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelAdd - } - ] - } - } - } - - fullLoginSequenceNewScanConfirm: { - desc: Logging In - prompt: loginGlobalNewScan - submit: [ - { - value: { promptValue: 0 } - action: @menu:fullLoginSequenceNewScan - } - { - value: { promptValue: 1 } - action: @menu:fullLoginSequenceUserStats - } - ] - } - - fullLoginSequenceNewScan: { - desc: Performing New Scan - module: new_scan - art: NEWSCAN - next: fullLoginSequenceSysStats - config: { - messageListMenu: newScanMessageList - } - } - - fullLoginSequenceSysStats: { - desc: System Stats - art: SYSSTAT - config: { pause: true } - next: fullLoginSequenceUserStats - } - fullLoginSequenceUserStats: { - desc: User Stats - art: STATUS - config: { pause: true } - next: mainMenu - } - - newScanMessageList: { - desc: New Messages - module: msg_list - art: NEWMSGS - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - TL6: { - // theme me! - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "x", "shift + x" ] - action: @method:fullExit - } - { - keys: [ "m", "shift + m" ] - action: @method:markAllRead - } - ] - } - } - } - - newScanFileBaseList: { - module: file_area_list - desc: New Files - config: { - art: { - browse: FNEWBRWSE - details: FDETAIL - detailsGeneral: FDETGEN - detailsNfo: FDETNFO - detailsFileList: FDETLST - help: FBHELP - } - } - form: { - 0: { - mci: { - MT1: { - mode: preview - ansiView: true - } - - HM2: { - focus: true - submit: true - argName: navSelect - items: [ - "prev", "next", "details", "toggle queue", "rate", "help", "quit" - ] - focusItemIndex: 1 - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:prevFile - } - { - value: { navSelect: 1 } - action: @method:nextFile - } - { - value: { navSelect: 2 } - action: @method:viewDetails - } - { - value: { navSelect: 3 } - action: @method:toggleQueue - } - { - value: { navSelect: 4 } - action: @menu:fileBaseGetRatingForSelectedEntry - } - { - value: { navSelect: 5 } - action: @method:displayHelp - } - { - value: { navSelect: 6 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "w", "shift + w" ] - action: @method:showWebDownloadLink - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "t", "shift + t" ] - action: @method:toggleQueue - } - { - keys: [ "v", "shift + v" ] - action: @method:viewDetails - } - { - keys: [ "r", "shift + r" ] - action: @menu:fileBaseGetRatingForSelectedEntry - } - { - keys: [ "?" ] - action: @method:displayHelp - } - ] - } - - 1: { - mci: { - HM1: { - focus: true - submit: true - argName: navSelect - items: [ - "general", "nfo/readme", "file listing" - ] - } - } - - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @method:detailsQuit - } - ] - } - - 2: { - // details - general - mci: {} - } - - 3: { - // details - nfo/readme - mci: { - MT1: { - mode: preview - } - } - } - - 4: { - // details - file listing - mci: { - VM1: { - - } - } - } - } - } - - /////////////////////////////////////////////////////////////////////// - // Main Menu - /////////////////////////////////////////////////////////////////////// - mainMenu: { - art: MMENU - desc: Main Menu - prompt: menuCommand - config: { - font: cp437 - interrupt: realtime - } - submit: [ - { - value: { command: "MSG" } - action: @menu:nodeMessage - } - { - value: { command: "G" } - action: @menu:fullLogoffSequence - } - { - value: { command: "D" } - action: @menu:doorMenu - } - { - value: { command: "F" } - action: @menu:fileBase - } - { - value: { command: "U" } - action: @menu:mainMenuUserList - } - { - value: { command: "L" } - action: @menu:mainMenuLastCallers - } - { - value: { command: "W" } - action: @menu:mainMenuWhosOnline - } - { - value: { command: "Y" } - action: @menu:mainMenuUserStats - } - { - value: { command: "M" } - action: @menu:messageArea - } - { - value: { command: "E" } - action: @menu:mailMenu - } - { - value: { command: "C" } - action: @menu:mainMenuUserConfig - } - { - value: { command: "S" } - action: @menu:mainMenuSystemStats - } - { - value: { command: "!" } - action: @menu:mainMenuGlobalNewScan - } - { - value: { command: "K" } - action: @menu:mainMenuFeedbackToSysOp - } - { - value: { command: "O" } - action: @menu:mainMenuOnelinerz - } - { - value: { command: "R" } - action: @menu:mainMenuRumorz - } - { - value: { command: "BBS"} - action: @menu:bbsList - } - { - value: { command: "UA" } - action: @menu:mainMenuUserAchievementsEarned - } - { - value: { command: "MRC" } - action: @menu:mrc - } - { - value: { command: "2FA" } - action: [ - { - // - // For security reasons, only allow 2FA/OTP to be - // configured over already secure (SSL, wss://, ...) - // connections. Not doing so risks leaking secrets! - // - acs: SC - action: @menu:userTwoFactorAuthOTPConfig - } - { - action: @menu:userTwoFactorAuthOTPSecConnRequired - } - ] - } - { - value: 1 - action: @menu:mainMenu - } - ] - } - - mainMenuUserAchievementsEarned: { - desc: Achievements - module: user_achievements_earned - art: USERACHIEV - form: { - 0: { - mci: { - VM1: { - focus: true - } - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - mrc: { - desc: MRC Chat - module: mrc - art: MRC - config: { - cls: true - - // max lines kept in scrollback buffer - maxScrollbackLines: 500 - } - form: { - 0: { - mci: { - MT1: { - mode: preview - autoScroll: true - } - ET2: { - argName: inputArea - submit: true - focus: true - } - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - { - keys: [ "down arrow", "up arrow", "page up", "page down" ] - action: @method:movementKeyPressed - } - ] - submit: { - *: [ - { - value: { inputArea: null } - action: @method:sendChatMessage - } - ] - } - } - } - } - - userTwoFactorAuthOTPConfig: { - desc: 2FA/OTP Config - module: user_2fa_otp_config - art: 2FACONFSCR - form: { - 0: { - mci: { - TM1: { - argName: enableToggle - focus: true - items: [ - // order is important here: - "disable" - "enable/reset" - ] - } - SM2: { - argName: otpType - items: [ - // order is important here: - "Time-Based - TOTP" - "HMAC-Based - HOTP" - "Google Authenticator" - ] - } - TM3: { - argName: submit - items: [ - "save" - "cancel" - ] - submit: true - } - } - - submit: { - *: [ - { - value: { submit: 0 } - action: @method:saveChanges - } - { - value: { submit: 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - { - keys: [ "q", "shift + q" ] - action: @method:showQRCode - } - { - keys: [ "s", "shift + s" ] - action: @method:showSecret - } - { - keys: [ "b", "shift + b" ] - action: @method:showBackupCodes - } - { - keys: [ "n", "shift + n" ] - action: @method:generateNewBackupCodes - } - ] - } - } - } - - userTwoFactorAuthOTPSecConnRequired: { - desc: Insecure Warning - art: 2FAOTPSECREQ - config: { - cls: true - pause: true - } - } - - userTwoFactorAuthOTPConfigShowDetails: { - desc: 2FA/OTP Details - module: show_art - config: { - pause: true - method: extraArgs - } - } - - nodeMessage: { - desc: Node Messaging - module: node_msg - art: NODEMSG - config: { - cls: true - art: { - header: NODEMSGHDR - footer: NODEMSGFTR - } - } - form: { - 0: { - mci: { - SM1: { - argName: node - } - ET2: { - argName: message - submit: true - } - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - submit: { - *: [ - { - value: { message: null } - action: @method:sendMessage - } - ] - } - } - } - } - - mainMenuLastCallers: { - desc: Last Callers - module: last_callers - art: LASTCALL - config: { pause: true } - } - - mainMenuWhosOnline: { - desc: Who's Online - module: whos_online - art: WHOSON - config: { pause: true } - } - - mainMenuUserStats: { - desc: User Stats - art: STATUS - config: { pause: true } - } - - mainMenuSystemStats: { - desc: System Stats - art: SYSSTAT - config: { pause: true } - } - - mainMenuUserList: { - desc: User Listing - module: user_list - art: USERLST - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - } - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - mainMenuUserConfig: { - module: user_config - art: CONFSCR - form: { - 0: { - mci: { - ET1: { - argName: realName - maxLength: @config:users.realNameMax - validate: @systemMethod:validateNonEmpty - focus: true - } - ME2: { - argName: birthdate - maskPattern: "####/##/##" - } - ME3: { - argName: sex - maskPattern: A - textStyle: upper - validate: @systemMethod:validateNonEmpty - } - ET4: { - argName: location - maxLength: @config:users.locationMax - validate: @systemMethod:validateNonEmpty - } - ET5: { - argName: affils - maxLength: @config:users.affilsMax - } - ET6: { - argName: email - maxLength: @config:users.emailMax - validate: @method:validateEmailAvail - } - ET7: { - argName: web - maxLength: @config:users.webMax - } - ME8: { - maskPattern: "##" - argName: termHeight - validate: @systemMethod:validateNonEmpty - } - SM9: { - argName: theme - } - ET10: { - argName: password - maxLength: @config:users.passwordMax - password: true - validate: @method:validatePassword - } - ET11: { - argName: passwordConfirm - maxLength: @config:users.passwordMax - password: true - validate: @method:validatePassConfirmMatch - } - TM25: { - argName: submission - items: [ "save", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { submission: 0 } - action: @method:saveChanges - } - { - value: { submission: 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - mainMenuGlobalNewScan: { - desc: Performing New Scan - module: new_scan - art: NEWSCAN - config: { - messageListMenu: newScanMessageList - } - } - - mainMenuFeedbackToSysOp: { - desc: Feedback to SysOp - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - }, - editorMode: edit - editorType: email - messageAreaTag: private_mail - toUserId: 1 /* always to +op */ - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - text: @sysStat:sysop_username - maxLength: 36 - // :TODO: readOnly: true - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateMessageSubject - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { value: "message", action: "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - }, - 2: { - TLTL: { - mci: { - TL1: { - width: 5 - } - TL2: { - width: 4 - } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - items: [ "save", "discard", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - } - } - } - - mainMenuOnelinerz: { - desc: Viewing Onelinerz - module: onelinerz - config: { - cls: true - art: { - view: ONELINER - add: ONEADD - } - } - form: { - 0: { - mci: { - VM1: { - focus: false - height: 10 - } - TM2: { - argName: addOrExit - items: [ "yeah!", "nah" ] - "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } - submit: true - focus: true - } - } - submit: { - *: [ - { - value: { addOrExit: 0 } - action: @method:viewAddScreen - } - { - value: { addOrExit: null } - action: @systemMethod:nextMenu - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:nextMenu - } - ] - }, - 1: { - mci: { - ET1: { - focus: true - maxLength: 70 - argName: oneliner - } - TL2: { - width: 60 - } - TM3: { - argName: addOrCancel - items: [ "add", "cancel" ] - "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } - submit: true - } - } - - submit: { - *: [ - { - value: { addOrCancel: 0 } - action: @method:addEntry - } - { - value: { addOrCancel: 1 } - action: @method:cancelAdd - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelAdd - } - ] - } - } - } - - mainMenuRumorz: { - desc: Rumorz - module: rumorz - config: { - cls: true - art: { - entries: RUMORS - add: RUMORADD - } - } - form: { - 0: { - mci: { - VM1: { - focus: false - height: 10 - } - TM2: { - argName: addOrExit - items: [ "yeah!", "nah" ] - "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } - submit: true - focus: true - } - } - submit: { - *: [ - { - value: { addOrExit: 0 } - action: @method:viewAddScreen - } - { - value: { addOrExit: null } - action: @systemMethod:nextMenu - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:nextMenu - } - ] - }, - 1: { - mci: { - ET1: { - focus: true - maxLength: 70 - argName: rumor - } - TL2: { - width: 60 - } - TM3: { - argName: addOrCancel - items: [ "add", "cancel" ] - "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } - submit: true - } - } - - submit: { - *: [ - { - value: { addOrCancel: 0 } - action: @method:addEntry - } - { - value: { addOrCancel: 1 } - action: @method:cancelAdd - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelAdd - } - ] - } - } - } - - bbsList: { - desc: Viewing BBS List - module: bbs_list - config: { - cls: true - 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: @systemMethod:prevMenu - } - ] - } - 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 - /////////////////////////////////////////////////////////////////////// - doorMenu: { - desc: Doors Menu - art: DOORMNU - prompt: menuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { command: "G" } - action: @menu:logoff - } - { - value: { command: "Q" } - action: @systemMethod:prevMenu - } - // - // The system supports many ways of launching doors including - // modules for DoorParty!, BBSLink, etc. - // - // Below are some examples. See the documentation for more info. - // - { - value: { command: "ABRACADABRA" } - action: @menu:doorAbracadabraExample - } - { - value: { command: "TWBBSLINK" } - action: @menu:doorTradeWars2002BBSLinkExample - } - { - value: { command: "DP" } - action: @menu:doorPartyExample - } - { - value: { command: "CN" } - action: @menu:doorCombatNetExample - } - { - value: { command: "EXODUS" } - action: @menu:doorExodusCataclysm - } - ] - } - - // - // Local Door Example via abracadabra module - // - // This example assumes launch_door.sh (which is passed args) - // launches the door. - // - doorAbracadabraExample: { - desc: Abracadabra Example - module: abracadabra - config: { - name: Example Door - dropFileType: DORINFO - cmd: /home/enigma/DOS/scripts/launch_door.sh - args: [ - "{node}", - "{dropFile}", - "{srvPort}", - ], - nodeMax: 1 - tooManyArt: DOORMANY - io: socket - } - } - - // - // BBSLink Example (TradeWars 2000) - // - // Register @ https://bbslink.net/ - // - doorTradeWars2002BBSLinkExample: { - desc: Playing TW 2002 (BBSLink) - module: bbs_link - config: { - sysCode: XXXXXXXX - authCode: XXXXXXXX - schemeCode: XXXXXXXX - door: tw - } - } - - // - // DoorParty! Example - // - // Register @ http://throwbackbbs.com/ - // - doorPartyExample: { - desc: Using DoorParty! - module: door_party - config: { - username: XXXXXXXX - password: XXXXXXXX - bbsTag: XX - } - } - - // - // CombatNet Example - // - // Register @ http://combatnet.us/ - // - doorCombatNetExample: { - desc: Using CombatNet - module: combatnet - config: { - bbsTag: CBNxxx - password: XXXXXXXXX - } - } - - // - // Exodus Example (cataclysm) - // Register @ https://oddnetwork.org/exodus/ - // - doorExodusCataclysm: { - desc: Cataclysm - module: exodus - config: { - rejectUnauthorized: false - board: XXX - key: XXXXXXXX - door: cataclysm - } - } - - /////////////////////////////////////////////////////////////////////// - // Message Area Menu - /////////////////////////////////////////////////////////////////////// - messageArea: { - art: MSGMNU - desc: Message Area - prompt: messageMenuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { command: "P" } - action: @menu:messageAreaNewPost - } - { - value: { command: "J" } - action: @menu:messageAreaChangeCurrentConference - } - { - value: { command: "C" } - action: @menu:messageAreaChangeCurrentArea - } - { - value: { command: "L" } - action: @menu:messageAreaMessageList - } - { - value: { command: "Q" } - action: @systemMethod:prevMenu - } - { - value: { command: "G" } - action: @menu:fullLogoffSequence - } - { - value: { command: "<" } - action: @systemMethod:prevConf - } - { - value: { command: ">" } - action: @systemMethod:nextConf - } - { - value: { command: "[" } - action: @systemMethod:prevArea - } - { - value: { command: "]" } - action: @systemMethod:nextArea - } - { - value: { command: "D" } - action: @menu:messageAreaSetNewScanDate - } - { - value: { command: "S" } - action: @menu:messageSearch - } - { - value: { command: "M" } - action: @menu:myMessages - } - { - value: { command: "A" } - action: @menu:editAutoSignature - } - { - value: 1 - action: @menu:messageArea - } - ] - } - - editAutoSignature: { - desc: Auto Sig Editor - module: autosig_edit - art: autosig - form: { - 0: { - mci: { - MT1: { - argName: signature - tabSwitchesView: true - } - BT2: { - text: save - argName: save - submit: true - } - } - submit: { - *: [ - { - value: { save: null } - action: @method:saveChanges - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageSearch: { - desc: Message Search - module: message_base_search - art: MSEARCH - config: { - messageListMenu: messageAreaSearchMessageList - } - form: { - 0: { - mci: { - ET1: { - focus: true - argName: searchTerms - } - BT2: { - argName: search - text: search - submit: true - } - SM3: { - argName: confTag - } - SM4: { - argName: areaTag - } - ET5: { - argName: toUserName - maxLength: @config:users.usernameMax - } - ET6: { - argName: fromUserName - maxLength: @config:users.usernameMax - } - BT7: { - argName: advancedSearch - text: advanced search - submit: true - } - } - - submit: { - *: [ - { - value: { search: null } - action: @method:search - } - { - value: { advancedSearch: null } - action: @method:search - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaSearchMessageList: { - desc: Message Search - module: msg_list - art: MSRCHLST - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - TL6: { - // theme me! - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - myMessages: { - desc: Personal Messages - module: my_messages - config: { - messageListMenu: messageAreaMyMessagesList - } - } - - messageAreaMyMessagesList: { - desc: Personal Messages - module: msg_list - art: MYMSGLST - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageSearchNoResults: { - desc: Message Search - art: MSRCNORES - config: { - pause: true - } - } - - messageAreaChangeCurrentConference: { - art: CCHANGE - module: msg_conf_list - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: conf - } - } - submit: { - *: [ - { - value: { conf: null } - action: @method:changeConference - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaSetNewScanDate: { - module: set_newscan_date - desc: Message Base - art: SETMNSDATE - config: { - target: message - scanDateFormat: YYYYMMDD - } - form: { - 0: { - mci: { - ME1: { - focus: true - submit: true - argName: scanDate - maskPattern: "####/##/##" - } - SM2: { - argName: targetSelection - submit: false - } - } - submit: { - *: [ - { - value: { scanDate: null } - action: @method:scanDateSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - changeMessageConfPreArt: { - module: show_art - config: { - method: messageConf - key: confTag - pause: true - cls: true - menuFlags: [ "popParent", "noHistory" ] - } - } - - messageAreaChangeCurrentArea: { - // :TODO: rename this art to ACHANGE - art: CHANGE - module: msg_area_list - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: area - } - } - submit: { - *: [ - { - value: { area: null } - action: @method:changeArea - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - changeMessageAreaPreArt: { - module: show_art - config: { - method: messageArea - key: areaTag - pause: true - cls: true - menuFlags: [ "popParent", "noHistory" ] - } - } - - messageAreaMessageList: { - module: msg_list - art: MSGLIST - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaViewPost: { - module: msg_area_view_fse - config: { - art: { - header: MSGVHDR - body: MSGBODY - footerView: MSGVFTR - help: MSGVHLP - }, - editorMode: view - editorType: area - } - form: { - 0: { - mci: { - // :TODO: ensure this block isn't even req. for theme to apply... - } - } - 1: { - mci: { - MT1: { - width: 79 - mode: preview - } - } - submit: { - *: [ - { - value: message - action: @method:editModeEscPressed - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - } - 2: { - TLTL: { - mci: { - TL1: { width: 5 } - TL2: { width: 4 } - } - } - } - 4: { - mci: { - HM1: { - // :TODO: (#)Jump/(L)Index (msg list)/Last - items: [ "prev", "next", "reply", "quit", "help" ] - focusItemIndex: 1 - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:prevMessage - } - { - value: { 1: 1 } - action: @method:nextMessage - } - { - value: { 1: 2 } - action: @method:replyMessage - extraArgs: { - menu: messageAreaReplyPost - } - } - { - value: { 1: 3 } - action: @systemMethod:prevMenu - } - { - value: { 1: 4 } - action: @method:viewModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "p", "shift + p" ] - action: @method:prevMessage - } - { - keys: [ "n", "shift + n" ] - action: @method:nextMessage - } - { - keys: [ "r", "shift + r" ] - action: @method:replyMessage - extraArgs: { - menu: messageAreaReplyPost - } - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "?" ] - action: @method:viewModeMenuHelp - } - { - keys: [ "down arrow", "up arrow", "page up", "page down" ] - action: @method:movementKeyPressed - } - ] - } - } - } - - messageAreaReplyPost: { - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - quote: MSGQUOT - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - } - editorMode: edit - editorType: area - } - form: { - 0: { - mci: { - // :TODO: use appropriate system properties for max lengths - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - validate: @systemMethod:validateNonEmpty - maxLength: 36 - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateNonEmpty - } - TL4: { - // :TODO: this is for RE: line (NYI) - //width: 27 - //textOverflow: ... - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - MT1: { - width: 79 - height: 14 - argName: message - mode: edit - } - } - submit: { - *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ], - viewId: 1 - } - ] - } - - 3: { - mci: { - HM1: { - items: [ "save", "discard", "quote", "help" ] - } - } - - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 }, - action: @method:editModeMenuQuote - } - { - value: { 1: 3 } - action: @method:editModeMenuHelp - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "s", "shift + s" ] - action: @method:editModeMenuSave - } - { - keys: [ "d", "shift + d" ] - action: @systemMethod:prevMenu - } - { - keys: [ "q", "shift + q" ] - action: @method:editModeMenuQuote - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - - // Quote builder - 5: { - mci: { - MT1: { - width: 79 - height: 7 - } - VM3: { - width: 79 - height: 4 - argName: quote - } - } - - submit: { - *: [ - { - value: { quote: null } - action: @method:appendQuoteEntry - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @method:quoteBuilderEscPressed - } - ] - } - } - } - // :TODO: messageAreaSelect (change msg areas -> call @systemMethod -> fallback to menu - messageAreaNewPost: { - desc: Posting message, - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - } - editorMode: edit - editorType: area - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - text: All - validate: @systemMethod:validateNonEmpty - maxLength: 36 - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateNonEmpty - // :TODO: Validate -> close/cancel if empty - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - - 1: { - "mci" : { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - } - 2: { - TLTL: { - mci: { - TL1: { width: 5 } - TL2: { width: 4 } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - "items" : [ "save", "discard", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - // :TODO: something like the following for overriding keymap - // this should only override specified entries. others will default - /* - "keyMap" : { - "accept" : [ "return" ] - } - */ - } - } - } - } - - - // - // User to User mail aka Email Menu - // - mailMenu: { - art: MAILMNU - desc: Mail Menu - prompt: menuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { command: "C" } - action: @menu:mailMenuCreateMessage - } - { - value: { command: "I" } - action: @menu:mailMenuInbox - } - { - value: { command: "Q" } - action: @systemMethod:prevMenu - } - { - value: { command: "G" } - action: @menu:fullLogoffSequence - } - { - value: 1 - action: @menu:mailMenu - } - ] - } - - mailMenuCreateMessage: { - desc: Mailing Someone - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - }, - editorMode: edit - editorType: email - messageAreaTag: private_mail - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - validate: @systemMethod:validateGeneralMailAddressedTo - maxLength: 36 - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateMessageSubject - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { value: "message", action: "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - }, - 2: { - TLTL: { - mci: { - TL1: { - width: 5 - } - TL2: { - width: 4 - } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - items: [ "save", "discard", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - } - } - } - - mailMenuInbox: { - module: msg_list - art: PRVMSGLIST - config: { - menuViewPost: messageAreaViewPost - messageAreaTag: private_mail - } - form: { - 0: { // main list - mci: { - VM1: { - focus: true - submit: true - argName: message - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "delete", "d", "shift + d" ] - action: @method:deleteSelected - } - ] - } - 1: { // delete prompt form - submit: { - *: [ - { - value: { promptValue: 0 } - action: @method:deleteMessageYes - } - { - value: { promptValue: 1 } - action: @method:deleteMessageNo - } - ] - } - } - } - } - - //////////////////////////////////////////////////////////////////////// - // File Base - //////////////////////////////////////////////////////////////////////// - - fileBase: { - desc: File Base - art: FMENU - prompt: fileMenuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { menuOption: "L" } - action: @menu:fileBaseListEntries - } - { - value: { menuOption: "B" } - action: @menu:fileBaseBrowseByAreaSelect - } - { - value: { menuOption: "F" } - action: @menu:fileAreaFilterEditor - } - { - value: { menuOption: "Q" } - action: @systemMethod:prevMenu - } - { - value: { menuOption: "G" } - action: @menu:fullLogoffSequence - } - { - value: { menuOption: "D" } - action: @menu:fileBaseDownloadManager - } - { - value: { menuOption: "W" } - action: @menu:fileBaseWebDownloadManager - } - { - value: { menuOption: "U" } - action: @menu:fileBaseUploadFiles - } - { - value: { menuOption: "S" } - action: @menu:fileBaseSearch - } - { - value: { menuOption: "P" } - action: @menu:fileBaseSetNewScanDate - } - { - value: { menuOption: "E" } - action: @menu:fileBaseExportListFilter - } - ] - } - - fileBaseExportListFilter: { - module: file_base_search - art: FBLISTEXPSEARCH - config: { - fileBaseListEntriesMenu: fileBaseExportList - } - form: { - 0: { - mci: { - ET1: { - focus: true - argName: searchTerms - } - BT2: { - argName: search - text: search - submit: true - } - ET3: { - maxLength: 64 - argName: tags - } - SM4: { - maxLength: 64 - argName: areaIndex - } - SM5: { - items: [ - "upload date", - "uploaded by", - "downloads", - "rating", - "estimated year", - "size", - "filename" - ] - argName: sortByIndex - } - SM6: { - items: [ - "decending", - "ascending" - ] - argName: orderByIndex - } - BT7: { - argName: advancedSearch - text: advanced search - submit: true - } - } - - submit: { - *: [ - { - value: { search: null } - action: @method:search - } - { - value: { advancedSearch: null } - action: @method:search - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseExportList: { - module: file_base_user_list_export - art: FBLISTEXP - config: { - pause: true - templates: { - entry: file_list_entry.asc - } - } - form: { - 0: { - mci: { - TL1: { } - TL2: { } - } - } - } - } - - fileBaseExportListNoResults: { - desc: Browsing Files - art: FBNORES - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - fileBaseSetNewScanDate: { - module: set_newscan_date - desc: File Base - art: SETFNSDATE - config: { - target: file - scanDateFormat: YYYYMMDD - } - form: { - 0: { - mci: { - ME1: { - focus: true - submit: true - argName: scanDate - maskPattern: "####/##/##" - } - } - submit: { - *: [ - { - value: { scanDate: null } - action: @method:scanDateSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseListEntries: { - module: file_area_list - desc: Browsing Files - config: { - art: { - browse: FBRWSE - details: FDETAIL - detailsGeneral: FDETGEN - detailsNfo: FDETNFO - detailsFileList: FDETLST - help: FBHELP - } - } - form: { - 0: { - mci: { - MT1: { - mode: preview - } - - HM2: { - focus: true - submit: true - argName: navSelect - items: [ - "prev", "next", "details", "toggle queue", "rate", "change filter", "help", "quit" - ] - focusItemIndex: 1 - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:prevFile - } - { - value: { navSelect: 1 } - action: @method:nextFile - } - { - value: { navSelect: 2 } - action: @method:viewDetails - } - { - value: { navSelect: 3 } - action: @method:toggleQueue - } - { - value: { navSelect: 4 } - action: @menu:fileBaseGetRatingForSelectedEntry - } - { - value: { navSelect: 5 } - action: @menu:fileAreaFilterEditor - } - { - value: { navSelect: 6 } - action: @method:displayHelp - } - { - value: { navSelect: 7 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "w", "shift + w" ] - action: @method:showWebDownloadLink - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "t", "shift + t" ] - action: @method:toggleQueue - } - { - keys: [ "f", "shift + f" ] - action: @menu:fileAreaFilterEditor - } - { - keys: [ "v", "shift + v" ] - action: @method:viewDetails - } - { - keys: [ "r", "shift + r" ] - action: @menu:fileBaseGetRatingForSelectedEntry - } - { - keys: [ "?" ] - action: @method:displayHelp - } - ] - } - - 1: { - mci: { - HM1: { - focus: true - submit: true - argName: navSelect - items: [ - "general", "nfo/readme", "file listing" - ] - } - } - - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @method:detailsQuit - } - ] - } - - 2: { - // details - general - mci: {} - } - - 3: { - // details - nfo/readme - mci: { - MT1: { - mode: preview - } - } - } - - 4: { - // details - file listing - mci: { - VM1: { - - } - } - } - } - } - - fileBaseBrowseByAreaSelect: { - desc: Browsing File Areas - module: file_base_area_select - art: FAREASEL - form: { - 0: { - mci: { - VM1: { - focus: true - argName: areaTag - } - } - - submit: { - *: [ - { - value: { areaTag: null } - action: @method:selectArea - } - ] - } - - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseGetRatingForSelectedEntry: { - desc: Rating a File - prompt: fileBaseRateEntryPrompt - config: { - cls: true - } - submit: [ - // :TODO: handle esc/q - { - // pass data back to caller - value: { rating: null } - action: @systemMethod:prevMenu - } - ] - } - - fileBaseListEntriesNoResults: { - desc: Browsing Files - art: FBNORES - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - fileBaseSearch: { - module: file_base_search - desc: Searching Files - art: FSEARCH - form: { - 0: { - mci: { - ET1: { - focus: true - argName: searchTerms - } - BT2: { - argName: search - text: search - submit: true - } - ET3: { - maxLength: 64 - argName: tags - } - SM4: { - maxLength: 64 - argName: areaIndex - } - SM5: { - items: [ - "upload date", - "uploaded by", - "downloads", - "rating", - "estimated year", - "size", - "filename", - ] - argName: sortByIndex - } - SM6: { - items: [ - "decending", - "ascending" - ] - argName: orderByIndex - } - BT7: { - argName: advancedSearch - text: advanced search - submit: true - } - } - - submit: { - *: [ - { - value: { search: null } - action: @method:search - } - { - value: { advancedSearch: null } - action: @method:search - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileAreaFilterEditor: { - desc: File Filter Editor - module: file_area_filter_edit - art: FFILEDT - form: { - 0: { - mci: { - ET1: { - argName: searchTerms - } - ET2: { - maxLength: 64 - argName: tags - } - SM3: { - maxLength: 64 - argName: areaIndex - } - SM4: { - items: [ - "upload date", - "uploaded by", - "downloads", - "rating", - "estimated year", - "size", - ] - argName: sortByIndex - } - SM5: { - items: [ - "decending", - "ascending" - ] - argName: orderByIndex - } - ET6: { - maxLength: 64 - argName: name - validate: @systemMethod:validateNonEmpty - } - HM7: { - focus: true - items: [ - "prev", "next", "make active", "save", "new", "delete" - ] - argName: navSelect - focusItemIndex: 1 - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:prevFilter - } - { - value: { navSelect: 1 } - action: @method:nextFilter - } - { - value: { navSelect: 2 } - action: @method:makeFilterActive - } - { - value: { navSelect: 3 } - action: @method:saveFilter - } - { - value: { navSelect: 4 } - action: @method:newFilter - } - { - value: { navSelect: 5 } - action: @method:deleteFilter - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseDownloadManager: { - desc: Download Manager - module: file_base_download_manager - config: { - art: { - queueManager: FDLMGR - /* - NYI - details: FDLDET - */ - } - emptyQueueMenu: fileBaseDownloadManagerEmptyQueue - } - form: { - 0: { - mci: { - VM1: { - argName: queueItem - } - HM2: { - focus: true - items: [ "download all", "quit" ] - argName: navSelect - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:downloadAll - } - { - value: { navSelect: 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "a", "shift + a" ] - action: @method:downloadAll - } - { - keys: [ "delete", "r", "shift + r" ] - action: @method:removeItem - } - { - keys: [ "c", "shift + c" ] - action: @method:clearQueue - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseWebDownloadManager: { - desc: Web D/L Manager - module: file_base_web_download_manager - config: { - art: { - queueManager: FWDLMGR - batchList: BATDLINF - } - emptyQueueMenu: fileBaseDownloadManagerEmptyQueue - } - form: { - 0: { - mci: { - VM1: { - argName: queueItem - } - HM2: { - focus: true - items: [ "get batch link", "quit", "help" ] - argName: navSelect - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:getBatchLink - } - { - value: { navSelect: 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "b", "shift + b" ] - action: @method:getBatchLink - } - { - keys: [ "delete", "r", "shift + r" ] - action: @method:removeItem - } - { - keys: [ "c", "shift + c" ] - action: @method:clearQueue - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseDownloadManagerEmptyQueue: { - desc: Empty Download Queue - art: FEMPTYQ - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - fileTransferProtocolSelection: { - desc: Protocol selection - module: file_transfer_protocol_select - art: FPROSEL - form: { - 0: { - mci: { - VM1: { - focus: true - argName: protocol - } - } - - submit: { - *: [ - { - value: { protocol: null } - action: @method:selectProtocol - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseUploadFiles: { - desc: Uploading - module: upload - config: { - interrupt: never - art: { - options: ULOPTS - fileDetails: ULDETAIL - processing: ULCHECK - dupes: ULDUPES - } - } - - form: { - // options - 0: { - mci: { - SM1: { - argName: areaSelect - focus: true - } - TM2: { - argName: uploadType - items: [ "blind", "supply filename" ] - } - ET3: { - argName: fileName - maxLength: 255 - validate: @method:validateNonBlindFileName - } - HM4: { - argName: navSelect - items: [ "continue", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:optionsNavContinue - } - { - value: { navSelect: 1 } - action: @systemMethod:prevMenu - } - ] - } - - "actionKeys" : [ - { - "keys" : [ "escape" ], - action: @systemMethod:prevMenu - } - ] - } - - 1: { - mci: { } - } - - // file details entry - 2: { - mci: { - MT1: { - argName: shortDesc - tabSwitchesView: true - focus: true - } - - ET2: { - argName: tags - } - - ME3: { - argName: estYear - maskPattern: "####" - } - - BT4: { - argName: continue - text: continue - submit: true - } - } - - submit: { - *: [ - { - value: { continue: null } - action: @method:fileDetailsContinue - } - ] - } - } - - // dupes - 3: { - mci: { - VM1: { - /* - Use 'dupeInfoFormat' to custom format: - - areaDesc - areaName - areaTag - desc - descLong - fileId - fileName - fileSha256 - storageTag - uploadTimestamp - - */ - - mode: preview - } - } - } - } - } - - fileBaseNoUploadAreasAvail: { - desc: File Base - art: ULNOAREA - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - sendFilesToUser: { - desc: Downloading - module: file_transfer - config: { - // defaults - generally use extraArgs - protocol: zmodem8kSexyz - direction: send - } - } - - recvFilesFromUser: { - desc: Uploading - module: file_transfer - config: { - // defaults - generally use extraArgs - protocol: zmodem8kSexyz - direction: recv - } - } - - - //////////////////////////////////////////////////////////////////////// - // Required entries - //////////////////////////////////////////////////////////////////////// - idleLogoff: { - art: IDLELOG - next: @systemMethod:logoff - } - //////////////////////////////////////////////////////////////////////// - // Demo Section - // :TODO: This entire section needs updated!!! - //////////////////////////////////////////////////////////////////////// - "demoMain" : { - "art" : "demo_selection_vm.ans", - "form" : { - "0" : { - "VM" : { - "mci" : { - "VM1" : { - "items" : [ - "Single Line Text Editing Views", - "Spinner & Toggle Views", - "Mask Edit Views", - "Multi Line Text Editor", - "Vertical Menu Views", - "Horizontal Menu Views", - "Art Display", - "Full Screen Editor" - ], - "height" : 10, - "itemSpacing" : 1, - "justify" : "center", - "focusTextStyle" : "small i" - } - }, - "submit" : { - "*" : [ - { - "value" : { "1" : 0 }, - "action" : "@menu:demoEditTextView" - }, - { - "value" : { "1" : 1 }, - "action" : "@menu:demoSpinAndToggleView" - }, - { - "value" : { "1" : 2 }, - "action" : "@menu:demoMaskEditView" - }, - { - "value" : { "1" : 3 }, - "action" : "@menu:demoMultiLineEditTextView" - }, - { - "value" : { "1" : 4 }, - "action" : "@menu:demoVerticalMenuView" - }, - { - "value" : { "1" : 5 }, - "action" : "@menu:demoHorizontalMenuView" - }, - { - "value" : { "1" : 6 }, - "action" : "@menu:demoArtDisplay" - }, - { - "value" : { "1" : 7 }, - "action" : "@menu:demoFullScreenEditor" - } - ] - } - } - } - } - }, - "demoEditTextView" : { - "art" : "demo_edit_text_view1.ans", - "form" : { - "0" : { - "BTETETETET" : { - "mci" : { - "ET1" : { - "width" : 20, - "maxLength" : 20 - }, - "ET2" : { - "width" : 20, - "maxLength" : 40, - "textOverflow" : "..." - }, - "ET3" : { - "width" : 20, - "fillChar" : "-", - "styleSGR1" : "|00|36", - "maxLength" : 20 - }, - "ET4" : { - "width" : 20, - "maxLength" : 20, - "password" : true - }, - "BT5" : { - "width" : 8, - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoSpinAndToggleView" : { - "art" : "demo_spin_and_toggle.ans", - "form" : { - "0" : { - "BTSMSMTM" : { - "mci" : { - "SM1" : { - "items" : [ "Henry Morgan", "François l'Ollonais", "Roche Braziliano", "Black Bart", "Blackbeard" ] - }, - "SM2" : { - "items" : [ "Razor 1911", "DrinkOrDie", "TRSI" ] - }, - "TM3" : { - "items" : [ "Yarly", "Nowaii" ], - "styleSGR1" : "|00|30|01", - "hotKeys" : { "Y" : 0, "N" : 1 } - }, - "BT8" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 8, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 8 - } - ] - } - } - } - }, - "demoMaskEditView" : { - "art" : "demo_mask_edit_text_view1.ans", - "form" : { - "0" : { - "BTMEME" : { - "mci" : { - "ME1" : { - "maskPattern" : "##/##/##", - "styleSGR1" : "|00|30|01", - //"styleSGR2" : "|00|45|01", - "styleSGR3" : "|00|30|35", - "fillChar" : "#" - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoMultiLineEditTextView" : { - "art" : "demo_multi_line_edit_text_view1.ans", - "form" : { - "0" : { - "BTMT" : { - "mci" : { - "MT1" : { - "width" : 70, - "height" : 17, - //"text" : "@art:demo_multi_line_edit_text_view_text.txt", - // "text" : "@systemMethod:textFromFile" - text: "Hints:\n\t* Insert / CTRL-V toggles overtype mode\n\t* CTRL-Y deletes the current line\n\t* Try Page Up / Page Down\n\t* Home goes to the start of line text\n\t* End goes to the end of a line\n\n\nTab handling:\n-------------------------------------------------\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\nA0\tBB\t1\tCCC\t2\tDDD\t3EEEE\nW\t\tX\t\tY\t\tZ\n\nAn excerpt from A Clockwork Orange:\n\"What sloochatted then, of course, was that my cellmates woke up and started joining in, tolchocking a bit wild in the near-dark, and the shoom seemed to wake up the whole tier, so that you could slooshy a lot of creeching and banging about with tin mugs on the wall, as though all the plennies in all the cells thought a big break was about to commence, O my brothers.\n", - "focus" : true - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoHorizontalMenuView" : { - "art" : "demo_horizontal_menu_view1.ans", - "form" : { - "0" : { - "BTHMHM" : { - "mci" : { - "HM1" : { - "items" : [ "One", "Two", "Three" ], - "hotKeys" : { "1" : 0, "2" : 1, "3" : 2 } - }, - "HM2" : { - "items" : [ "Uno", "Dos", "Tres" ], - "hotKeys" : { "U" : 0, "D" : 1, "T" : 2 } - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoVerticalMenuView" : { - "art" : "demo_vertical_menu_view1.ans", - "form" : { - "0" : { - "BTVM" : { - "mci" : { - "VM1" : { - "items" : [ - "|33Oblivion/2", - "|33iNiQUiTY", - "|33ViSiON/X" - ], - "focusItems" : [ - "|33Oblivion|01/|00|332", - "|01|33i|00|33N|01i|00|33QU|01i|00|33TY", - "|33ViSiON/X" - ] - // - // :TODO: how to do the following: - // 1) Supply a view a string for a standard vs focused item - // "items" : [...], "focusItems" : [ ... ] ? - // "draw" : "@method:drawItemX", then items: [...] - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - - }, - "demoArtDisplay" : { - "art" : "demo_selection_vm.ans", - "form" : { - "0" : { - "VM" : { - "mci" : { - "VM1" : { - "items" : [ - "Defaults - DOS ANSI", - "bw_mindgames.ans - DOS", - "test.ans - DOS", - "Defaults - Amiga", - "Pause at Term Height" - ], - // :TODO: justify not working?? - "focusTextStyle" : "small i" - } - }, - "submit" : { - "*" : [ - { - "value" : { "1" : 0 }, - "action" : "@menu:demoDefaultsDosAnsi" - }, - { - "value" : { "1" : 1 }, - "action" : "@menu:demoDefaultsDosAnsi_bw_mindgames" - }, - { - "value" : { "1" : 2 }, - "action" : "@menu:demoDefaultsDosAnsi_test" - } - ] - } - } - } - } - }, - "demoDefaultsDosAnsi" : { - "art" : "DM-ENIG2.ANS" - }, - "demoDefaultsDosAnsi_bw_mindgames" : { - "art" : "bw_mindgames.ans" - }, - "demoDefaultsDosAnsi_test" : { - "art" : "test.ans" - }, - "demoFullScreenEditor" : { - "module" : "fse", - "config" : { - "editorType" : "netMail", - "art" : { - "header" : "demo_fse_netmail_header.ans", - "body" : "demo_fse_netmail_body.ans", - "footerEditor" : "demo_fse_netmail_footer_edit.ans", - "footerEditorMenu" : "demo_fse_netmail_footer_edit_menu.ans", - "footerView" : "demo_fse_netmail_footer_view.ans", - "help" : "demo_fse_netmail_help.ans" - } - }, - "form" : { - "0" : { - "ETETET" : { - "mci" : { - "ET1" : { - // :TODO: from/to may be set by args - // :TODO: focus may change dep on view vs edit - "width" : 36, - "focus" : true, - "argName" : "to" - }, - "ET2" : { - "width" : 36, - "argName" : "from" - }, - "ET3" : { - "width" : 65, - "maxLength" : 72, - "submit" : [ "enter" ], - "argName" : "subject" - } - }, - "submit" : { - "3" : [ - { - "value" : { "subject" : null }, - "action" : "@method:headerSubmit" - } - ] - } - } - }, - "1" : { - "MT" : { - "mci" : { - "MT1" : { - "width" : 79, - "height" : 17, - "text" : "", // :TODO: should not be req. - "argName" : "message" - } - }, - "submit" : { - "*" : [ - { - "value" : "message", - "action" : "@method:editModeEscPressed" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 1 - } - ] - } - }, - "2" : { - "TLTL" : { - "mci" : { - "TL1" : { - "width" : 5 - }, - "TL2" : { - "width" : 4 - } - } - } - }, - "3" : { - "HM" : { - "mci" : { - "HM1" : { - // :TODO: Continue, Save, Discard, Clear, Quote, Help - "items" : [ "Save", "Discard", "Quote", "Help" ] - } - }, - "submit" : { - "*" : [ - { - "value" : { "1" : 0 }, - "action" : "@method:editModeMenuSave" - }, - { - "value" : { "1" : 1 }, - "action" : "@menu:demoMain" - }, - { - "value" : { "1" : 2 }, - "action" : "@method:editModeMenuQuote" - }, - { - "value" : { "1" : 3 }, - "action" : "@method:editModeMenuHelp" - }, - { - "value" : 1, - "action" : "@method:editModeEscPressed" - } - ] - }, - "actionKeys" : [ // :TODO: Need better name - { - "keys" : [ "escape" ], - "action" : "@method:editModeEscPressed" - } - ] - } - } - } - } - } -} diff --git a/misc/menu_templates/doors.in.hjson b/misc/menu_templates/doors.in.hjson new file mode 100644 index 00000000..c26ee114 --- /dev/null +++ b/misc/menu_templates/doors.in.hjson @@ -0,0 +1,132 @@ +{ + menus: { + doorsMainMenu: { + desc: Doors Menu + art: DOORMNU + prompt: menuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { command: "G" } + action: @menu:fullLogoffSequence + } + { + value: { command: "Q" } + action: @systemMethod:prevMenu + } + // + // The system supports many ways of launching doors including + // modules for DoorParty!, BBSLink, etc. + // + // Below are some examples. See the documentation for more info. + // + { + value: { command: "ABRACADABRA" } + action: @menu:doorAbracadabraExample + } + { + value: { command: "TWBBSLINK" } + action: @menu:doorTradeWars2002BBSLinkExample + } + { + value: { command: "DP" } + action: @menu:doorPartyExample + } + { + value: { command: "CN" } + action: @menu:doorCombatNetExample + } + { + value: { command: "EXODUS" } + action: @menu:doorExodusCataclysm + } + ] + } + + // + // Local Door Example via abracadabra module + // + // This example assumes launch_door.sh (which is passed args) + // launches the door. + // + doorAbracadabraExample: { + desc: Abracadabra Example + module: abracadabra + config: { + name: Example Door + dropFileType: DORINFO + cmd: /home/enigma/DOS/scripts/launch_door.sh + args: [ + "{node}", + "{dropFile}", + "{srvPort}", + ], + nodeMax: 1 + tooManyArt: DOORMANY + io: socket + } + } + + // + // BBSLink Example (TradeWars 2000) + // + // Register @ https://bbslink.net/ + // + doorTradeWars2002BBSLinkExample: { + desc: Playing TW 2002 (BBSLink) + module: bbs_link + config: { + sysCode: XXXXXXXX + authCode: XXXXXXXX + schemeCode: XXXXXXXX + door: tw + } + } + + // + // DoorParty! Example + // + // Register @ http://throwbackbbs.com/ + // + doorPartyExample: { + desc: Using DoorParty! + module: door_party + config: { + username: XXXXXXXX + password: XXXXXXXX + bbsTag: XX + } + } + + // + // CombatNet Example + // + // Register @ http://combatnet.us/ + // + doorCombatNetExample: { + desc: Using CombatNet + module: combatnet + config: { + bbsTag: CBNxxx + password: XXXXXXXXX + } + } + + // + // Exodus Example (cataclysm) + // Register @ https://oddnetwork.org/exodus/ + // + doorExodusCataclysm: { + desc: Cataclysm + module: exodus + config: { + rejectUnauthorized: false + board: XXX + key: XXXXXXXX + door: cataclysm + } + } + } +} \ No newline at end of file diff --git a/misc/menu_templates/file_base.in.hjson b/misc/menu_templates/file_base.in.hjson new file mode 100644 index 00000000..572abf97 --- /dev/null +++ b/misc/menu_templates/file_base.in.hjson @@ -0,0 +1,918 @@ +{ + menus: { + fileBaseMainMenu: { + desc: File Base + art: FMENU + prompt: fileMenuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { menuOption: "L" } + action: @menu:fileBaseListEntries + } + { + value: { menuOption: "B" } + action: @menu:fileBaseBrowseByAreaSelect + } + { + value: { menuOption: "F" } + action: @menu:fileBaseFilterEditor + } + { + value: { menuOption: "Q" } + action: @systemMethod:prevMenu + } + { + value: { menuOption: "G" } + action: @menu:fullLogoffSequence + } + { + value: { menuOption: "D" } + action: @menu:fileBaseDownloadManager + } + { + value: { menuOption: "W" } + action: @menu:fileBaseWebDownloadManager + } + { + value: { menuOption: "U" } + action: @menu:fileBaseUploadFiles + } + { + value: { menuOption: "S" } + action: @menu:fileBaseSearch + } + { + value: { menuOption: "P" } + action: @menu:fileBaseSetNewScanDate + } + { + value: { menuOption: "E" } + action: @menu:fileBaseExportListFilter + } + ] + } + + fileBaseListEntries: { + module: file_area_list + desc: Browsing Files + config: { + art: { + browse: FBRWSE + details: FDETAIL + detailsGeneral: FDETGEN + detailsNfo: FDETNFO + detailsFileList: FDETLST + help: FBHELP + } + } + form: { + 0: { + mci: { + MT1: { + mode: preview + } + + HM2: { + focus: true + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "change filter", "help", "quit" + ] + focusItemIndex: 1 + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:prevFile + } + { + value: { navSelect: 1 } + action: @method:nextFile + } + { + value: { navSelect: 2 } + action: @method:viewDetails + } + { + value: { navSelect: 3 } + action: @method:toggleQueue + } + { + value: { navSelect: 4 } + action: @menu:fileBaseGetRatingForSelectedEntry + } + { + value: { navSelect: 5 } + action: @menu:fileBaseFilterEditor + } + { + value: { navSelect: 6 } + action: @method:displayHelp + } + { + value: { navSelect: 7 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "w", "shift + w" ] + action: @method:showWebDownloadLink + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "t", "shift + t" ] + action: @method:toggleQueue + } + { + keys: [ "f", "shift + f" ] + action: @menu:fileBaseFilterEditor + } + { + keys: [ "v", "shift + v" ] + action: @method:viewDetails + } + { + keys: [ "r", "shift + r" ] + action: @menu:fileBaseGetRatingForSelectedEntry + } + { + keys: [ "?" ] + action: @method:displayHelp + } + ] + } + + 1: { + mci: { + HM1: { + focus: true + submit: true + argName: navSelect + items: [ + "general", "nfo/readme", "file listing" + ] + } + } + + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @method:detailsQuit + } + ] + } + + 2: { + // details - general + mci: {} + } + + 3: { + // details - nfo/readme + mci: { + MT1: { + mode: preview + } + } + } + + 4: { + // details - file listing + mci: { + VM1: { + + } + } + } + } + } + + fileBaseBrowseByAreaSelect: { + desc: Browsing File Areas + module: file_base_area_select + art: FAREASEL + form: { + 0: { + mci: { + VM1: { + focus: true + argName: areaTag + } + } + + submit: { + *: [ + { + value: { areaTag: null } + action: @method:selectArea + } + ] + } + + actionKeys: @reference:common.quitToPrev + } + } + } + + fileBaseFilterEditor: { + desc: File Filter Editor + module: file_area_filter_edit + art: FFILEDT + form: { + 0: { + mci: { + ET1: { + argName: searchTerms + } + ET2: { + maxLength: 64 + argName: tags + } + SM3: { + maxLength: 64 + argName: areaIndex + } + SM4: { + items: [ + "upload date", + "uploaded by", + "downloads", + "rating", + "estimated year", + "size", + ] + argName: sortByIndex + } + SM5: { + items: [ + "decending", + "ascending" + ] + argName: orderByIndex + } + ET6: { + maxLength: 64 + argName: name + validate: @systemMethod:validateNonEmpty + } + HM7: { + focus: true + items: [ + "prev", "next", "make active", "save", "new", "delete" + ] + argName: navSelect + focusItemIndex: 1 + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:prevFilter + } + { + value: { navSelect: 1 } + action: @method:nextFilter + } + { + value: { navSelect: 2 } + action: @method:makeFilterActive + } + { + value: { navSelect: 3 } + action: @method:saveFilter + } + { + value: { navSelect: 4 } + action: @method:newFilter + } + { + value: { navSelect: 5 } + action: @method:deleteFilter + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseDownloadManager: { + desc: Download Manager + module: file_base_download_manager + config: { + art: { + queueManager: FDLMGR + /* + NYI + details: FDLDET + */ + } + emptyQueueMenu: fileBaseDownloadManagerEmptyQueue + } + form: { + 0: { + mci: { + VM1: { + argName: queueItem + } + HM2: { + focus: true + items: [ "download all", "quit" ] + argName: navSelect + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:downloadAll + } + { + value: { navSelect: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "a", "shift + a" ] + action: @method:downloadAll + } + { + keys: [ "delete", "r", "shift + r" ] + action: @method:removeItem + } + { + keys: [ "c", "shift + c" ] + action: @method:clearQueue + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseDownloadManagerEmptyQueue: { + desc: Empty Download Queue + art: FEMPTYQ + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + fileBaseWebDownloadManager: { + desc: Web D/L Manager + module: file_base_web_download_manager + config: { + art: { + queueManager: FWDLMGR + batchList: BATDLINF + } + emptyQueueMenu: fileBaseDownloadManagerEmptyQueue + } + form: { + 0: { + mci: { + VM1: { + argName: queueItem + } + HM2: { + focus: true + items: [ "get batch link", "quit", "help" ] + argName: navSelect + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:getBatchLink + } + { + value: { navSelect: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "b", "shift + b" ] + action: @method:getBatchLink + } + { + keys: [ "delete", "r", "shift + r" ] + action: @method:removeItem + } + { + keys: [ "c", "shift + c" ] + action: @method:clearQueue + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseUploadFiles: { + desc: Uploading + module: upload + config: { + interrupt: never + art: { + options: ULOPTS + fileDetails: ULDETAIL + processing: ULCHECK + dupes: ULDUPES + } + } + + form: { + // options + 0: { + mci: { + SM1: { + argName: areaSelect + focus: true + } + TM2: { + argName: uploadType + items: [ "blind", "supply filename" ] + } + ET3: { + argName: fileName + maxLength: 255 + validate: @method:validateNonBlindFileName + } + HM4: { + argName: navSelect + items: [ "continue", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:optionsNavContinue + } + { + value: { navSelect: 1 } + action: @systemMethod:prevMenu + } + ] + } + + "actionKeys" : [ + { + "keys" : [ "escape" ], + action: @systemMethod:prevMenu + } + ] + } + + 1: { + mci: { } + } + + // file details entry + 2: { + mci: { + MT1: { + argName: shortDesc + tabSwitchesView: true + focus: true + } + + ET2: { + argName: tags + } + + ME3: { + argName: estYear + maskPattern: "####" + } + + BT4: { + argName: continue + text: continue + submit: true + } + } + + submit: { + *: [ + { + value: { continue: null } + action: @method:fileDetailsContinue + } + ] + } + } + + // dupes + 3: { + mci: { + VM1: { + /* + Use 'dupeInfoFormat' to custom format: + + areaDesc + areaName + areaTag + desc + descLong + fileId + fileName + fileSha256 + storageTag + uploadTimestamp + + */ + + mode: preview + } + } + } + } + } + + fileBaseSearch: { + module: file_base_search + desc: Searching Files + art: FSEARCH + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + ET3: { + maxLength: 64 + argName: tags + } + SM4: { + maxLength: 64 + argName: areaIndex + } + SM5: { + items: [ + "upload date", + "uploaded by", + "downloads", + "rating", + "estimated year", + "size", + "filename", + ] + argName: sortByIndex + } + SM6: { + items: [ + "decending", + "ascending" + ] + argName: orderByIndex + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseSetNewScanDate: { + module: set_newscan_date + desc: File Base + art: SETFNSDATE + config: { + target: file + scanDateFormat: YYYYMMDD + } + form: { + 0: { + mci: { + ME1: { + focus: true + submit: true + argName: scanDate + maskPattern: "####/##/##" + } + } + submit: { + *: [ + { + value: { scanDate: null } + action: @method:scanDateSubmit + } + ] + } + actionKeys: @reference:common.quitToPrev + } + } + } + + fileBaseExportListFilter: { + module: file_base_search + art: FBLISTEXPSEARCH + config: { + fileBaseListEntriesMenu: fileBaseExportList + } + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + ET3: { + maxLength: 64 + argName: tags + } + SM4: { + maxLength: 64 + argName: areaIndex + } + SM5: { + items: [ + "upload date", + "uploaded by", + "downloads", + "rating", + "estimated year", + "size", + "filename" + ] + argName: sortByIndex + } + SM6: { + items: [ + "decending", + "ascending" + ] + argName: orderByIndex + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseExportList: { + module: file_base_user_list_export + art: FBLISTEXP + config: { + pause: true + templates: { + entry: file_list_entry.asc + } + } + form: { + 0: { + mci: { + TL1: { } + TL2: { } + } + } + } + } + + fileBaseExportListNoResults: { + desc: Browsing Files + art: FBNORES + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + // Referenced by various menus + fileBaseGetRatingForSelectedEntry: { + desc: Rating a File + prompt: fileBaseRateEntryPrompt + config: { + cls: true + } + submit: [ + // :TODO: handle esc/q + { + // pass data back to caller + value: { rating: null } + action: @systemMethod:prevMenu + } + ] + } + + // default menu entry used by the 'file_area_list' module + // when there are no search results for the provided criteria + fileBaseListEntriesNoResults: { + desc: Browsing Files + art: FBNORES + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + // default menu entry used by the 'file_base_download_manager' module + // for protocol selection + fileTransferProtocolSelection: { + desc: Protocol selection + module: file_transfer_protocol_select + art: FPROSEL + form: { + 0: { + mci: { + VM1: { + focus: true + argName: protocol + } + } + + submit: { + *: [ + { + value: { protocol: null } + action: @method:selectProtocol + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + // default menu entry used by the 'upload' module for when + // no areas are available for the user to upload to + fileBaseNoUploadAreasAvail: { + desc: File Base + art: ULNOAREA + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + // default menu entry used by the 'file_transfer_protocol_select' module + // when performing user downloads + sendFilesToUser: { + desc: Downloading + module: file_transfer + config: { + // defaults - generally use extraArgs + protocol: zmodem8kSexyz + direction: send + } + } + + // default menu entry used by the 'file_transfer_protocol_select' module + // when performing user uploads + recvFilesFromUser: { + desc: Uploading + module: file_transfer + config: { + // defaults - generally use extraArgs + protocol: zmodem8kSexyz + direction: recv + } + } + } + + prompts: { + fileMenuCommand: { + art: FILPMPT + mci: { + TL1: {} + ET2: { + argName: menuOption + width: 20 + maxLength: 20 + textStyle: upper + focus: true + } + } + } + + fileBaseRateEntryPrompt: { + art: RATEFILE + mci: { + SM1: { + argName: rating + items: [ "-----", "*----", "**---", "***--", "****-", "*****" ] + } + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + + fileBaseTagEntryPrompt: { + art: TAGFILE + mci: { + ET1: { + argName: tags + } + } + } + } +} \ No newline at end of file diff --git a/misc/menu_templates/login.in.hjson b/misc/menu_templates/login.in.hjson new file mode 100644 index 00000000..bcee88e2 --- /dev/null +++ b/misc/menu_templates/login.in.hjson @@ -0,0 +1,607 @@ +{ + menus: { + // + // Send telnet connections to matrix where users can login, apply, etc. + // + telnetConnected: { + art: CONNECT + next: matrix + config: { nextTimeout: 1500 } + } + + // + // SSH connections are pre-authenticated via the SSH server itself. + // Jump directly to either the 2FA/OTP auth or the login sequence + // depending on user ACS. + // + sshConnected: { + art: CONNECT + next: [ + { + acs: AR2 + next: loginTwoFactorAuthOTPLoop + } + { + next: fullLoginSequenceLoginArt + } + ] + config: { nextTimeout: 1500 } + } + + // + // Another SSH specialization: If the user logs in with a new user + // name (e.g. "new", "apply", ...) they will be directed to the + // application process. + // + sshConnectedNewUser: { + art: CONNECT + next: newUserApplicationPreSsh + config: { nextTimeout: 1500 } + } + + // Ye ol' standard matrix + matrix: { + art: matrix + form: { + 0: { + VM: { + mci: { + VM1: { + submit: true + focus: true + argName: navSelect + items: [ + { + text: login + data: login + } + { + text: apply + data: apply + } + + // + // To enable the forgot password option, you'll need to have + // the web server & email configured. Once that is in place, + // uncomment the section below. + // + // See docs for more information + // + /* + { + text: forgot pass + data: forgot + } + */ + { + text: log off + data: logoff + } + ] + } + } + submit: { + *: [ + { + value: { navSelect: "login" } + action: @menu:login + } + { + value: { navSelect: "apply" } + action: @menu:newUserApplicationPre + } + { + value: { navSelect: "forgot" } + action: @menu:forgotPassword + } + { + value: { navSelect: "logoff" } + action: @menu:logoff + } + ] + } + } + } + } + } + + login: { + art: USERLOG + next: [ + { + // + // Users with 2FA/OTP enabled *must* go through + // an additional OTP authentication step + // + acs: AR2 + next: loginTwoFactorAuthOTPLoop + } + { + // ...everyone else can carry on as per usual + next: fullLoginSequenceLoginArt + } + ] + config: { + tooNodeMenu: loginAttemptTooNode + inactive: loginAttemptAccountInactive + disabled: loginAttemptAccountDisabled + locked: loginAttemptAccountLocked + } + form: { + 0: { + mci: { + ET1: { + maxLength: @config:users.usernameMax + argName: username + focus: true + } + ET2: { + password: true + maxLength: @config:users.passwordMax + argName: password + submit: true + } + } + submit: { + *: [ + { + value: { password: null } + action: @systemMethod:login + } + ] + } + actionKeys: @reference:common.escToPrev + } + } + } + + loginAttemptTooNode: { + art: TOONODE + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountLocked: { + art: ACCOUNTLOCKED + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountDisabled: { + art: ACCOUNTDISABLED + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountInactive: { + art: ACCOUNTINACTIVE + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + forgotPassword: { + desc: Forgot password + prompt: forgotPasswordPrompt + submit: [ + { + value: { username: null } + action: @systemMethod:sendForgotPasswordEmail + extraArgs: { next: "forgotPasswordSubmitted" } + } + ] + } + + forgotPasswordSubmitted: { + desc: Forgot password + art: FORGOTPWSENT + config: { + cls: true + pause: true + } + next: @systemMethod:logoff + } + + fullLoginSequenceLoginArt: { + desc: Logging In + art: WELCOME + config: { pause: true } + next: fullLoginSequenceLastCallers + } + + fullLoginSequenceLastCallers: { + desc: Last Callers + module: last_callers + art: LASTCALL + config: { + pause: true + font: cp437 + } + next: fullLoginSequenceWhosOnline + } + + fullLoginSequenceWhosOnline: { + desc: Who's Online + module: whos_online + art: WHOSON + config: { pause: true } + next: fullLoginSequenceOnelinerz + } + + fullLoginSequenceOnelinerz: { + desc: Viewing Onelinerz + module: onelinerz + next: [ + { + // calls >= 2 + acs: NC2 + next: fullLoginSequenceNewScanConfirm + } + { + // new users - skip new scan + next: fullLoginSequenceUserStats + } + ] + config: { + cls: true + art: { + view: ONELINER + add: ONEADD + } + } + form: { + 0: { + mci: { + VM1: { + focus: false + height: 10 + } + TM2: { + argName: addOrExit + items: [ "yeah!", "nah" ] + "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } + submit: true + focus: true + } + } + submit: { + *: [ + { + value: { addOrExit: 0 } + action: @method:viewAddScreen + } + { + value: { addOrExit: null } + action: @systemMethod:nextMenu + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:nextMenu + } + ] + }, + 1: { + mci: { + ET1: { + focus: true + maxLength: 70 + argName: oneliner + } + TL2: { + width: 60 + } + TM3: { + argName: addOrCancel + items: [ "add", "cancel" ] + "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } + submit: true + } + } + + submit: { + *: [ + { + value: { addOrCancel: 0 } + action: @method:addEntry + } + { + value: { addOrCancel: 1 } + action: @method:cancelAdd + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelAdd + } + ] + } + } + } + + fullLoginSequenceNewScanConfirm: { + desc: Logging In + prompt: loginGlobalNewScan + submit: [ + { + value: { promptValue: 0 } + action: @menu:fullLoginSequenceNewScan + } + { + value: { promptValue: 1 } + action: @menu:fullLoginSequenceUserStats + } + ] + } + + fullLoginSequenceNewScan: { + desc: Performing New Scan + module: new_scan + art: NEWSCAN + next: fullLoginSequenceSysStats + config: { + messageListMenu: newScanMessageList + } + } + + newScanMessageList: { + desc: New Messages + module: msg_list + art: NEWMSGS + config: { + menuViewPost: messageBaseViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + TL6: { } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "x", "shift + x" ] + action: @method:fullExit + } + { + keys: [ "m", "shift + m" ] + action: @method:markAllRead + } + ] + } + } + } + + newScanFileBaseList: { + module: file_area_list + desc: New Files + config: { + art: { + browse: FNEWBRWSE + details: FDETAIL + detailsGeneral: FDETGEN + detailsNfo: FDETNFO + detailsFileList: FDETLST + help: FBHELP + } + } + form: { + 0: { + mci: { + MT1: { + mode: preview + ansiView: true + } + + HM2: { + focus: true + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "help", "quit" + ] + focusItemIndex: 1 + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:prevFile + } + { + value: { navSelect: 1 } + action: @method:nextFile + } + { + value: { navSelect: 2 } + action: @method:viewDetails + } + { + value: { navSelect: 3 } + action: @method:toggleQueue + } + { + value: { navSelect: 4 } + action: @menu:fileBaseGetRatingForSelectedEntry + } + { + value: { navSelect: 5 } + action: @method:displayHelp + } + { + value: { navSelect: 6 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "w", "shift + w" ] + action: @method:showWebDownloadLink + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "t", "shift + t" ] + action: @method:toggleQueue + } + { + keys: [ "v", "shift + v" ] + action: @method:viewDetails + } + { + keys: [ "r", "shift + r" ] + action: @menu:fileBaseGetRatingForSelectedEntry + } + { + keys: [ "?" ] + action: @method:displayHelp + } + ] + } + + 1: { + mci: { + HM1: { + focus: true + submit: true + argName: navSelect + items: [ + "general", "nfo/readme", "file listing" + ] + } + } + + actionKeys: @reference:common.quitToPrev + } + + 2: { + // details - general + mci: { } + } + + 3: { + // details - nfo/readme + mci: { + MT1: { + mode: preview + } + } + } + + 4: { + // details - file listing + mci: { + VM1: { } + } + } + } + } + + fullLoginSequenceSysStats: { + desc: System Stats + art: SYSSTAT + config: { pause: true } + next: fullLoginSequenceUserStats + } + + fullLoginSequenceUserStats: { + desc: User Stats + art: STATUS + config: { pause: true } + next: mainMenu + } + + // + // Empty menu to catch us in a 2FA/OTP auth loop + // until the user either authenticates successfully + // or the system boots them. + // + loginTwoFactorAuthOTPLoop: { + next: loginTwoFactorAuthOTP + } + + loginTwoFactorAuthOTP: { + art: 2FAOTP + next: fullLoginSequenceLoginArt + form: { + 0: { + mci: { + ET1: { + argName: token + focus: true + submit: true + } + } + submit: { + *: [ + { + value: { token: null } + action: @systemMethod:login2FA_OTP + } + ] + } + actionKeys: [ + { + // no turning back at this point... + keys: [ "escape" ] + action: @systemMethod:logoff + } + ] + } + } + } + + } + + prompts: { + loginGlobalNewScan: { + art: GNSPMPT + mci: { + TM1: { + argName: promptValue + items: [ "yes", "no" ] + focus: true + hotKeys: { Y: 0, N: 1 } + hotKeySubmit: true + } + } + } + + } +} \ No newline at end of file diff --git a/misc/menu_templates/main.in.hjson b/misc/menu_templates/main.in.hjson new file mode 100644 index 00000000..ac4b1771 --- /dev/null +++ b/misc/menu_templates/main.in.hjson @@ -0,0 +1,1024 @@ +{ + /* + ./\/\.' ENiGMA½ Menu Configuration -/--/-------- - -- - + + _____________________ _____ ____________________ __________\_ / + \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! + // __|___// | \// |// | \// | | \// \ /___ /_____ + /____ _____| __________ ___|__| ____| \ / _____ \ + ---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ + /__ _\ + <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ + + *-----------------------------------------------------------------------------* + + General Information + ------------------------------- - - + This configuration is in HJSON (http://hjson.org/) format. Strict to-spec + JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. + + See http://hjson.org/ for more information and syntax. + + Various editors and IDEs such as Sublime Text 3, Visual Studio Code, and so + on have syntax highlighting for the HJSON format which are highly recommended. + + ------------------------------- -- - - + Menu Configuration + ------------------------------- - - + ENiGMA½ makes no assumptions about specific menu types (main, doors, etc.), + but instead allows full customization of all menus throughout the system. + Some menus such as a main menu are considered "standard" while others are + backed by a specific module. SysOps can tweak various settings about these + modules (look & feel, keyboard interation, and so on) or even fully replace + the module with something else. + + This file starts out as an example setup. Look at the examples, change + settings, menu ordering/flow, add/remove menus, implement ACS control, + etc.! + + Remember you can *live edit* this file. That is, make a change and save + while you're logged into the system and it will take effect on the next + menu change or screen refresh. + + Please see RTFM ...er, uh... see the documentation for more information, and + don't be shy to ask for help: + + BBS : Xibalba @ xibalba.l33t.codes + FTN : BBS Discussion on fsxNet + IRC : #enigma-bbs / FreeNode + Email : bryan@l33t.codes + */ + + // You may include as many additional fragments as you like here. + includes: [ + XXXXXXXX + ] + + // This section creates common fragments to use as @reference + common: { + escToPrev: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + + quitToPrev: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + + menus: { + /////////////////////////////////////////////////////////////////////// + // Main Menu + /////////////////////////////////////////////////////////////////////// + mainMenu: { + art: MMENU + desc: Main Menu + prompt: menuCommand + config: { + font: cp437 + interrupt: realtime + } + submit: [ + { + value: { command: "MSG" } + action: @menu:nodeMessage + } + { + value: { command: "G" } + action: @menu:fullLogoffSequence + } + { + value: { command: "D" } + action: @menu:doorsMainMenu + } + { + value: { command: "F" } + action: @menu:fileBaseMainMenu + } + { + value: { command: "U" } + action: @menu:mainMenuUserList + } + { + value: { command: "L" } + action: @menu:mainMenuLastCallers + } + { + value: { command: "W" } + action: @menu:mainMenuWhosOnline + } + { + value: { command: "Y" } + action: @menu:mainMenuUserStats + } + { + value: { command: "M" } + action: @menu:messageBaseMainMenu + } + { + value: { command: "E" } + action: @menu:privateMailMenu + } + { + value: { command: "C" } + action: @menu:mainMenuUserConfig + } + { + value: { command: "S" } + action: @menu:mainMenuSystemStats + } + { + value: { command: "!" } + action: @menu:mainMenuGlobalNewScan + } + { + value: { command: "K" } + action: @menu:mainMenuFeedbackToSysOp + } + { + value: { command: "O" } + action: @menu:mainMenuOnelinerz + } + { + value: { command: "R" } + action: @menu:mainMenuRumorz + } + { + value: { command: "BBS"} + action: @menu:bbsList + } + { + value: { command: "UA" } + action: @menu:mainMenuUserAchievementsEarned + } + { + value: { command: "MRC" } + action: @menu:mrc + } + { + value: { command: "2FA" } + action: [ + { + // + // For security reasons, only allow 2FA/OTP to be + // configured over already secure (SSL, wss://, ...) + // connections. Not doing so risks leaking secrets! + // + acs: SC + action: @menu:userTwoFactorAuthOTPConfig + } + { + action: @menu:userTwoFactorAuthOTPSecConnRequired + } + ] + } + { + value: 1 + action: @menu:mainMenu + } + ] + } + + mainMenuUserAchievementsEarned: { + desc: Achievements + module: user_achievements_earned + art: USERACHIEV + form: { + 0: { + mci: { + VM1: { + focus: true + } + } + actionKeys: @reference:common.quitToPrev + } + } + } + + mrc: { + desc: MRC Chat + module: mrc + art: MRC + config: { + cls: true + + // max lines kept in scrollback buffer + maxScrollbackLines: 500 + } + form: { + 0: { + mci: { + MT1: { + mode: preview + autoScroll: true + } + ET2: { + argName: inputArea + submit: true + focus: true + } + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + { + keys: [ "down arrow", "up arrow", "page up", "page down" ] + action: @method:movementKeyPressed + } + ] + submit: { + *: [ + { + value: { inputArea: null } + action: @method:sendChatMessage + } + ] + } + } + } + } + + userTwoFactorAuthOTPConfig: { + desc: 2FA/OTP Config + module: user_2fa_otp_config + art: 2FACONFSCR + form: { + 0: { + mci: { + TM1: { + argName: enableToggle + focus: true + items: [ + // order is important here: + "disable" + "enable/reset" + ] + } + SM2: { + argName: otpType + items: [ + // order is important here: + "Time-Based - TOTP" + "HMAC-Based - HOTP" + "Google Authenticator" + ] + } + TM3: { + argName: submit + items: [ + "save" + "cancel" + ] + submit: true + } + } + + submit: { + *: [ + { + value: { submit: 0 } + action: @method:saveChanges + } + { + value: { submit: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + { + keys: [ "q", "shift + q" ] + action: @method:showQRCode + } + { + keys: [ "s", "shift + s" ] + action: @method:showSecret + } + { + keys: [ "b", "shift + b" ] + action: @method:showBackupCodes + } + { + keys: [ "n", "shift + n" ] + action: @method:generateNewBackupCodes + } + ] + } + } + } + + userTwoFactorAuthOTPSecConnRequired: { + desc: Insecure Warning + art: 2FAOTPSECREQ + config: { + cls: true + pause: true + } + } + + userTwoFactorAuthOTPConfigShowDetails: { + desc: 2FA/OTP Details + module: show_art + config: { + pause: true + method: extraArgs + } + } + + nodeMessage: { + desc: Node Messaging + module: node_msg + art: NODEMSG + config: { + cls: true + art: { + header: NODEMSGHDR + footer: NODEMSGFTR + } + } + form: { + 0: { + mci: { + SM1: { + argName: node + } + ET2: { + argName: message + submit: true + } + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + submit: { + *: [ + { + value: { message: null } + action: @method:sendMessage + } + ] + } + } + } + } + + mainMenuLastCallers: { + desc: Last Callers + module: last_callers + art: LASTCALL + config: { pause: true } + } + + mainMenuWhosOnline: { + desc: Who's Online + module: whos_online + art: WHOSON + config: { pause: true } + } + + mainMenuUserStats: { + desc: User Stats + art: STATUS + config: { pause: true } + } + + mainMenuSystemStats: { + desc: System Stats + art: SYSSTAT + config: { pause: true } + } + + mainMenuUserList: { + desc: User Listing + module: user_list + art: USERLST + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + } + } + actionKeys: @reference:common.quitToPrev + } + } + } + + mainMenuUserConfig: { + module: user_config + art: CONFSCR + form: { + 0: { + mci: { + ET1: { + argName: realName + maxLength: @config:users.realNameMax + validate: @systemMethod:validateNonEmpty + focus: true + } + ME2: { + argName: birthdate + maskPattern: "####/##/##" + } + ME3: { + argName: sex + maskPattern: A + textStyle: upper + validate: @systemMethod:validateNonEmpty + } + ET4: { + argName: location + maxLength: @config:users.locationMax + validate: @systemMethod:validateNonEmpty + } + ET5: { + argName: affils + maxLength: @config:users.affilsMax + } + ET6: { + argName: email + maxLength: @config:users.emailMax + validate: @method:validateEmailAvail + } + ET7: { + argName: web + maxLength: @config:users.webMax + } + ME8: { + maskPattern: "##" + argName: termHeight + validate: @systemMethod:validateNonEmpty + } + SM9: { + argName: theme + } + ET10: { + argName: password + maxLength: @config:users.passwordMax + password: true + validate: @method:validatePassword + } + ET11: { + argName: passwordConfirm + maxLength: @config:users.passwordMax + password: true + validate: @method:validatePassConfirmMatch + } + TM25: { + argName: submission + items: [ "save", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { submission: 0 } + action: @method:saveChanges + } + { + value: { submission: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + mainMenuGlobalNewScan: { + desc: Performing New Scan + module: new_scan + art: NEWSCAN + config: { + messageListMenu: newScanMessageList + } + } + + mainMenuFeedbackToSysOp: { + desc: Feedback to SysOp + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + }, + editorMode: edit + editorType: email + messageAreaTag: private_mail + toUserId: 1 /* always to +op */ + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + text: @sysStat:sysop_username + maxLength: 36 + // :TODO: readOnly: true + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateMessageSubject + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { value: "message", action: "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + }, + 2: { + TLTL: { + mci: { + TL1: { + width: 5 + } + TL2: { + width: 4 + } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + items: [ "save", "discard", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + + mainMenuOnelinerz: { + desc: Viewing Onelinerz + module: onelinerz + config: { + cls: true + art: { + view: ONELINER + add: ONEADD + } + } + form: { + 0: { + mci: { + VM1: { + focus: false + height: 10 + } + TM2: { + argName: addOrExit + items: [ "yeah!", "nah" ] + "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } + submit: true + focus: true + } + } + submit: { + *: [ + { + value: { addOrExit: 0 } + action: @method:viewAddScreen + } + { + value: { addOrExit: null } + action: @systemMethod:nextMenu + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:nextMenu + } + ] + }, + 1: { + mci: { + ET1: { + focus: true + maxLength: 70 + argName: oneliner + } + TL2: { + width: 60 + } + TM3: { + argName: addOrCancel + items: [ "add", "cancel" ] + "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } + submit: true + } + } + + submit: { + *: [ + { + value: { addOrCancel: 0 } + action: @method:addEntry + } + { + value: { addOrCancel: 1 } + action: @method:cancelAdd + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelAdd + } + ] + } + } + } + + mainMenuRumorz: { + desc: Rumorz + module: rumorz + config: { + cls: true + art: { + entries: RUMORS + add: RUMORADD + } + } + form: { + 0: { + mci: { + VM1: { + focus: false + height: 10 + } + TM2: { + argName: addOrExit + items: [ "yeah!", "nah" ] + "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } + submit: true + focus: true + } + } + submit: { + *: [ + { + value: { addOrExit: 0 } + action: @method:viewAddScreen + } + { + value: { addOrExit: null } + action: @systemMethod:nextMenu + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:nextMenu + } + ] + }, + 1: { + mci: { + ET1: { + focus: true + maxLength: 70 + argName: rumor + } + TL2: { + width: 60 + } + TM3: { + argName: addOrCancel + items: [ "add", "cancel" ] + "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } + submit: true + } + } + + submit: { + *: [ + { + value: { addOrCancel: 0 } + action: @method:addEntry + } + { + value: { addOrCancel: 1 } + action: @method:cancelAdd + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelAdd + } + ] + } + } + } + + bbsList: { + desc: Viewing BBS List + module: bbs_list + config: { + cls: true + 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: @systemMethod:prevMenu + } + ] + } + 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 + } + ] + } + } + } + } + + fullLogoffSequence: { + desc: Logging Off + prompt: logoffConfirmation + submit: [ + { + value: { promptValue: 0 } + action: @menu:fullLogoffSequencePreAd + } + { + value: { promptValue: 1 } + action: @systemMethod:prevMenu + } + ] + } + + fullLogoffSequencePreAd: { + art: PRELOGAD + desc: Logging Off + next: fullLogoffSequenceRandomBoardAd + config: { + cls: true + nextTimeout: 1500 + } + } + + fullLogoffSequenceRandomBoardAd: { + art: OTHRBBS + desc: Logging Off + next: logoff + config: { + baudRate: 57600 + pause: true + cls: true + } + } + + logoff: { + art: LOGOFF + desc: Logging Off + next: @systemMethod:logoff + } + + //////////////////////////////////////////////////////////////////////// + // These entries are required by the system and must exist. + // You can still modify/theme them, however. + //////////////////////////////////////////////////////////////////////// + idleLogoff: { + art: IDLELOG + next: @systemMethod:logoff + } + } + + prompts: { + menuCommand: { + art: MNUPRMT + mci: { + TL1: { } + ET2: { + argName: command + width: 20 + maxLength: 20 + submit: true + textStyle: upper + focus: true + } + } + } + + logoffConfirmation: { + art: LOGPMPT + mci: { + TM1: { + argName: promptValue + items: [ "yes", "no" ] + focus: true + hotKeys: { Y: 0, N: 1 } + hotKeySubmit: true + } + } + } + + forgotPasswordPrompt: { + art: FORGOTPW + mci: { + ET1: { + argName: username + maxLength: @config:users.usernameMax + width: 32 + focus: true + } + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + + //////////////////////////////////////////////////////////////////////// + // These entries are required by the system and must exist. + // You can still modify/theme them, however. + //////////////////////////////////////////////////////////////////////// + pause: { + art: pause + config: { + trailingLF: no + } + } + } +} diff --git a/misc/menu_templates/message_base.in.hjson b/misc/menu_templates/message_base.in.hjson new file mode 100644 index 00000000..a7bcac65 --- /dev/null +++ b/misc/menu_templates/message_base.in.hjson @@ -0,0 +1,816 @@ +{ + menus: { + messageBaseMainMenu: { + art: MSGMNU + desc: Message Area + prompt: messageBaseMenuPrompt + config: { + interrupt: realtime + } + submit: [ + { + value: { command: "P" } + action: @menu:messageBaseNewPost + } + { + value: { command: "J" } + action: @menu:messageBaseChangeCurrentConference + } + { + value: { command: "C" } + action: @menu:messageBaseChangeCurrentArea + } + { + value: { command: "L" } + action: @menu:messageBaseMessageList + } + { + value: { command: "<" } + action: @systemMethod:prevConf + } + { + value: { command: ">" } + action: @systemMethod:nextConf + } + { + value: { command: "[" } + action: @systemMethod:prevArea + } + { + value: { command: "]" } + action: @systemMethod:nextArea + } + { + value: { command: "D" } + action: @menu:messageBaseSetNewScanDate + } + { + value: { command: "S" } + action: @menu:messageBaseSearch + } + { + value: { command: "M" } + action: @menu:messageBaseMyMessages + } + { + value: { command: "A" } + action: @menu:editAutoSignature + } + { + value: { command: "Q" } + action: @systemMethod:prevMenu + } + { + value: { command: "G" } + action: @menu:fullLogoffSequence + } + ] + } + + messageBaseNewPost: { + desc: Posting message, + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + } + editorMode: edit + editorType: area + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + text: All + validate: @systemMethod:validateNonEmpty + maxLength: 36 + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateNonEmpty + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + + 1: { + "mci" : { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + } + 2: { + TLTL: { + mci: { + TL1: { width: 5 } + TL2: { width: 4 } + } + } + } + 3: { + HM: { + mci: { + HM1: { + "items" : [ "save", "discard", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + + messageBaseChangeCurrentConference: { + art: CCHANGE + module: msg_conf_list + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: conf + } + } + submit: { + *: [ + { + value: { conf: null } + action: @method:changeConference + } + ] + } + actionKeys: @reference:common.quitToPrev + } + } + } + + messageBaseChangeCurrentArea: { + art: CHANGE + module: msg_area_list + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: area + } + } + submit: { + *: [ + { + value: { area: null } + action: @method:changeArea + } + ] + } + actionKeys: @reference:common.quitToPrev + } + } + } + + messageBaseMessageList: { + module: msg_list + art: MSGLIST + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: @reference:common.quitToPrev + } + } + } + + messageBaseSetNewScanDate: { + module: set_newscan_date + desc: Message Base + art: SETMNSDATE + config: { + target: message + scanDateFormat: YYYYMMDD + } + form: { + 0: { + mci: { + ME1: { + focus: true + submit: true + argName: scanDate + maskPattern: "####/##/##" + } + SM2: { + argName: targetSelection + submit: false + } + } + submit: { + *: [ + { + value: { scanDate: null } + action: @method:scanDateSubmit + } + ] + } + actionKeys: @reference:common.quitToPrev + } + } + } + + messageBaseSearch: { + desc: Message Search + module: message_base_search + art: MSEARCH + config: { + messageListMenu: messageBaseSearchResultsMessageList + } + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + SM3: { + argName: confTag + } + SM4: { + argName: areaTag + } + ET5: { + argName: toUserName + maxLength: @config:users.usernameMax + } + ET6: { + argName: fromUserName + maxLength: @config:users.usernameMax + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageBaseSearchResultsMessageList: { + desc: Message Search + module: msg_list + art: MSRCHLST + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + TL6: { + // theme me! + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: @reference:common.quitToPrev + } + } + } + + // The message_base_search module looks for this entry by default + messageSearchNoResults: { + desc: Message Search + art: MSRCNORES + config: { + pause: true + } + } + + messageBaseMyMessages: { + desc: Personal Messages + module: my_messages + config: { + messageListMenu: messageBaseMyMessagesList + } + } + + messageBaseMyMessagesList: { + desc: Personal Messages + module: msg_list + art: MYMSGLST + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: @reference:common.quitToPrev + } + } + } + + editAutoSignature: { + desc: Auto Sig Editor + module: autosig_edit + art: autosig + form: { + 0: { + mci: { + MT1: { + argName: signature + tabSwitchesView: true + } + BT2: { + text: save + argName: save + submit: true + } + } + submit: { + *: [ + { + value: { save: null } + action: @method:saveChanges + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageAreaViewPost: { + module: msg_area_view_fse + config: { + art: { + header: MSGVHDR + body: MSGBODY + footerView: MSGVFTR + help: MSGVHLP + }, + editorMode: view + editorType: area + } + form: { + 0: { + mci: { + // :TODO: ensure this block isn't even req. for theme to apply... + } + } + 1: { + mci: { + MT1: { + width: 79 + mode: preview + } + } + submit: { + *: [ + { + value: message + action: @method:editModeEscPressed + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + } + 2: { + TLTL: { + mci: { + TL1: { width: 5 } + TL2: { width: 4 } + } + } + } + 4: { + mci: { + HM1: { + // :TODO: (#)Jump/(L)Index (msg list)/Last + items: [ "prev", "next", "reply", "quit", "help" ] + focusItemIndex: 1 + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:prevMessage + } + { + value: { 1: 1 } + action: @method:nextMessage + } + { + value: { 1: 2 } + action: @method:replyMessage + extraArgs: { + menu: messageAreaReplyPost + } + } + { + value: { 1: 3 } + action: @systemMethod:prevMenu + } + { + value: { 1: 4 } + action: @method:viewModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "p", "shift + p" ] + action: @method:prevMessage + } + { + keys: [ "n", "shift + n" ] + action: @method:nextMessage + } + { + keys: [ "r", "shift + r" ] + action: @method:replyMessage + extraArgs: { + menu: messageAreaReplyPost + } + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "?" ] + action: @method:viewModeMenuHelp + } + { + keys: [ "down arrow", "up arrow", "page up", "page down" ] + action: @method:movementKeyPressed + } + ] + } + } + } + + messageAreaReplyPost: { + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + quote: MSGQUOT + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + } + editorMode: edit + editorType: area + } + form: { + 0: { + mci: { + // :TODO: use appropriate system properties for max lengths + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + validate: @systemMethod:validateNonEmpty + maxLength: 36 + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateNonEmpty + } + TL4: { + // :TODO: this is for RE: line (NYI) + //width: 27 + //textOverflow: ... + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + MT1: { + width: 79 + height: 14 + argName: message + mode: edit + } + } + submit: { + *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ], + viewId: 1 + } + ] + } + + 3: { + mci: { + HM1: { + items: [ "save", "discard", "quote", "help" ] + } + } + + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 }, + action: @method:editModeMenuQuote + } + { + value: { 1: 3 } + action: @method:editModeMenuHelp + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "s", "shift + s" ] + action: @method:editModeMenuSave + } + { + keys: [ "d", "shift + d" ] + action: @systemMethod:prevMenu + } + { + keys: [ "q", "shift + q" ] + action: @method:editModeMenuQuote + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + + // Quote builder + 5: { + mci: { + MT1: { + width: 79 + height: 7 + } + VM3: { + width: 79 + height: 4 + argName: quote + } + } + + submit: { + *: [ + { + value: { quote: null } + action: @method:appendQuoteEntry + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @method:quoteBuilderEscPressed + } + ] + } + } + } + + // + // The 'msg_conf_list' module defaults to looking for + // a menu entry of 'changeConfPreArtMenu'. If found, + // this menu will be executed upon changing message + // conferences using the conference tag as an art spec. + // + changeMessageConfPreArt: { + module: show_art + config: { + method: messageConf + key: confTag + pause: true + cls: true + menuFlags: [ "popParent", "noHistory" ] + } + } + + // + // The 'msg_area_list' module defaults to looking for + // a menu entry of 'changeMessageAreaPreArt'. If found, + // this menu will be executed upon changing message + // areas using the area tag as an art spec. + // + changeMessageAreaPreArt: { + module: show_art + config: { + method: messageArea + key: areaTag + pause: true + cls: true + menuFlags: [ "popParent", "noHistory" ] + } + } + } + + prompts: { + messageBaseMenuPrompt: { + art: MSGPMPT + mci: { + //TL1: {} + ET2: { + argName: command + width: 20 + maxLength: 20 + submit: true + textStyle: upper + focus: true + } + } + }, + + // default prompt entry used by the 'msg_lsit' module + deleteMessageFromListPrompt: { + art: MSGDELPMPT + mci: { + TM1: { + argName: promptValue + items: [ "yes", "no" ] + focus: true + hotKeys: { Y: 0, N: 1 } + hotKeySubmit: true + } + } + } + } +} \ No newline at end of file diff --git a/misc/menu_templates/new_user.in.hjson b/misc/menu_templates/new_user.in.hjson new file mode 100644 index 00000000..084e3788 --- /dev/null +++ b/misc/menu_templates/new_user.in.hjson @@ -0,0 +1,362 @@ +{ + menus: { + // A quick preamble - defaults to warning about broken terminals + newUserApplicationPre: { + art: NEWUSER1 + next: newUserApplication + desc: Applying + config: { + pause: true + cls: true + menuFlags: [ "noHistory" ] + } + } + + newUserApplication: { + module: nua + art: NUA + next: [ + { + // Initial SysOp does not send feedback to themselves + acs: ID1 + next: fullLoginSequenceLoginArt + } + { + // ...everyone else does + next: newUserFeedbackToSysOpPreamble + } + ] + form: { + 0: { + mci: { + ET1: { + focus: true + argName: username + maxLength: @config:users.usernameMax + validate: @systemMethod:validateUserNameAvail + } + ET2: { + argName: realName + maxLength: @config:users.realNameMax + validate: @systemMethod:validateNonEmpty + } + MET3: { + argName: birthdate + maskPattern: "####/##/##" + validate: @systemMethod:validateBirthdate + } + ME4: { + argName: sex + maskPattern: A + textStyle: upper + validate: @systemMethod:validateNonEmpty + } + ET5: { + argName: location + maxLength: @config:users.locationMax + validate: @systemMethod:validateNonEmpty + } + ET6: { + argName: affils + maxLength: @config:users.affilsMax + } + ET7: { + argName: email + maxLength: @config:users.emailMax + validate: @systemMethod:validateEmailAvail + } + ET8: { + argName: web + maxLength: @config:users.webMax + } + ET9: { + argName: password + password: true + maxLength: @config:users.passwordMax + validate: @systemMethod:validatePasswordSpec + } + ET10: { + argName: passwordConfirm + password: true + maxLength: @config:users.passwordMax + validate: @method:validatePassConfirmMatch + } + TM12: { + argName: submission + items: [ "apply", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { "submission" : 0 } + action: @method:submitApplication + extraArgs: { + inactive: userNeedsActivated + error: newUserCreateError + } + } + { + value: { "submission" : 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: @reference:common.escToPrev + } + } + } + + // A quick preamble - defaults to warning about broken terminals (SSH version) + newUserApplicationPreSsh: { + art: NEWUSER1 + next: newUserApplicationSsh + desc: Applying + config: { + pause: true + cls: true + menuFlags: [ "noHistory" ] + } + } + + // + // SSH specialization of NUA + // Canceling this form logs off vs falling back to matrix + // + newUserApplicationSsh: { + module: nua + art: NUA + fallback: logoff + next: newUserFeedbackToSysOpPreamble + form: { + 0: { + mci: { + ET1: { + focus: true + argName: username + maxLength: @config:users.usernameMax + validate: @systemMethod:validateUserNameAvail + } + ET2: { + argName: realName + maxLength: @config:users.realNameMax + validate: @systemMethod:validateNonEmpty + } + MET3: { + argName: birthdate + maskPattern: "####/##/##" + validate: @systemMethod:validateBirthdate + } + ME4: { + argName: sex + maskPattern: A + textStyle: upper + validate: @systemMethod:validateNonEmpty + } + ET5: { + argName: location + maxLength: @config:users.locationMax + validate: @systemMethod:validateNonEmpty + } + ET6: { + argName: affils + maxLength: @config:users.affilsMax + } + ET7: { + argName: email + maxLength: @config:users.emailMax + validate: @systemMethod:validateEmailAvail + } + ET8: { + argName: web + maxLength: @config:users.webMax + } + ET9: { + argName: password + password: true + maxLength: @config:users.passwordMax + validate: @systemMethod:validatePasswordSpec + } + ET10: { + argName: passwordConfirm + password: true + maxLength: @config:users.passwordMax + validate: @method:validatePassConfirmMatch + } + TM12: { + argName: submission + items: [ "apply", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { "submission" : 0 } + action: @method:submitApplication + extraArgs: { + inactive: userNeedsActivated + error: newUserCreateError + } + } + { + value: { "submission" : 1 } + action: @systemMethod:logoff + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:logoff + } + ] + } + } + } + + newUserFeedbackToSysOpPreamble: { + art: LETTER + config: { pause: true } + next: newUserFeedbackToSysOp + } + + newUserFeedbackToSysOp: { + desc: Feedback to SysOp + module: msg_area_post_fse + next: [ + { + acs: AS2 + next: fullLoginSequenceLoginArt + } + { + next: newUserInactiveDone + } + ] + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + }, + editorMode: edit + editorType: email + messageAreaTag: private_mail + toUserId: 1 /* always to +op */ + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + text: @sysStat:sysop_username + maxLength: 36 + // :TODO: readOnly: true + } + ET3: { + argName: subject + maxLength: 72 + submit: true + text: New user feedback + validate: @systemMethod:validateMessageSubject + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + } + 1: { + mci: { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ + { + value: message + action: @method:editModeEscPressed + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + }, + 2: { + TLTL: { + mci: { + TL1: { + width: 5 + } + TL2: { + width: 4 + } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + items: [ "save", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + + newUserInactiveDone: { + desc: Finished with NUA + art: DONE + config: { pause: true } + next: @menu:logoff + } + } +} \ No newline at end of file diff --git a/misc/menu_templates/private_mail.in.hjson b/misc/menu_templates/private_mail.in.hjson new file mode 100644 index 00000000..eeefb25d --- /dev/null +++ b/misc/menu_templates/private_mail.in.hjson @@ -0,0 +1,197 @@ +{ + menus: { + privateMailMenu: { + art: MAILMNU + desc: Private Mail + prompt: menuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { command: "C" } + action: @menu:privateMailMenuCreateMessage + } + { + value: { command: "I" } + action: @menu:privateMailMenuInbox + } + { + value: { command: "Q" } + action: @systemMethod:prevMenu + } + { + value: { command: "G" } + action: @menu:fullLogoffSequence + } + ] + } + + privateMailMenuCreateMessage: { + desc: Mailing Someone + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + }, + editorMode: edit + editorType: email + messageAreaTag: private_mail + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + validate: @systemMethod:validateGeneralMailAddressedTo + maxLength: 36 + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateMessageSubject + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + actionKeys: @reference: common.escToPrev + } + 1: { + mci: { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { value: "message", action: "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + }, + 2: { + TLTL: { + mci: { + TL1: { + width: 5 + } + TL2: { + width: 4 + } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + items: [ "save", "discard", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + + privateMailMenuInbox: { + module: msg_list + art: PRVMSGLIST + config: { + menuViewPost: messageAreaViewPost + messageAreaTag: private_mail + } + form: { + 0: { // main list + mci: { + VM1: { + focus: true + submit: true + argName: message + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "delete", "d", "shift + d" ] + action: @method:deleteSelected + } + ] + } + 1: { // delete prompt form + submit: { + *: [ + { + value: { promptValue: 0 } + action: @method:deleteMessageYes + } + { + value: { promptValue: 1 } + action: @method:deleteMessageNo + } + ] + } + } + } + } + + } +} \ No newline at end of file diff --git a/misc/prompt_template.in.hjson b/misc/prompt_template.in.hjson index e5a50630..4e451a1f 100644 --- a/misc/prompt_template.in.hjson +++ b/misc/prompt_template.in.hjson @@ -1,6 +1,6 @@ { /* - ./\/\.' ENiGMA½ Prompt Configuration -/--/-------- - -- - + ./\/\.' ENiGMA½ Prompt Configuration -/--/-------- - -- - _____________________ _____ ____________________ __________\_ / \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! @@ -12,22 +12,23 @@ ------------------------------------------------------------------------------- - This configuration is in HJSON (http://hjson.org/) format. Strict to-spec + This configuration is in HJSON (http://hjson.org/) format. Strict to-spec JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. - + See http://hjson.org/ for more information and syntax. - If you haven't yet, copy the conents of this file to something like - sick_board_prompt.hjson. Point to it via config.hjson using the + If you haven't yet, copy the conents of this file to something like + sick_board_prompt.hjson. Point to it via config.hjson using the 'general.promptFile' key: - - general: { promptFile: "sick_board_prompt.hjson" } - + + general: { promptFile: "sick_board_prompt.hjson" } + */ // :TODO: this entire file needs cleaned up a LOT // :TODO: Convert all of this to HJSON prompts: { + /* userCredentials: { "art" : "usercred", "mci" : { @@ -58,20 +59,9 @@ } } }, + */ - logoffConfirmation: { - art: LOGPMPT - mci: { - TM1: { - argName: promptValue - items: [ "yes", "no" ] - focus: true - hotKeys: { Y: 0, N: 1 } - hotKeySubmit: true - } - } - } - + /* loginSequenceFlavorSelect: { art: LOGINSEL mci: { @@ -85,66 +75,8 @@ } } } + */ - loginGlobalNewScan: { - art: GNSPMPT - mci: { - TM1: { - argName: promptValue - items: [ "yes", "no" ] - focus: true - hotKeys: { Y: 0, N: 1 } - hotKeySubmit: true - } - } - } - - menuCommand: { - art: MNUPRMT - mci: { - TL1: { - // theme me! - } - ET2: { - argName: command - width: 20 - maxLength: 20 - submit: true - textStyle: upper - focus: true - } - } - }, - - messageMenuCommand: { - art: MSGPMPT - mci: { - TL1: { - // theme me! - } - ET2: { - argName: command - width: 20 - maxLength: 20 - submit: true - textStyle: upper - focus: true - } - } - }, - - deleteMessageFromListPrompt: { - art: MSGDELPMPT - mci: { - TM1: { - argName: promptValue - items: [ "yes", "no" ] - focus: true - hotKeys: { Y: 0, N: 1 } - hotKeySubmit: true - } - } - } "newAreaPostPrompt" : { "art" : "message_area_new_post", @@ -160,70 +92,14 @@ } }, - forgotPasswordPrompt: { - art: FORGOTPW - mci: { - ET1: { - argName: username - maxLength: @config:users.usernameMax - width: 32 - focus: true - } - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - /////////////////////////////////////////////////////////////////////// // File Base Related /////////////////////////////////////////////////////////////////////// - fileMenuCommand: { - art: FILPMPT - mci: { - TL1: {} - ET2: { - argName: menuOption - width: 20 - maxLength: 20 - textStyle: upper - focus: true - } - } - } - fileBaseRateEntryPrompt: { - art: RATEFILE - mci: { - SM1: { - argName: rating - items: [ "-----", "*----", "**---", "***--", "****-", "*****" ] - } - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - - fileBaseTagEntryPrompt: { - art: TAGFILE - mci: { - ET1: { - argName: tags - } - } - } /////////////////////////////////////////////////////////////////////// // Standard / Required - // + // // Prompts in this section are considered "standard" and are required // to be present // @@ -271,7 +147,7 @@ /* see notes in menu_module.js also ...how to allow for this to come from the theme first??? - same as custom vc drawing/etc.? ... + same as custom vc drawing/etc.? ... { "theme" : { From 8c1ff7e2283620a8dfa4524d8da5cb36da6cb05e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 5 Jul 2020 15:32:10 -0600 Subject: [PATCH 31/43] Exit on error --- core/bbs.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/bbs.js b/core/bbs.js index 87289e94..e3d08ed4 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -125,6 +125,7 @@ function main() { if(err && !errorDisplayed) { console.error('Error initializing: ' + util.inspect(err)); + return process.exit(); } } ); From f5f8b08a27e5cc8871c4e32aac85377144251c90 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 6 Jul 2020 17:55:53 -0600 Subject: [PATCH 32/43] argName: message -> messageIndex --- WHATSNEW.md | 1 + misc/menu_templates/login.in.hjson | 2 +- misc/menu_templates/message_base.in.hjson | 6 +++--- misc/menu_templates/private_mail.in.hjson | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 5d204610..f1eed328 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -7,6 +7,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * A full configuration revamp has taken place. Configuration files such as `config.hjson`, `menu.hjson`, and `theme.hjson` can now utilize includes via the `includes` directive, reference 'self' sections using `@reference:` and import environment variables with `@environment`. * An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `includes`ed in `menu.hjson`. * New `PV` ACS check for arbitrary user properties. See [ACS](/docs/configuration/acs.md) for details. +* The `message` arg used by `msg_list` has been deprecated. Please starting using `messageIndex` for this purpose. Support for `message` will be removed in the future. ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/misc/menu_templates/login.in.hjson b/misc/menu_templates/login.in.hjson index bcee88e2..d16ace0c 100644 --- a/misc/menu_templates/login.in.hjson +++ b/misc/menu_templates/login.in.hjson @@ -372,7 +372,7 @@ VM1: { focus: true submit: true - argName: message + argName: messageIndex } TL6: { } } diff --git a/misc/menu_templates/message_base.in.hjson b/misc/menu_templates/message_base.in.hjson index a7bcac65..e5bfb822 100644 --- a/misc/menu_templates/message_base.in.hjson +++ b/misc/menu_templates/message_base.in.hjson @@ -245,7 +245,7 @@ VM1: { focus: true submit: true - argName: message + argName: messageIndex } } submit: { @@ -372,7 +372,7 @@ VM1: { focus: true submit: true - argName: message + argName: messageIndex } TL6: { // theme me! @@ -421,7 +421,7 @@ VM1: { focus: true submit: true - argName: message + argName: messageIndex } } submit: { diff --git a/misc/menu_templates/private_mail.in.hjson b/misc/menu_templates/private_mail.in.hjson index eeefb25d..0ebd2fa9 100644 --- a/misc/menu_templates/private_mail.in.hjson +++ b/misc/menu_templates/private_mail.in.hjson @@ -145,7 +145,7 @@ module: msg_list art: PRVMSGLIST config: { - menuViewPost: messageAreaViewPost + menuViewPost: messageBaseViewPost messageAreaTag: private_mail } form: { @@ -154,7 +154,7 @@ VM1: { focus: true submit: true - argName: message + argName: messageIndex } } submit: { From 51f3b0ffcea5e675a355fc0288275332860d6122 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 6 Jul 2020 17:59:34 -0600 Subject: [PATCH 33/43] Minor --- misc/menu_templates/main.in.hjson | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/misc/menu_templates/main.in.hjson b/misc/menu_templates/main.in.hjson index ac4b1771..c601fed1 100644 --- a/misc/menu_templates/main.in.hjson +++ b/misc/menu_templates/main.in.hjson @@ -72,9 +72,6 @@ } menus: { - /////////////////////////////////////////////////////////////////////// - // Main Menu - /////////////////////////////////////////////////////////////////////// mainMenu: { art: MMENU desc: Main Menu @@ -84,10 +81,6 @@ interrupt: realtime } submit: [ - { - value: { command: "MSG" } - action: @menu:nodeMessage - } { value: { command: "G" } action: @menu:fullLogoffSequence @@ -178,8 +171,8 @@ ] } { - value: 1 - action: @menu:mainMenu + value: { command: "MSG" } + action: @menu:nodeMessage } ] } From 189c42ebea3b10fe836263cab001b807e3c5863b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 6 Jul 2020 18:16:53 -0600 Subject: [PATCH 34/43] Doc updates --- UPGRADE.md | 25 +--- docs/configuration/creating-config.md | 2 +- docs/configuration/directory-structure.md | 8 +- docs/configuration/menu-hjson.md | 4 +- misc/prompt_template.in.hjson | 170 ---------------------- 5 files changed, 16 insertions(+), 193 deletions(-) delete mode 100644 misc/prompt_template.in.hjson diff --git a/UPGRADE.md b/UPGRADE.md index 3898a320..0bedeb2e 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -2,29 +2,17 @@ This document covers basic upgrade notes for major ENiGMA½ version updates. # Before Upgrading -* Always back up your system! +* Always back up your system! (See [Administration](/docs/admin/administration.md)) * Seriously, always back up your system! -* At least back up the `db` directory and your `menu.hjson` (or renamed equivalent) # General Notes ## Configuration File Updates -In general, look at the `menu_template.in.hjson`, and `config_template.in.hjson` as well as the default `luciano_blocktronics/theme.hjson` files when you update. These files may come with new sections you wish to merge into your system! +In general, look at template menu files in `misc/menu_templates`, and `config_template.in.hjson` as well as the default `luciano_blocktronics/theme.hjson` files when you update. These files may come with new sections you wish to merge into your system! -### menu.hjson -Upgrades often come with changes to the default `menu_template.in.hjson`. It is wise to use a *different* file name for your BBS's version of this file and point to it via `config.hjson`. For example: - -```hjson -general: { - menuFile: my_bbs.hjson -} -``` - -After updating code, use a program such as DiffMerge to merge in updates to -`my_bbs.hjson` from the shipping `menu.hjson`. - -### theme.hjson -Any custom themes you have created may now be missing features as well. Take a look at the default `luciano_blocktronics/theme.hjson` file. You can use missing sections in your `theme.hjson` (which will generally correspond to sections you've also merged in to your `menu.hjson`). +### Menus & Theme Updates +Upgrades often come with changes to the default menu templates found in `misc/menu_tempaltes`. You can use these as references for changes and additions to the default menu sets. This also applies to the default `luciano_blocktronics` theme and it's `theme.hjson` file. +See [Updating](/docs/admin/updating.md) for details on menu files/etc. # Upgrading the Code Upgrading from GitHub is easy: @@ -33,7 +21,7 @@ Upgrading from GitHub is easy: cd /path/to/enigma-bbs git pull rm -rf npm_modules # do this any time you update Node.js itself -npm install +npm install # or simply 'yarn' ``` # Problems @@ -42,6 +30,7 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or # 0.0.11-beta to 0.0.12-beta * Be aware that `master` is now mainline! This means all `git pull`'s will yield the latest version. See [WHATSNEW](WHATSNEW.md) for more information. +* There is no longer a `prompt.hjson` file. Prompts are simply part of the menu set in the `prompts` section. # 0.0.10-alpha to 0.0.11-beta * Node.js 12.x LTS is now in use. Follow standard Node.js upgrade procedures (e.g.: `nvm install 12 && nvm use 12`). diff --git a/docs/configuration/creating-config.md b/docs/configuration/creating-config.md index 068ad5fa..432752b5 100644 --- a/docs/configuration/creating-config.md +++ b/docs/configuration/creating-config.md @@ -10,5 +10,5 @@ Your initial configuration skeleton can be created using the `oputil.js` command ./oputil.js config new ``` -You will be asked a series of questions to create an initial configuration, which will be saved to `/enigma-bbs-install-path/config/config.hjson`. This will also produce `config/-menu.hjson` and `config/-prompt.hjson` files (where `` is replaced by the name you provided in the steps above). See [Menu HJSON](menu-hjson.md) for more information. +You will be asked a series of questions to create an initial configuration, which will be saved to `/enigma-bbs-install-path/config/config.hjson`. This will also produce menu files under `config/menus/`. See [Menu HJSON](menu-hjson.md) for more information. diff --git a/docs/configuration/directory-structure.md b/docs/configuration/directory-structure.md index d968df17..e139be80 100644 --- a/docs/configuration/directory-structure.md +++ b/docs/configuration/directory-structure.md @@ -2,14 +2,16 @@ layout: page title: Directory Structure --- -All paths mentioned here are relative to the ENiGMA½ checkout directory. +All paths mentioned here are relative to the ENiGMA½ checkout directory. | Directory | Description | |---------------------|-----------------------------------------------------------------------------------------------------------| | `/art/general` | Non-theme art - welcome ANSI, logoff ANSI, etc. See [General Art]({{ site.baseurl }}{% link art/general.md %}). | `/art/themes` | Theme art. Themes should be in their own subdirectory and contain a theme.hjson. See [Themes]({{ site.baseurl }}{% link art/themes.md %}). -| `/config` | config.hjson, [menu.hjson]({{ site.baseurl }}{% link configuration/menu-hjson.md %}) and prompt.hjson storage. Also default path for SSL certs and public/private keys -| `/db` | All ENiGMA½ databases in Sqlite3 format +| `/config` | [config.hjson](config-hjson.md) system configuration. +| `/config/menus` | [menu.hjson](menu-hjson.md)storage. +| `/config/security` | D path for SSL certs and public/private keys. +| `/db` | All ENiGMA½ databases in Sqlite3 format. | `/docs` | These docs ;-) | `/dropfiles` | Dropfiles created for [local doors]({{ site.baseurl }}{% link modding/local-doors.md %}) | `/logs` | Logs. See [Monitoring Logs]({{ site.baseurl }}{% link troubleshooting/monitoring-logs.md %}) diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index d8d21af8..898fc315 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -3,7 +3,7 @@ layout: page title: Menu HSJON --- ## Menu HJSON -The core of a ENiGMA½ based BBS is `menu.hjson`. Note that when `menu.hjson` is referenced, we're actually talking about `config/yourboardname-menu.hjson` or similar. This file determines the menus (or screens) a user can see, the order they come in and how they interact with each other, ACS configuration, etc. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. See [HJSON General Information](hjson.md) for more information. +The core of a ENiGMA½ based BBS is `menu.hjson`. Note that when `menu.hjson` is referenced, we're actually talking about `config/menus/yourboardname-*.hjson`. These files determines the menus (or screens) a user can see, the order they come in and how they interact with each other, ACS configuration, etc. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. See [HJSON General Information](hjson.md) for more information. Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each entry defines a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: @@ -13,6 +13,8 @@ Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each en Menu entries live under the `menus` section of `menu.hjson`. The *key* for a menu is it's name that can be referenced by other menus and areas of the system. +:information_source: Remember that the top level menu may include additional files using the `includes` directive. See [Configuration Files](config-files.md) for more information on this. + ## Common Menu Entry Members Below is a table of **common** menu entry members. These members apply to most entries, though entries that are backed by a specialized module (ie: `module: bbs_list`) may differ. See documentation for the module in question for particulars. diff --git a/misc/prompt_template.in.hjson b/misc/prompt_template.in.hjson deleted file mode 100644 index 4e451a1f..00000000 --- a/misc/prompt_template.in.hjson +++ /dev/null @@ -1,170 +0,0 @@ -{ - /* - ./\/\.' ENiGMA½ Prompt Configuration -/--/-------- - -- - - - _____________________ _____ ____________________ __________\_ / - \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! - // __|___// | \// |// | \// | | \// \ /___ /_____ - /____ _____| __________ ___|__| ____| \ / _____ \ - ---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ - /__ _\ - <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ - - ------------------------------------------------------------------------------- - - This configuration is in HJSON (http://hjson.org/) format. Strict to-spec - JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. - - See http://hjson.org/ for more information and syntax. - - - If you haven't yet, copy the conents of this file to something like - sick_board_prompt.hjson. Point to it via config.hjson using the - 'general.promptFile' key: - - general: { promptFile: "sick_board_prompt.hjson" } - - */ - // :TODO: this entire file needs cleaned up a LOT - // :TODO: Convert all of this to HJSON - prompts: { - /* - userCredentials: { - "art" : "usercred", - "mci" : { - "ET1" : { - "argName" : "username", - "maxLength" : "@config:users.usernameMax" - }, - "ET2" : { - "submit" : true, - "argName" : "password", - "password" : true, - "maxLength" : "@config:users.passwordMax" - } - } - }, - "userLoginCredentials" : { - "art" : "USRCRED", - "mci" : { - "ET1" : { - "argName" : "username", - "maxLength" : "@config:users.usernameMax" - }, - "ET2" : { - "submit" : true, - "argName" : "password", - "password" : true, - "maxLength" : "@config:users.passwordMax" - } - } - }, - */ - - /* - loginSequenceFlavorSelect: { - art: LOGINSEL - mci: { - TM1: { - argName: promptValue - items: [ "yes", "no" ] - focus: true - focusItemIndex: 1 - hotKeys: { Y: 0, N: 1 } - hotKeySubmit: true - } - } - } - */ - - - "newAreaPostPrompt" : { - "art" : "message_area_new_post", - "mci" : { - "ET1" : { - "argName" : "to", - "width" : 20 - }, - "ET2" : { - "argName" : "subject", - "width" : 20 - } - } - }, - - /////////////////////////////////////////////////////////////////////// - // File Base Related - /////////////////////////////////////////////////////////////////////// - - - /////////////////////////////////////////////////////////////////////// - // Standard / Required - // - // Prompts in this section are considered "standard" and are required - // to be present - // - /////////////////////////////////////////////////////////////////////// - pause: { - // - // Any menu 'pause' will use this prompt - // - art: pause - config: { - trailingLF: no - } - /* - "mci" : { - // :TODO: Need special pause for a key MCI - // e.g. %PA -> themed prompt - } - - ...or maybe pause should just be special: - { - ... - "pause" true - // uses theme pause which can be art/inline/etc. - - } - - ... better, a special prompt - - GetKeyView - * echoKey : false - - */ - } - /*, - "standard" : { - // any menu 'pause' will display this, pause for a key, then erase and move on - "pause" : { - "art" : "pause" - // :TODO: support mci mappings - } - }, - "custom" : { - - }*/ - /* - see notes in menu_module.js also - ...how to allow for this to come from the theme first??? - same as custom vc drawing/etc.? ... - - { - "theme" : { - "inlineArt" : { - "something" : "%MC and |01Pipe codes here" - } - } - } - - "pause" : { - "art" : "@inline:simplePrompt", - // support pipe codes & MCI - "simplePrompt" : "--------/ Pause /----------------", - "mci" : { - - } - } - */ - } -} From 99df4f284f737bc9bc6c5fc6c8262eb83a4057f4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 6 Jul 2020 18:36:21 -0600 Subject: [PATCH 35/43] Doc updates on prompts --- WHATSNEW.md | 2 +- docs/configuration/menu-hjson.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index f1eed328..acef4338 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -5,7 +5,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * The `master` branch has become mainline. What this means to users is `git pull` will always give you the latest and greatest. Make sure to read [Updating](/docs/admin/updating.md) and keep an eye on `WHATSNEW.md` (this file) and [UPGRADE](UPGRADE.md)! See also [ticket #276](https://github.com/NuSkooler/enigma-bbs/issues/276). * The default configuration has been moved to [config_default.js](/core/config_default.js). * A full configuration revamp has taken place. Configuration files such as `config.hjson`, `menu.hjson`, and `theme.hjson` can now utilize includes via the `includes` directive, reference 'self' sections using `@reference:` and import environment variables with `@environment`. -* An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `includes`ed in `menu.hjson`. +* An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `includes`ed in `menu.hjson`. With the removal of prompts the `PromptsChanged` event will no longer be fired. * New `PV` ACS check for arbitrary user properties. See [ACS](/docs/configuration/acs.md) for details. * The `message` arg used by `msg_list` has been deprecated. Please starting using `messageIndex` for this purpose. Support for `message` will be removed in the future. diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 898fc315..886fba7f 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -186,7 +186,9 @@ In the above entry, you'll notice `form`. This defines a form(s) object. In this * Upon submit, the first match will be executed. For example, if the user selects "login", the first entry with a value of `{ matrixSubmit: 0 }` will match (due to 0 being the first index in the list and `matrixSubmit` being the arg name in question) causing `action` of `@menu:login` to be executed (go to `login` menu). ## Prompts -TODO: describe the "prompts" section, default setup, etc. +Prompts are found in the `prompts` section of menu files. Prompts allow for quick user input and shorthand form requirements for menus. Additionally, prompts are often used for for multiple menus. Consider a pause prompt or menu command input for example. + +TODO: additional prompt docs ## ACS Checks Menu modules can check user ACS in order to restrict areas and perform flow control. See [ACS](acs.md) for available ACS syntax. From 9d8b43b605c8f3d89bb5f98f3468f99b4719b33b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 6 Jul 2020 18:49:21 -0600 Subject: [PATCH 36/43] Formatting --- misc/menu_templates/main.in.hjson | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/misc/menu_templates/main.in.hjson b/misc/menu_templates/main.in.hjson index c601fed1..cacb18ee 100644 --- a/misc/menu_templates/main.in.hjson +++ b/misc/menu_templates/main.in.hjson @@ -517,11 +517,11 @@ module: msg_area_post_fse config: { art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP }, editorMode: edit editorType: email From c91ae50c6bee76bb79911de6399b9f49c4a2cbfd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 6 Jul 2020 19:13:22 -0600 Subject: [PATCH 37/43] Fix oputil --- core/config.js | 13 +++++++++---- core/config_loader.js | 4 ++++ core/oputil/oputil_common.js | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/core/config.js b/core/config.js index 791845b8..788514ae 100644 --- a/core/config.js +++ b/core/config.js @@ -12,7 +12,12 @@ exports.Config = class Config extends ConfigLoader { super(options); } - static create(baseConfigPath, cb) { + static create(baseConfigPath, options, cb) { + if (!cb && _.isFunction(options)) { + cb = options; + options = {}; + } + const replacePaths = [ 'loginServers.ssh.algorithms.kex', 'loginServers.ssh.algorithms.cipher', @@ -24,7 +29,7 @@ exports.Config = class Config extends ConfigLoader { 'args', 'sendArgs', 'recvArgs', 'recvArgsNonBatch', ]; - const options = { + const configOptions = Object.assign({}, options, { defaultConfig : DefaultConfig, defaultsCustomizer : (defaultVal, configVal, key, path) => { if (Array.isArray(defaultVal) && Array.isArray(configVal)) { @@ -43,9 +48,9 @@ exports.Config = class Config extends ConfigLoader { Events.emit(Events.getSystemEvents().ConfigChanged); } }, - }; + }); - systemConfigInstance = new Config(options); + systemConfigInstance = new Config(configOptions); systemConfigInstance.init(baseConfigPath, err => { if (err) { console.stdout(`Configuration ${baseConfigPath} error: ${err.message}`); // eslint-disable-line no-console diff --git a/core/config_loader.js b/core/config_loader.js index 768cbe1d..29c2ab58 100644 --- a/core/config_loader.js +++ b/core/config_loader.js @@ -13,12 +13,14 @@ module.exports = class ConfigLoader { defaultConfig = {}, defaultsCustomizer = null, onReload = null, + keepWsc = false, } = { hotReload : true, defaultConfig : {}, defaultsCustomizer : null, onReload : null, + keepWsc : false, } ) { @@ -28,6 +30,7 @@ module.exports = class ConfigLoader { this.defaultConfig = defaultConfig; this.defaultsCustomizer = defaultsCustomizer; this.onReload = onReload; + this.keepWsc = keepWsc; } init(baseConfigPath, cb) { @@ -176,6 +179,7 @@ module.exports = class ConfigLoader { const options = { filePath, hotReload : this.hotReload, + keepWsc : this.keepWsc, callback : this._configFileChanged.bind(this), }; diff --git a/core/oputil/oputil_common.js b/core/oputil/oputil_common.js index ff6e348b..72b6f14b 100644 --- a/core/oputil/oputil_common.js +++ b/core/oputil/oputil_common.js @@ -71,7 +71,7 @@ function getConfigPath() { function initConfig(cb) { const configPath = getConfigPath(); - config.init(configPath, { keepWsc : true, hotReload : false }, cb); + config.Config.create(configPath, { keepWsc : true, hotReload : false }, cb); } function initConfigAndDatabases(cb) { From f4ccc998f52f94be96648276dca00ff310738254 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 6 Jul 2020 19:23:13 -0600 Subject: [PATCH 38/43] Fix theme slightly --- art/themes/luciano_blocktronics/theme.hjson | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 4045a549..f5dbbe30 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -255,8 +255,8 @@ VM1: { height: 14 width: 70 - itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |15{newIndicator}" - focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}" + itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts:<15.16} |15{newIndicator}" + focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts:<15.16} {newIndicator}" } } } From 60a1f14f6ef0d290872c41319e9caf7c1ff864b5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 6 Jul 2020 21:02:18 -0600 Subject: [PATCH 39/43] Better 'config new' and fix a couple menu refs --- core/msg_list.js | 9 +++--- core/oputil/oputil_config.js | 34 +++++++++++------------ misc/menu_templates/login.in.hjson | 4 +-- misc/menu_templates/main.in.hjson | 2 +- misc/menu_templates/message_base.in.hjson | 4 +-- misc/menu_templates/private_mail.in.hjson | 4 +-- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/core/msg_list.js b/core/msg_list.js index 73ab4850..8b91585b 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -60,8 +60,8 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( this.menuMethods = { selectMessage : (formData, extraArgs, cb) => { if(MciViewIds.allViews.msgList === formData.submitId) { - this.initialFocusIndex = formData.value.messageIndex || - formData.value.message; // older deprecated arg name + // 'messageIndex' or older deprecated 'message' member + this.initialFocusIndex = _.get(formData, 'value.messageIndex', formData.value.message); const modOpts = { extraArgs : { @@ -108,8 +108,9 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( if(MciViewIds.allViews.msgList != formData.submitId) { return cb(null); } - const messageIndex = formData.value.messageIndex || - formData.value.message; // older, deprecated arg name + + // newer 'messageIndex' or older deprecated value + const messageIndex = _.get(formData, 'value.messageIndex', formData.value.message); return this.promptDeleteMessageConfirm(messageIndex, cb); }, deleteMessageYes : (formData, extraArgs, cb) => { diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index 28c065d4..b1490c28 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -231,11 +231,8 @@ function buildNewConfig() { .replace(/[^a-z0-9_-]/ig, '_') .replace(/_+/g, '_') .toLowerCase(); - const menuFile = `menus/${boardName}-main.hjson`; - const mainTemplate = hjson.rt.parse(fs.readFileSync(paths.join(__dirname, '../../misc/menu_templates/main.in.hjson'), 'utf8')); - - const includeFiles = [ + const includeFilesIn = [ 'message_base.in.hjson', 'private_mail.in.hjson', 'login.in.hjson', @@ -244,8 +241,11 @@ function buildNewConfig() { 'file_base.in.hjson', ]; - includeFiles.forEach(incFile => { + let includeFiles = []; + includeFilesIn.forEach(incFile => { const outName = `${boardName}-${incFile.replace('.in', '')}`; + includeFiles.push(outName); + copyFileSyncSilent( paths.join(__dirname, '../../misc/menu_templates', incFile), paths.join(__dirname, '../../config/menus', outName), @@ -253,21 +253,21 @@ function buildNewConfig() { ); }); - mainTemplate.includes = includeFiles.map(incFile => { - return `${boardName}-${incFile.replace('.in', '')}`; - }); + // We really only need includes to be replaced + const mainTemplate = fs.readFileSync(paths.join(__dirname, '../../misc/menu_templates/main.in.hjson'), 'utf8') + .replace(/%INCLUDE_FILES%/g, includeFiles.join('\n\t\t')); // cheesy, but works! - if (writeConfig( + const menuFile = `${boardName}-main.hjson`; + fs.writeFileSync( + paths.join(__dirname, '../../config/menus', menuFile), mainTemplate, - paths.join(__dirname, '../../config/menus', `${boardName}-main.hjson`))) - { - config.general.menuFile = menuFile; + 'utf8' + ); - if(writeConfig(config, configPath)) { - console.info('Configuration generated'); - } else { - console.error('Failed writing configuration'); - } + config.general.menuFile = paths.join(__dirname, '../../config/menus/', menuFile); + + if(writeConfig(config, configPath)) { + console.info('Configuration generated'); } else { console.error('Failed writing configuration'); } diff --git a/misc/menu_templates/login.in.hjson b/misc/menu_templates/login.in.hjson index d16ace0c..c3fc5340 100644 --- a/misc/menu_templates/login.in.hjson +++ b/misc/menu_templates/login.in.hjson @@ -364,7 +364,7 @@ module: msg_list art: NEWMSGS config: { - menuViewPost: messageBaseViewPost + menuViewPost: messageAreaViewPost } form: { 0: { @@ -379,7 +379,7 @@ submit: { *: [ { - value: { message: null } + value: { messageIndex: null } action: @method:selectMessage } ] diff --git a/misc/menu_templates/main.in.hjson b/misc/menu_templates/main.in.hjson index cacb18ee..f2caef56 100644 --- a/misc/menu_templates/main.in.hjson +++ b/misc/menu_templates/main.in.hjson @@ -51,7 +51,7 @@ // You may include as many additional fragments as you like here. includes: [ - XXXXXXXX + %INCLUDE_FILES% ] // This section creates common fragments to use as @reference diff --git a/misc/menu_templates/message_base.in.hjson b/misc/menu_templates/message_base.in.hjson index e5bfb822..fd4adf97 100644 --- a/misc/menu_templates/message_base.in.hjson +++ b/misc/menu_templates/message_base.in.hjson @@ -381,7 +381,7 @@ submit: { *: [ { - value: { message: null } + value: { messageIndex: null } action: @method:selectMessage } ] @@ -427,7 +427,7 @@ submit: { *: [ { - value: { message: null } + value: { messageIndex: null } action: @method:selectMessage } ] diff --git a/misc/menu_templates/private_mail.in.hjson b/misc/menu_templates/private_mail.in.hjson index 0ebd2fa9..ebf4d104 100644 --- a/misc/menu_templates/private_mail.in.hjson +++ b/misc/menu_templates/private_mail.in.hjson @@ -145,7 +145,7 @@ module: msg_list art: PRVMSGLIST config: { - menuViewPost: messageBaseViewPost + menuViewPost: messageAreaViewPost messageAreaTag: private_mail } form: { @@ -160,7 +160,7 @@ submit: { *: [ { - value: { message: null } + value: { messageIndex: null } action: @method:selectMessage } ] From 992ee6056e78e75e0a190f714d5551a31501dfbd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 6 Jul 2020 21:14:31 -0600 Subject: [PATCH 40/43] Log if a variable isn't found --- core/config_loader.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/config_loader.js b/core/config_loader.js index 29c2ab58..47b3d4d4 100644 --- a/core/config_loader.js +++ b/core/config_loader.js @@ -161,7 +161,8 @@ module.exports = class ConfigLoader { let value = process.env[varName]; if (!value) { - return; + // console is about as good as we can do here + return console.info(`WARNING: environment variable "${varName}" from spec "${spec}" not found!`); } if ('array' === array) { From c565b8caecbcb3039517d932c17caea3cf34878a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 7 Jul 2020 19:41:17 -0600 Subject: [PATCH 41/43] Handle missing section --- core/theme.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/theme.js b/core/theme.js index f763614d..c64792d5 100644 --- a/core/theme.js +++ b/core/theme.js @@ -265,6 +265,10 @@ exports.ThemeManager = class ThemeManager { }; [ 'menus', 'prompts'].forEach(sectionName => { + if (!_.isObject(mergedTheme.sectionName)) { + return Log.error({sectionName}, 'Merged theme is missing section'); + } + Object.keys(mergedTheme[sectionName]).forEach(entryName => { let createdFormSection = false; const mergedThemeMenu = mergedTheme[sectionName][entryName]; From c8676f6c2c091674d02a6eb55a7040b82a01aa26 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 7 Jul 2020 19:47:51 -0600 Subject: [PATCH 42/43] Fix a dumb --- core/theme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/theme.js b/core/theme.js index c64792d5..f9cf5792 100644 --- a/core/theme.js +++ b/core/theme.js @@ -265,7 +265,7 @@ exports.ThemeManager = class ThemeManager { }; [ 'menus', 'prompts'].forEach(sectionName => { - if (!_.isObject(mergedTheme.sectionName)) { + if (!_.isObject(mergedTheme[sectionName])) { return Log.error({sectionName}, 'Merged theme is missing section'); } From e97f7ea61ebec7a41c51a55e0dfa1ddf4a727db8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Jul 2020 18:33:11 -0600 Subject: [PATCH 43/43] Make prompts.hjson issue stand out --- UPGRADE.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/UPGRADE.md b/UPGRADE.md index 0bedeb2e..7e352c31 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -30,7 +30,15 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or # 0.0.11-beta to 0.0.12-beta * Be aware that `master` is now mainline! This means all `git pull`'s will yield the latest version. See [WHATSNEW](WHATSNEW.md) for more information. -* There is no longer a `prompt.hjson` file. Prompts are simply part of the menu set in the `prompts` section. +* **BREAKING CHANGE** There is no longer a `prompt.hjson` file. Prompts are now simply part of the menu set in the `prompts` section. If you have an existing system you will need to add your `prompt.hjson` to your `menu.hjson`'s `includes` section at a minimum. Example: +```hjson +// menu.hjson +{ + includes: [ + my-prompts.hjson // ref your old prompts here + ] +} +``` # 0.0.10-alpha to 0.0.11-beta * Node.js 12.x LTS is now in use. Follow standard Node.js upgrade procedures (e.g.: `nvm install 12 && nvm use 12`).