diff --git a/art/themes/luciano_blocktronics/MMENU.ANS b/art/themes/luciano_blocktronics/MMENU.ANS index bd8a483f..9db39ca9 100644 Binary files a/art/themes/luciano_blocktronics/MMENU.ANS and b/art/themes/luciano_blocktronics/MMENU.ANS differ diff --git a/art/themes/luciano_blocktronics/USERACHIEV.ANS b/art/themes/luciano_blocktronics/USERACHIEV.ans similarity index 100% rename from art/themes/luciano_blocktronics/USERACHIEV.ANS rename to art/themes/luciano_blocktronics/USERACHIEV.ans diff --git a/art/themes/luciano_blocktronics/mrc.ans b/art/themes/luciano_blocktronics/mrc.ans new file mode 100644 index 00000000..a3079f3c Binary files /dev/null and b/art/themes/luciano_blocktronics/mrc.ans differ diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 998d4c34..0e21329c 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -1078,12 +1078,65 @@ } } - - //////////////////////////////// ERC /////////////////////////////// - - ercClient: { + mrc: { config: { - //chatEntryFormat: "|00|08[|03{bbsTag}|08] |10{userName}|08: |02{message}" + messageFormat: "|00|10<|02{fromUserName}|10>|00 |03{message}|00" + privateMessageFormat: "|00|10<|02{fromUserName}|15->{toUserName}|10>|00 |03{message}|00" + } + 0: { + mci: { + MT1: { + width: 72 + height: 19 + } + ET2: { + width: 69 // fnarr! + maxLength: 140 + } + TL3: { + width: 20 + } + TL4: { + width: 20 + } + TL5: { + width: 2 + } + TL6: { + width: 2 + } + } + } + } + + irc: { + config: { + messageFormat: "|00|10<|02{fromUserName}|10>|00 |03{message}|00" + privateMessageFormat: "|00|10<|02{fromUserName}|15->{toUserName}|10>|00 |03{message}|00" + } + 0: { + mci: { + MT1: { + width: 72 + height: 17 + } + ET2: { + width: 69 // fnarr! + maxLength: 140 + } + TL3: { + width: 20 + } + TL4: { + width: 20 + } + TL5: { + width: 2 + } + TL6: { + width: 2 + } + } } } } diff --git a/config/menu.hjson b/config/menu.hjson new file mode 100644 index 00000000..99585ade --- /dev/null +++ b/config/menu.hjson @@ -0,0 +1,4237 @@ +{ + /* + ./\/\.' 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 the login sequence + // + sshConnected: { + art: CONNECT + 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 + // + // To enable forgot password, you will need to have the web server + // enabled and mail/SMTP configured. Once that is in place, swap out + // the commented lines below as well as in the submit block + // + items: [ + { + text: login + data: login + } + { + text: apply + data: apply + } + { + 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: 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 + } + + 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 + // :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: "MRC" } + action: @menu:mrc + } + { + 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: 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 + } + ] + } + } + } + + ercClient: { + art: erc + module: erc_client + config: { + host: localhost + port: 5001 + bbsTag: CHANGEME + } + + form: { + 0: { + mci: { + MT1: { + width: 79 + height: 21 + mode: preview + autoScroll: true + } + ET3: { + autoScale: false + width: 77 + argName: inputArea + focus: true + submit: true + } + } + + submit: { + *: [ + { + value: { inputArea: null } + action: @method:inputAreaSubmit + } + ] + } + actionKeys: [ + { + keys: [ "tab" ] + } + { + keys: [ "up arrow" ] + action: @method:scrollDown + } + { + keys: [ "down arrow" ] + action: @method:scrollUp + } + ] + } + } + } + + mrc: { + desc: MRC Chat + module: mrc + art: MRC + config: { + cls: true + } + form: { + 0: { + mci: { + MT1: { + mode: preview + autoScroll: true + } + ET2: { + argName: inputArea + submit: true + focus: true + } + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:quit + } + { + keys: [ "down arrow", "up arrow", "page up", "page down" ] + action: @method:movementKeyPressed + } + ] + submit: { + *: [ + { + value: { inputArea: null } + action: @method:sendChatMessage + } + ] + } + } + } + } + + 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 + // :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: 1 + action: @menu:messageArea + } + ] + } + + 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 + } + ] + } + } + } + + 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 + } + 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 + } + 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 + } + 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/core/config.js b/core/config.js index 5c1f9c42..c8a4c46a 100644 --- a/core/config.js +++ b/core/config.js @@ -169,6 +169,11 @@ 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? @@ -212,7 +217,8 @@ function getDefaultConfig() { badUserNames : [ 'sysop', 'admin', 'administrator', 'root', 'all', - 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix' + 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix', + 'server', 'client', 'notme' ], preAuthIdleLogoutSeconds : 60 * 3, // 3m @@ -253,6 +259,7 @@ function getDefaultConfig() { 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/') , @@ -447,6 +454,16 @@ function getDefaultConfig() { } }, + chatServers : { + mrc: { + enabled : false, + serverHostname : 'mrc.bottomlessabyss.net', + serverPort : 5000, + retryDelay : 10000, + multiplexerPort : 5000, + } + }, + infoExtractUtils : { Exiftool2Desc : { cmd : `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x diff --git a/core/listening_server.js b/core/listening_server.js index aa573fa1..7cb7405e 100644 --- a/core/listening_server.js +++ b/core/listening_server.js @@ -28,7 +28,7 @@ function getServer(packageName) { function startListening(cb) { const moduleUtil = require('./module_util.js'); // late load so we get Config - async.each( [ 'login', 'content' ], (category, next) => { + async.each( [ 'login', 'content', 'chat' ], (category, next) => { moduleUtil.loadModulesForCategory(`${category}Servers`, (module, nextModule) => { const moduleInst = new module.getModule(); try { diff --git a/core/module_util.js b/core/module_util.js index f61929d2..033d6094 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -117,6 +117,7 @@ function getModulePaths() { config.paths.mods, config.paths.loginServers, config.paths.contentServers, + config.paths.chatServers, config.paths.scannerTossers, ]; } diff --git a/core/mrc.js b/core/mrc.js new file mode 100644 index 00000000..6fd8ac35 --- /dev/null +++ b/core/mrc.js @@ -0,0 +1,439 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Log = require('./logger.js').log; +const { MenuModule } = require('./menu_module.js'); +const { + pipeToAnsi, + stripMciColorCodes +} = require('./color_codes.js'); +const stringFormat = require('./string_format.js'); +const StringUtil = require('./string_util.js'); + +// deps +const _ = require('lodash'); +const async = require('async'); +const net = require('net'); +const moment = require('moment'); + +exports.moduleInfo = { + name : 'MRC Client', + desc : 'Connects to an MRC chat server', + author : 'RiPuk', + packageName : 'codes.l33t.enigma.mrc.client', + + // Whilst this module was put together by me (RiPuk), it should be noted that a lot of the ideas (and even some code snippets) were + // borrowed from the Synchronet implementation of MRC by echicken. So...thanks, your code was very helpful in putting this together. + // Source at http://cvs.synchro.net/cgi-bin/viewcvs.cgi/xtrn/mrc/. +}; + +const FormIds = { + mrcChat : 0, +}; + +var MciViewIds = { + mrcChat : { + chatLog : 1, + inputArea : 2, + roomName : 3, + roomTopic : 4, + mrcUsers : 5, + mrcBbses : 6, + customRangeStart : 10, // 10+ = customs + } +}; + + + +// TODO: this is a bit shit, could maybe do it with an ansi instead +const helpText = ` +General Chat: +/rooms & /join - Send a private message +---- +/whoon - Who's on what BBS +/chatters - Who's in what room +/topic - Set the room topic +/bbses & /info - Info about BBS's connected +/meetups - Info about MRC MeetUps +--- +/l33t - l337 5p34k +/kewl - BBS KeWL SPeaK +/rainbow - Crazy rainbow text +`; + + +exports.getModule = class mrcModule extends MenuModule { + constructor(options) { + super(options); + + this.log = Log.child( { module : 'MRC' } ); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + + this.state = { + socket: '', + alias: this.client.user.username, + room: '', + room_topic: '', + nicks: [], + last_ping: 0 + }; + + this.menuMethods = { + + sendChatMessage : (formData, extraArgs, cb) => { + + const inputAreaView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.inputArea); + const inputData = inputAreaView.getData(); + + this.processOutgoingMessage(inputData); + inputAreaView.clearText(); + + return cb(null); + }, + + movementKeyPressed : (formData, extraArgs, cb) => { + const bodyView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); // :TODO: use const here vs magic # + switch(formData.key.name) { + case 'down arrow' : bodyView.scrollDocumentUp(); break; + case 'up arrow' : bodyView.scrollDocumentDown(); break; + case 'page up' : bodyView.keyPressPageUp(); break; + case 'page down' : bodyView.keyPressPageDown(); break; + } + + this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); + + return cb(null); + }, + + quit : (formData, extraArgs, cb) => { + this.sendServerMessage('LOGOFF'); + clearInterval(this.heartbeat); + this.state.socket.destroy(); + return this.prevMenu(cb); + } + }; + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + async.series( + [ + (callback) => { + return this.prepViewController('mrcChat', FormIds.mrcChat, mciData.menu, callback); + }, + (callback) => { + return this.validateMCIByViewIds('mrcChat', [ MciViewIds.mrcChat.chatLog, MciViewIds.mrcChat.inputArea ], callback); + }, + (callback) => { + const connectOpts = { + port : 5000, + host : 'localhost', + }; + + // connect to multiplexer + this.state.socket = net.createConnection(connectOpts, () => { + const self = this; + // handshake with multiplexer + self.state.socket.write(`--DUDE-ITS--|${self.state.alias}\n`); + + self.clientConnect(); + + // send register to central MRC and get stats every 60s + self.heartbeat = setInterval(function () { + self.sendHeartbeat(); + self.sendServerMessage('STATS'); + }, 60000); + }); + + // when we get data, process it + this.state.socket.on('data', data => { + data = data.toString(); + this.processReceivedMessage(data); + }); + + return(callback); + } + ], + err => { + return cb(err); + } + ); + }); + } + + /** + * Adds a message to the chat log on screen + */ + addMessageToChatLog(message) { + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + const messageLength = stripMciColorCodes(message).length; + const chatWidth = chatLogView.dimens.width; + let padAmount = 0; + let spaces = 2; + + if (messageLength > chatWidth) { + padAmount = chatWidth - (messageLength % chatWidth) - spaces; + } else { + padAmount = chatWidth - messageLength - spaces ; + } + + if (padAmount < 0) padAmount = 0; + + const padding = ' |00' + ' '.repeat(padAmount); + chatLogView.addText(pipeToAnsi(message + padding)); + } + + /** + * Processes data received from the MRC multiplexer + */ + processReceivedMessage(blob) { + blob.split('\n').forEach( message => { + + try { + message = JSON.parse(message); + } catch (e) { + return; + } + + if (message.from_user == 'SERVER') { + const params = message.body.split(':'); + + switch (params[0]) { + case 'BANNER': + this.addMessageToChatLog(params[1].replace(/^\s+/, '')); + break; + + case 'ROOMTOPIC': + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomName).setText(`#${params[1]}`); + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.roomTopic).setText(pipeToAnsi(params[2])); + this.state.room = params[1]; + break; + + case 'USERLIST': + this.state.nicks = params[1].split(','); + break; + + case 'STATS': { + const stats = params[1].split(' '); + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcUsers).setText(stats[2]); + this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.mrcBbses).setText(stats[0]); + this.state.last_ping = stats[1]; + break; + } + + default: + this.addMessageToChatLog(message.body); + break; + } + + } else { + if (message.to_room == this.state.room) { + // if we're here then we want to show it to the user + const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); + this.addMessageToChatLog('|08' + currentTime + '|00 ' + message.body + '|00'); + } + } + + this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); + }); + } + + /** + * Receives the message input from the user and does something with it based on what it is + */ + processOutgoingMessage(message, to_user) { + if (message.startsWith('/')) { + this.processSlashCommand(message); + } else { + if (message == '') { + // don't do anything if message is blank, just update stats + this.sendServerMessage('STATS'); + return; + } + + // else just format and send + const textFormatObj = { + fromUserName : this.state.alias, + toUserName : to_user, + message : message + }; + + const messageFormat = + this.config.messageFormat || + '|00|10<|02{fromUserName}|10>|00 |03{message}|00'; + + const privateMessageFormat = + this.config.outgoingPrivateMessageFormat || + '|00|10<|02{fromUserName}|10|14->|02{toUserName}>|00 |03{message}|00'; + + let formattedMessage = ''; + if (to_user == undefined) { + // normal message + formattedMessage = stringFormat(messageFormat, textFormatObj); + } else { + // pm + formattedMessage = stringFormat(privateMessageFormat, textFormatObj); + } + + try { + this.sendMessageToMultiplexer(to_user || '', '', this.state.room, formattedMessage); + } catch(e) { + this.client.log.warn( { error : e.message }, 'MRC error'); + } + } + + } + + /** + * Processes a message that begins with a slash + */ + processSlashCommand(message) { + // get the chat log view in case we need it + const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + + const cmd = message.split(' '); + cmd[0] = cmd[0].substr(1).toLowerCase(); + + switch (cmd[0]) { + case 'pm': + this.processOutgoingMessage(cmd[2], cmd[1]); + break; + case 'rainbow': { + // this is brutal, but i love it + const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { + const cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); + a += `|${cc}${c}|00 `; + return a; + }, '').substr(0, 140).replace(/\\s\|\d*$/, ''); + + this.processOutgoingMessage(line); + break; + } + case 'l33t': + this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), 'l33t')); + break; + + case 'kewl': { + const text_modes = Array('f','v','V','i','M'); + const mode = text_modes[Math.floor(Math.random() * text_modes.length)]; + this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), mode)); + break; + } + case 'whoon': + this.sendServerMessage('WHOON'); + break; + + case 'motd': + this.sendServerMessage('MOTD'); + break; + + case 'meetups': + this.sendServerMessage('MEETUPS'); + break; + + case 'bbses': + this.sendServerMessage('CONNECTED'); + break; + + case 'topic': + this.sendServerMessage(`NEWTOPIC:${this.state.room}:${message.substr(7)}`); + break; + + case 'info': + this.sendServerMessage(`INFO ${cmd[1]}`); + break; + + case 'join': + this.joinRoom(cmd[1]); + break; + + case 'chatters': + this.sendServerMessage('CHATTERS'); + break; + + case 'rooms': + this.sendServerMessage('LIST'); + break; + + case 'clear': + chatLogView.setText(''); + break; + + case '?': + this.addMessageToChatLog(helpText); + break; + + default: + + break; + } + + // just do something to get the cursor back to the right place ¯\_(ツ)_/¯ + this.sendServerMessage('STATS'); + + } + + /** + * Creates a json object, stringifies it and sends it to the MRC multiplexer + */ + sendMessageToMultiplexer(to_user, to_site, to_room, body) { + + const message = { + from_user: this.state.alias, + from_room: this.state.room, + to_user: to_user, + to_site: to_site, + to_room: to_room, + body: body + }; + + // TODO: check socket still exists here + this.state.socket.write(JSON.stringify(message) + '\n'); + } + + /** + * Sends an MRC 'server' message + */ + sendServerMessage(command, to_site) { + Log.debug({ module: 'mrc', command: command }, 'Sending server command'); + this.sendMessageToMultiplexer('SERVER', to_site || '', this.state.room, command); + } + + /** + * Sends a heartbeat to the MRC server + */ + sendHeartbeat() { + this.sendServerMessage('IAMHERE'); + } + + /** + * Joins a room, unsurprisingly + */ + joinRoom(room) { + // room names are displayed with a # but referred to without. confusing. + room = room.replace(/^#/, ''); + this.state.room = room; + this.sendServerMessage(`NEWROOM:${this.state.room}:${room}`); + this.sendServerMessage('USERLIST'); + } + + /** + * Things that happen when a local user connects to the MRC multiplexer + */ + clientConnect() { + this.sendServerMessage('MOTD'); + this.joinRoom('lobby'); + this.sendServerMessage('STATS'); + this.sendHeartbeat(); + } +}; + + + + diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js new file mode 100644 index 00000000..9b976eff --- /dev/null +++ b/core/servers/chat/mrc_multiplexer.js @@ -0,0 +1,320 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Log = require('../../logger.js').log; +const { ServerModule } = require('../../server_module.js'); +const Config = require('../../config.js').get; +const { Errors } = require('../../enig_error.js'); +const SysProps = require('../../system_property.js'); +const StatLog = require('../../stat_log.js'); + +// deps +const net = require('net'); +const _ = require('lodash'); +const os = require('os'); + + +// MRC +const protocolVersion = '1.2.9'; +const lineDelimiter = new RegExp('\r\n|\r|\n'); + +const ModuleInfo = exports.moduleInfo = { + name : 'MRC', + desc : 'An MRC Chat Multiplexer', + author : 'RiPuk', + packageName : 'codes.l33t.enigma.mrc.server', + notes : 'https://bbswiki.bottomlessabyss.net/index.php?title=MRC_Chat_platform', +}; + +const connectedSockets = new Set(); + +exports.getModule = class MrcModule extends ServerModule { + constructor() { + super(); + + this.log = Log.child( { server : 'MRC' } ); + + const config = Config(); + this.mrcConnectOpts = { + host : config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', + port : config.chatServers.mrc.serverPort || 5000, + retryDelay : config.chatServers.mrc.retryDelay || 10000 + }; + } + + _connectionHandler() { + const config = Config(); + const boardName = config.general.prettyBoardName || config.general.boardName; + const enigmaVersion = 'ENiGMA½-BBS_' + require('../../../package.json').version; + + const handshake = `${boardName}~${enigmaVersion}/${os.platform()}.${os.arch()}/${protocolVersion}`; + this.log.debug({ handshake : handshake }, 'Handshaking with MRC server'); + + this.sendRaw(handshake); + this.log.info(this.mrcConnectOpts, 'Connected to MRC server'); + } + + createServer(cb) { + + if (!this.enabled) { + return cb(null); + } + + this.connectToMrc(); + this.createLocalListener(); + + return cb(null); + } + + listen(cb) { + if (!this.enabled) { + return cb(null); + } + + const config = Config(); + + const port = parseInt(config.chatServers.mrc.multiplexerPort); + if(isNaN(port)) { + this.log.warn( { port : config.chatServers.mrc.multiplexerPort, server : ModuleInfo.name }, 'Invalid port' ); + return cb(Errors.Invalid(`Invalid port: ${config.chatServers.mrc.multiplexerPort}`)); + } + Log.info( { server : ModuleInfo.name, port : config.chatServers.mrc.multiplexerPort }, 'MRC multiplexer starting up'); + return this.server.listen(port, cb); + } + + /** + * Handles connecting to to the MRC server + */ + connectToMrc() { + const self = this; + + // create connection to MRC server + this.mrcClient = net.createConnection(this.mrcConnectOpts, self._connectionHandler.bind(self)); + + this.mrcClient.requestedDisconnect = false; + + // do things when we get data from MRC central + let buffer = new Buffer.from(''); + + function handleData(chunk) { + if(_.isString(chunk)) { + buffer += chunk; + } else { + buffer = Buffer.concat([buffer, chunk]); + } + + var lines = buffer.toString().split(lineDelimiter); + + if (lines.pop()) { + // if buffer is not ended with \r\n, there's more chunks. + return; + } else { + // else, initialize the buffer. + buffer = new Buffer.from(''); + } + + lines.forEach( line => { + if (line.length) { + let message = self.parseMessage(line); + if (message) { + self.receiveFromMRC(message); + } + } + }); + } + + this.mrcClient.on('data', (data) => { + handleData(data); + }); + + this.mrcClient.on('end', () => { + this.log.info(this.mrcConnectOpts, 'Disconnected from MRC server'); + }); + + this.mrcClient.on('close', () => { + + if (this.mrcClient && this.mrcClient.requestedDisconnect) + return; + + this.log.info(this.mrcConnectOpts, 'Disconnected from MRC server, reconnecting'); + this.log.debug('Waiting ' + this.mrcConnectOpts.retryDelay + 'ms before retrying'); + + setTimeout(function() { + self.connectToMrc(); + }, this.mrcConnectOpts.retryDelay); + }); + + this.mrcClient.on('error', err => { + this.log.info( { error : err.message }, 'MRC server error'); + }); + } + + createLocalListener() { + // start a local server for clients to connect to + + this.server = net.createServer( socket => { + socket.setEncoding('ascii'); + + socket.on('data', data => { + // split on \n to deal with getting messages in batches + data.toString().split(lineDelimiter).forEach( item => { + if (item == '') return; + + // save username with socket + if(item.startsWith('--DUDE-ITS--')) { + connectedSockets.add(socket); + socket.username = item.split('|')[1]; + Log.debug( { server : 'MRC', user: socket.username } , 'User connected'); + } else { + this.receiveFromClient(socket.username, item); + } + }); + }); + + socket.on('end', function() { + connectedSockets.delete(socket); + }); + + socket.on('error', err => { + if('ECONNRESET' !== err.code) { // normal + this.log.error( { error: err.message }, 'MRC error' ); + } + }); + }); + } + + get enabled() { + return _.get(Config(), 'chatServers.mrc.enabled', false) && this.isConfigured(); + } + + isConfigured() { + const config = Config(); + return _.isNumber(_.get(config, 'chatServers.mrc.multiplexerPort')); + } + + /** + * Sends received messages to local clients + */ + sendToClient(message) { + connectedSockets.forEach( (client) => { + if (message.to_user == '' || message.to_user == client.username || message.to_user == 'CLIENT' || message.from_user == client.username || message.to_user == 'NOTME' ) { + // this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user'); + client.write(JSON.stringify(message) + '\n'); + } + }); + } + + /** + * Processes messages received from the central MRC server + */ + receiveFromMRC(message) { + + const config = Config(); + const siteName = slugify(config.general.boardName); + + if (message.from_user == 'SERVER' && message.body == 'HELLO') { + // reply with extra bbs info + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOSYS:${StatLog.getSystemStat(SysProps.SysOpUsername)}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOWEB:${config.general.website}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOTEL:${config.general.telnetHostname}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOSSH:${config.general.sshHostname}`); + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFODSC:${config.general.description}`); + + } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { + // reply to heartbeat + this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${siteName}`); + + } else { + // if not a heartbeat, and we have clients then we need to send something to them + this.sendToClient(message); + } + } + + /** + * Takes an MRC message and parses it into something usable + */ + parseMessage(line) { + + const [from_user, from_site, from_room, to_user, to_site, to_room, body ] = line.split('~'); + + // const msg = line.split('~'); + // if (msg.length < 7) { + // return; + // } + + return { from_user, from_site, from_room, to_user, to_site, to_room, body }; + } + + /** + * Receives a message from a local client and sanity checks before sending on to the central MRC server + */ + receiveFromClient(username, message) { + try { + message = JSON.parse(message); + } catch (e) { + Log.debug({ server : 'MRC', user : username, message : message }, 'Dodgy message received from client'); + } + + this.sendToMrcServer(message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body); + } + + /** + * Converts a message back into the MRC format and sends it to the central MRC server + */ + sendToMrcServer(fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { + const config = Config(); + const siteName = slugify(config.general.boardName); + + const line = [ + fromUser, + siteName, + sanitiseRoomName(fromRoom), + sanitiseName(toUser || ''), + sanitiseName(toSite || ''), + sanitiseRoomName(toRoom || ''), + sanitiseMessage(messageBody) + ].join('~') + '~'; + + // Log.debug({ server : 'MRC', data : line }, 'Sending data'); + this.sendRaw(line); + } + + sendRaw(message) { + // optionally log messages here + this.mrcClient.write(message + '\n'); + } +}; + +/** + * User / site name must be ASCII 33-125, no MCI, 30 chars max, underscores + */ +function sanitiseName(str) { + return str.replace( + /\s/g, '_' + ).replace( + /[^\x21-\x7D]|(\|\w\w)/g, '' // Non-printable & MCI + ).substr( + 0, 30 + ); +} + +function sanitiseRoomName(message) { + return message.replace(/[^\x21-\x7D]|(\|\w\w)/g, '').substr(0, 30); +} + +function sanitiseMessage(message) { + return message.replace(/[^\x20-\x7D]/g, ''); +} + +/** + * SLugifies the BBS name for use as an MRC "site name" + */ +function slugify(text) { + return text.toString() + .replace(/\s+/g, '_') // Replace spaces with _ + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '_') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, ''); // Trim - from end of text +} \ No newline at end of file diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 5e523e72..960a9896 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -50,6 +50,21 @@ general: { // Your BBS Name! boardName: XXXXX + + // Your BBS name, with pipe codes for styling + prettyBoardName : '|08XXXXX' + + // Telnet hostname and port for your board + telnetHostname : 'xibalba.l33t.codes:44510' + + // SSH hostname and port for your board + sshHostname : 'xibalba.l33t.codes:44511' + + // Your board's website + website : 'https://enigma-bbs.github.io' + + // Short board description + description : 'Yet another awesome ENiGMA½ BBS' } paths: { @@ -274,6 +289,16 @@ } } + chatServers: { + // multi relay chat settings. No need to sign up, just enable it. + // More info: https://bbswiki.bottomlessabyss.net/index.php?title=MRC_Chat_platform + mrc: { + enabled : false + serverHostname : 'mrc.bottomlessabyss.net' + serverPort : 5000 + } + } + // // Currently, ENiGMA½ can use external email to mail // users for password resets. Additional functionality will diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 9aac86f3..9ae54d17 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -1066,6 +1066,10 @@ value: { command: "UA" } action: @menu:mainMenuUserAchievementsEarned } + { + value: { command: "MRC" } + action: @menu:mrc + } { value: 1 action: @menu:mainMenu @@ -1094,6 +1098,48 @@ } } + mrc: { + desc: MRC Chat + module: mrc + art: MRC + config: { + cls: true + } + 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 + } + ] + } + } + } + } + nodeMessage: { desc: Node Messaging module: node_msg