From c1ef853abaa564946404161e2d62ff0d7d584269 Mon Sep 17 00:00:00 2001 From: Craig Hendricks <codefenix@conchaos.synchro.net> Date: Mon, 3 Mar 2025 21:08:53 +0000 Subject: [PATCH] MCR Change List on 3/3/2025: Enhancements: - Enhanced mention handling: user can use the /mentions command or press the UP hotkey to reset the mention counter to zero (0) and also review all previous mentions for the current session. - User can press the PGUP hotkey as a shortcut key for /scroll - Seconds are included in the timestamp in widescreen (132 column) mode. - Passwords get masked when using trust commands (identify, register, roompass, update password, & roomconfig password). Fixes: - Added an IAMHERE immediately after connecting before joining a room. This fixes auto-joining the lobby (or the configured room) on startup. In addition, mrc-session now also passes user's terminal size, security level, IP address, and sysop's name to the MRC server on startup. - The to_site property in messages from the server no longer needs to be considered when allowing an incoming message from mrc-connector to mrc-client. This resolves the issue of messages from ddial users not appearing in mrc-client, as well as other types of messagea (e.g.: !weather <city name> $). - Fixed user list (nick list) refresh when someone joins a room or their client session times out. - Stats on the status line no longer wait for user input to get updated. - Commands not strictly defined in mrc-session are no longer stopped from being sent out to the server. This means server commands are no longer required to be issued with /quote (but it will still work for things like /quote help for the server command list). - The HOME, END, INSERT, and PAGEDN keys no longer send an empty message if accidentally pressed. - mrc-connector.js now announces a user's departure immediately before sending the LOGOFF command. - Fixed an issue in mrc-client.js where the new_alias function saved the new default alias string, but didn't actually use the new alias string for the current session. New Features: - Basic twit filter implemented. Hides messages from problematic chatters. Usage: - /twit add nameoftwit Filters out nameoftwit's messages. - /twit del nameoftwit nameoftwit's messages are re-allowed. - /twit list Lists twits the user has added. - /twit clear Empty's the user's twit list. Twit names are saved per-user to mrc-client.ini (separated by ~ character) and remembered in future sessions. - User can press the LEFT and RIGHT arrow hotkeys to change their outgoing message color. The selected color is indicated by the > symbol to the left of the inputline, and gets saved and reused in future sessions. - Added "commands" option under [startup] in mrc-client.ini. This is a comma-separated list of commands defined by the sysop to execute when a user enters MRC. For example, including "chatters" will automatically display chatters in the current room on startup after displaying the motd screen and joining the room (requested by Amessyroom of Too Lazy BBS). - Local system messages for the user get displayed in the chat window (e.g.: email notifications, new echomail posts & replies, etc.). This way the user doesn't need to wait until leaving MRC to be notified of such things. - Added /r as a shortcut to reply to the last private message. - User can use the CTRL+D shortcut to clear the inputline, rather than having to repeatedly press backspace. - Optional "MRC for Synchronet" splash on startup. Sysop can customize it by editing the mrc-splash.msg file, or just disable it entirely by setting splash = false in mrc-client.ini. Housekeeping: - Massive reorganization of the /help information. All help info on local commands is now contained in external msg files. Redundant and obsolete commands have been removed, and text has been added to bring awareness to additional server command availability by typing /quote help - "banners" is not a command that the server expects to receive from clients in the USER context, so the banners definition in mrc-session.js, the associated function call in mrc-client.js, and the associated setting in mrc-session.ini have been removed. - Updated readme.txt file, including info on the toggle_nicks setting that was missed in the previous update. Known issue: - Strings starting with semicolons do not go out, because they are recognized by InputLine.js as sysop command prefixes. Maybe the "submit" command can be overridden to bypass this. Acknowledgements: Big thanks to StingRay, cr1mson, paulie420, and MeaTLoTioN for testing and providing helpful feedback, and as always to StackFault for insight. --- xtrn/mrc/mrc-client.example.ini | 13 +- xtrn/mrc/mrc-client.js | 367 ++++++++++++++++++++++++-------- xtrn/mrc/mrc-connector.js | 23 +- xtrn/mrc/mrc-help-main.msg | 21 ++ xtrn/mrc/mrc-help-nick.msg | 12 ++ xtrn/mrc/mrc-help-theme.msg | 5 + xtrn/mrc/mrc-help-twit.msg | 10 + xtrn/mrc/mrc-session.js | 158 +++++++------- xtrn/mrc/mrc-splash.msg | 8 + xtrn/mrc/readme.txt | 38 +++- 10 files changed, 459 insertions(+), 196 deletions(-) create mode 100644 xtrn/mrc/mrc-help-main.msg create mode 100644 xtrn/mrc/mrc-help-nick.msg create mode 100644 xtrn/mrc/mrc-help-theme.msg create mode 100644 xtrn/mrc/mrc-help-twit.msg create mode 100644 xtrn/mrc/mrc-splash.msg diff --git a/xtrn/mrc/mrc-client.example.ini b/xtrn/mrc/mrc-client.example.ini index 589b897001..8c0eda1dfe 100644 --- a/xtrn/mrc/mrc-client.example.ini +++ b/xtrn/mrc/mrc-client.example.ini @@ -5,10 +5,15 @@ ping_interval = 60 [startup] room = lobby motd = true -banners = true +commands = +splash = true [aliases] -[client] -;change this to true if you want show the nick list on connect. -show_nicks=false +[theme] + +[msg_color] + +[show_nicks] + +[twit_list] diff --git a/xtrn/mrc/mrc-client.js b/xtrn/mrc/mrc-client.js index 2e0c7ee959..4108746bb6 100644 --- a/xtrn/mrc/mrc-client.js +++ b/xtrn/mrc/mrc-client.js @@ -5,6 +5,36 @@ * echicken -at- bbs.electronicchicken.com * * I started out with good intentions. At least it works. + * + * ------------------------------------------------------------- + * Codefenix's comments: + * codefenix -at- conchaos.synchro.net + * + * Sincere thanks and kudos to echicken for laying down the + * foundation with a great initial working build. I've used it + * for a long time to join weekly MRC meetups, and it has always + * worked great. + * + * In 2023 I took it upon myself to start studying this code, + * and attempted make some basic cosmetic changes I felt were + * needed, like timestamped messages and a togglable nick list. + * + * In 2024, nelgin implemented SSL (BIG thanks for that!). Later + * that same year I decided to get back into it add basic support + * for mentions, indented line-wrapping, and basic customizable + * themes. + * + * 2025 brings the most changes yet: twit filter, browseable + * mentions, and numerous fixes previously missed. + * + * I'm a big believer in MRC, and I've always tried to be + * respectful of the original design of this code and not make + * too many jarring changes. Please excuse some of the coding + * decisions I made that may appear less than elegant. I do hope + * to optimize and improve the features implemented. Thanks to + * everyone who has provided feedback and words of encouragement. + * + * */ load('sbbsdefs.js'); @@ -21,8 +51,11 @@ var input_state = 'chat'; var show_nicks = false; var stat_mode = 0; // 0 = Local Session stats (Chatters, Latency, & Mentions); 1 = Global MRC Stats +var mentions = []; +var mention_index = 0; const MENTION_MARKER = ascii(15); const INDENT_MARKER = ascii(28); +const NOTIF_FILE = system.data_dir + "msgs/" + format("%04d.msg", user.number); // default theme var theme_bg_color = BG_BLUE; @@ -38,13 +71,14 @@ const settings = { aliases: f.iniGetObject('aliases') || {}, client: f.iniGetObject('client') || {}, show_nicks: f.iniGetObject('show_nicks') || {}, - theme: f.iniGetObject('theme') || {} + theme: f.iniGetObject('theme') || {}, + msg_color: f.iniGetObject('msg_color') || {}, + twit_list: f.iniGetObject('twit_list') || {} }; f.close(); f = undefined; - const NICK_COLOURS = [ '\x01h\x01r', '\x01h\x01g', @@ -71,34 +105,6 @@ const ACT_LVL = [ /* 3: HI */ "\x01H\x01RHI" ]; -function pipe_to_ctrl_a(str) { // could use the pipeToCtrlA function in funclib.js instead - str = str.replace(/\|00/g, "\x01N\x01K"); - str = str.replace(/\|01/g, "\x01N\x01B"); - str = str.replace(/\|02/g, "\x01N\x01G"); - str = str.replace(/\|03/g, "\x01N\x01C"); - str = str.replace(/\|04/g, "\x01N\x01R"); - str = str.replace(/\|05/g, "\x01N\x01M"); - str = str.replace(/\|06/g, "\x01N\x01Y"); - str = str.replace(/\|07/g, "\x01N\x01W"); - str = str.replace(/\|08/g, "\x01H\x01K"); - str = str.replace(/\|09/g, "\x01H\x01B"); - str = str.replace(/\|10/g, "\x01H\x01G"); - str = str.replace(/\|11/g, "\x01H\x01C"); - str = str.replace(/\|12/g, "\x01H\x01R"); - str = str.replace(/\|13/g, "\x01H\x01M"); - str = str.replace(/\|14/g, "\x01H\x01Y"); - str = str.replace(/\|15/g, "\x01H\x01W"); - str = str.replace(/\|16/g, "\x01" + 0); - str = str.replace(/\|17/g, "\x01" + 4); - str = str.replace(/\|18/g, "\x01" + 2); - str = str.replace(/\|19/g, "\x01" + 6); - str = str.replace(/\|20/g, "\x01" + 1); - str = str.replace(/\|21/g, "\x01" + 5); - str = str.replace(/\|22/g, "\x01" + 3); - str = str.replace(/\|23/g, "\x01" + 7); - return str; -} - function resize_nicklist(frames, nicks) { const maxlen = show_nicks ? Math.max(1, nicks.reduce(function (a, c) { return c.length > a ? c.length : a; @@ -121,7 +127,7 @@ function redraw_nicklist(frames, nicks, colours) { } } -function init_display() { +function init_display(msg_color) { const w = console.screen_columns; const h = console.screen_rows; const f = { top: new Frame(1, 1, w, h, BG_BLACK|LIGHTGRAY) }; @@ -130,10 +136,12 @@ function init_display() { f.divider = new Frame(1, h - 1, w, 1, theme_bg_color|LIGHTGRAY, f.top); f.nicklist = new Frame(w - 2, 2, 2, h - 3, BG_BLACK|LIGHTGRAY, f.top); f.nicks = new Frame(w - 1, 2, 1, h - 3, BG_BLACK|LIGHTGRAY, f.nicklist); - f.input = new Frame(1, h, w, 1, BG_BLACK|WHITE, f.top); + f.input_color = new Frame(1, h, 1, 1, BG_BLACK|LIGHTGRAY, f.top); + f.input = new Frame(2, h, w-2, 1, BG_BLACK|WHITE, f.top); // TODO: Test f.output_scroll = new ScrollBar(f.output, { autohide: true }); f.nick_scroll = new ScrollBar(f.nicks, { autohide: true }); f.output.word_wrap = true; + f.input_color.putmsg( pipeToCtrlA( format( "|%02d>", msg_color) ) ); f.divider.gotoxy(f.divider.width - 5, 1); f.divider.putmsg(theme_fg_color + '/help'); f.top.open(); @@ -141,19 +149,19 @@ function init_display() { } function refresh_stats(frames, session) { - if (input_state == 'chat' ) { + if (input_state == 'chat' ) { frames.divider.clear(); if (stat_mode == 0) { - frames.divider.putmsg(format(theme_fg_color + "CLIENT STATS" + theme_2nd_color + "> " + + frames.divider.putmsg(format(theme_fg_color + "CLIENT STATS" + theme_2nd_color + "> " + theme_fg_color + "Latency" + theme_2nd_color + " [" + theme_fg_color + "%d" + theme_2nd_color + "] " + theme_fg_color + "Chatters" + theme_2nd_color + " [" + theme_fg_color + "%d" + theme_2nd_color + "] " + theme_fg_color + "Mentions" + theme_2nd_color + " [" + theme_fg_color + "%d" + theme_2nd_color + "]", session.latency, session.nicks.length, session.mention_count - )); + )); } else { - frames.divider.putmsg(format(theme_fg_color + "GLOBAL STATS" + theme_2nd_color + "> " + + frames.divider.putmsg(format(theme_fg_color + "GLOBAL STATS" + theme_2nd_color + "> " + theme_fg_color + "BBSes" + theme_2nd_color + " [" + theme_fg_color + "%d" + theme_2nd_color + "] " + theme_fg_color + "Rooms" + theme_2nd_color + " [" + theme_fg_color + "%d" + theme_2nd_color + "] " + theme_fg_color + "Users" + theme_2nd_color + " [" + theme_fg_color + "%d" + theme_2nd_color + "] " + @@ -179,8 +187,8 @@ function append_message(frames, msg, mention) { } // programmatically insert indent(s), if the message length exceeds the frame width. - if (strlen(msg) >= frames.output.width - 8) { - var indent = "\r\n" + (new Array(7).join( " " )) + INDENT_MARKER + " "; + if (strlen(msg) >= frames.output.width - (console.screen_columns < 132 ? 8 : 11)) { + var indent = "\r\n" + (new Array( console.screen_columns < 132 ? 7 : 10 ).join( " " )) + INDENT_MARKER + " "; msg = lfexpand(word_wrap(msg, frames.output.width - 9)).replace(/\r\n$/g, "").replace(/\r\n/g, indent).trim(); } @@ -190,20 +198,29 @@ function append_message(frames, msg, mention) { "\x01n" + msg + '\r\n' // message itself ); frames.output.scroll(0, -1); - if (input_state == 'scroll') frames.output.scrollTo(0, top); + + if (mention) { + mentions.push(top+1); + } + + if (input_state == 'scroll' || input_state == 'mentions') frames.output.scrollTo(0, top); } function getShortTime(d) { - return format( "%02d:%02d", d.getHours(), d.getMinutes() ); + if (console.screen_columns < 132 ) { + return format( "%02d:%02d", d.getHours(), d.getMinutes() ); + } else { + return format( "%02d:%02d:%02d", d.getHours(), d.getMinutes(), d.getSeconds() ); + } } -function display_message(frames, msg, mention) { //, colour param was listed but not used.. - const body = pipe_to_ctrl_a(truncsp(msg.body) || '').split(' '); +function display_message(frames, msg, mention) { + const body = pipeToCtrlA(truncsp(msg.body) || '').split(' '); append_message(frames, body[0] + '\x01n\x01w ' + body.slice(1).join(' ') + '\x01n\x01w', mention); } function display_server_message(frames, msg) { - append_message(frames, '\x01h\x01w' + pipe_to_ctrl_a(truncsp(msg) || '')); + append_message(frames, '\x01h\x01w' + pipeToCtrlA(truncsp(msg) || '')); } function display_title(frames, room, title) { @@ -225,9 +242,10 @@ function new_alias() { ) + suffix; set_alias(alias); settings.aliases[user.alias] = alias; + return alias; } -function list_themes () { +function list_themes() { var theme_list = []; var theme_files = directory(backslash(js.startup_dir) + "mrc-theme-*.ini"); for (var t = 0; t < theme_files.length; t++) { @@ -236,7 +254,7 @@ function list_themes () { return theme_list.join(", "); } -function set_theme (theme_name) { +function set_theme(theme_name) { var f = new File(js.startup_dir + format("mrc-theme-%s.ini", theme_name)); if ( f.open('r') ) { const theme = f.iniGetObject(); @@ -250,13 +268,99 @@ function set_theme (theme_name) { } } -function save_setting (alias, setting_name, setting_value) { +function save_setting(alias, setting_name, setting_value) { const f = new File(js.startup_dir + 'mrc-client.ini'); f.open('r+'); f.iniSetValue(setting_name, alias, setting_value); f.close(); } +function manage_twits(cmd, twit, twitlist, frames) { + switch (cmd) { + case "add": + if (twitlist.indexOf(twit) < 0) { + twitlist.push(twit); + if (twitlist.indexOf(twit) >= 0) { + save_setting(user.alias, "twit_list", twitlist.join(ascii(126))); + display_server_message(frames, "\x01n\x01c\x01h" + twit + "\x01w successfully added to Twit List\x01n.\x01n\x01w"); + } + } + break; + case "del": + var indexRmv = twitlist.indexOf(twit); + if (indexRmv >= 0) { + twitlist.splice(indexRmv, 1); + if (twitlist.indexOf(twit.toLowerCase()) < 0) { + save_setting(user.alias, "twit_list", twitlist.join(ascii(126))); + display_server_message(frames, "\x01n\x01c\x01h" + twit + "\x01w successfully REMOVED from Twit List\x01n.\x01n\x01w"); + } + } + break; + case "list": + if (twitlist.length > 0) { + display_server_message(frames, "\x01n\x01c\x01hTwit List (\x01w" + twitlist.length + "\x01c)\x01n:\x01n\x01w"); + for (var twit in twitlist ) { + display_server_message(frames, "\x01n\x01w - \x01h" + twitlist[twit] + "\x01n\x01w"); + } + } else { + display_server_message(frames, "\x01n\x01c\x01hYour Twit List is \x01wempty\x01n.\x01n\x01w"); + } + break; + case "clear": + twitlist.splice(0,twitlist.length); + save_setting(user.alias, "twit_list", ""); + display_server_message(frames, "\x01n\x01c\x01hYour Twit List is cleared.\x01n\x01w"); + break; + default: + display_server_message(frames, "\x01k\x01h*.:\x01n\x01w (*) Invalid twit command.\x01n\x01w"); + display_server_message(frames, "\x01k\x01h*.:\x01n\x01w See\x01H\x01K:\x01N \x01H/\x01Chelp\x01N \x01Htwit\x01n\x01w"); + display_server_message(frames, "\x01k\x01h*.: __\x01n\x01w"); + } +} + +function display_system_messages(frames) { // display local system messages (e.g.: user new email alerts, etc.) + if (file_size(NOTIF_FILE) > 0) { + var fnotif = new File(NOTIF_FILE); + if ( fnotif.open('r') ) { + const notif = fnotif.readAll(); + fnotif.close(); + fnotif = undefined; + display_server_message(frames, "\x01k\x01h*.:\x01y\x01h :: System Notification ::\x01n\x01w"); + for (var notif_line in notif) { + display_server_message(frames, "\x01k\x01h*.: " + notif[notif_line]); + } + display_server_message(frames, "\x01k\x01h*.:\x01k\x01h__\x01n\x01w"); + } + console.beep(); + file_removecase(NOTIF_FILE); + } +} + +function browse_mentions(frames) { + if (mentions.length > 0) { + mention_index = mentions.length-1; + input_state = 'mentions'; + frames.divider.clear(); + frames.divider.putmsg(theme_fg_color + format('UP/DOWN to browse mentions (%d of %d), ENTER to return', mention_index+1, mentions.length )); + frames.output.scrollTo(1, mentions[mention_index]); + frames.output_scroll.cycle(); + } +} + +function display_external_text(frames, which) { + var text = []; + var fhelp = new File(js.startup_dir + "mrc-" + which + ".msg"); + if ( fhelp.open('r') ) { + text = fhelp.readAll(); + fhelp.close(); + for (var line in text) { + display_server_message(frames, text[line]); + } + } else { + display_server_message(frames, "\x01w\x01h (*) Topic not found: \x01c" + which + "\x01n" ); + } +} + function main() { var msg; @@ -274,15 +378,21 @@ function main() { if (settings.show_nicks[user.alias]) { show_nicks = settings.show_nicks[user.alias]; } - if (settings.theme[user.alias]) { set_theme(settings.theme[user.alias]); } + if (settings.msg_color[user.alias]) { + session.msg_color = settings.msg_color[user.alias]; + } + if (settings.twit_list[user.alias]) { + session.twit_list = settings.twit_list[user.alias].split(ascii(126)); + } - const frames = init_display(); + const frames = init_display(session.msg_color); const inputline = new InputLine(frames.input); inputline.show_cursor = true; inputline.max_buffer = 140; + inputline.cursor_attr = BG_BLACK|session.msg_color; resize_nicklist(frames, []); redraw_nicklist(frames, []); @@ -297,42 +407,30 @@ function main() { session.on('error', function (err) { display_message(frames, { from_user: 'ERROR', body: err }); }); - session.on('help', function (cmd, help, ars) { - if (!ars || user.compare_ars(ars)) { - display_server_message(frames, format('\x01h\x01w/\x01h\x01c%s \x01h\x01w- \x01n\x01w%s', cmd, help)); + + session.on('local_help', function (help_topic) { + display_external_text(frames, "help-" + (help_topic ? help_topic : "main")); + if (help_topic == "theme") { + display_server_message(frames, "\x01n\x01w Available theme options: " + list_themes()); } - }); - session.on('local_help', function (msg) { - display_server_message(frames, '\x01h\x01w/\x01h\x01cscroll \x01h\x01w- \x01n\x01wScroll the output area'); - display_server_message(frames, '\x01h\x01w/\x01h\x01cscroll_nicks \x01h\x01w- \x01n\x01wScroll the nicklist'); - display_server_message(frames, '\x01h\x01w/\x01h\x01cnick_prefix \x01h\x01w- \x01n\x01wSet a single-character prefix for your handle, eg. /nick_prefix @'); - display_server_message( - frames, - '\x01h\x01w/\x01h\x01cnick_color \x01h\x01w- \x01n\x01wSet your nick color to one of ' - + PIPE_COLOURS.reduce(function (a, c) { - a += format('|%02d%s ', c, c); - return a; - }, '') - + ', eg. /nick_color 11' - ); - display_server_message(frames, '\x01h\x01w/\x01h\x01cnick_suffix \x01h\x01w- \x01n\x01wSet an eight-character suffix for your handle, eg. /nick_suffix <poop>'); - display_server_message(frames, '\x01h\x01w/\x01h\x01ctoggle_nicks \x01h\x01w- \x01n\x01wShow/hide the nicklist'); - display_server_message(frames, '\x01h\x01w/\x01h\x01ctoggle_stats \x01h\x01w- \x01n\x01wSwitch between global MRC stats and client/session stats.'); - display_server_message(frames, "\x01h\x01w/\x01h\x01ctheme \x01n\x01w- \x01nSelect a theme: " + list_themes() ); - display_server_message(frames, '\x01h\x01w/\x01h\x01cquit \x01n\x01w- \x01h\x01wExit the program'); + display_server_message(frames, "\x01k\x01h*.:\x01k\x01h__\x01n\x01w"); }); session.on('message', function (msg) { if (msg.from_user == 'SERVER') { display_server_message(frames, msg.body); } else { - var mention = false; - if (msg.body.toUpperCase().indexOf(user.alias.toUpperCase()) >= 0 && msg.from_user !== user.alias) { - console.beep(); - mention = true; - session.mention_count = session.mention_count + 1; - refresh_stats (frames, session); - } - display_message(frames, msg, mention); //, nick_colours[msg.from_user] || ''); + if (session.twit_list.indexOf(msg.from_user.toLowerCase()) < 0) { + var mention = false; + if (msg.body.toUpperCase().indexOf(user.alias.toUpperCase()) >= 0 && msg.from_user !== user.alias) { + console.beep(); + mention = true; + session.mention_count = session.mention_count + 1; + refresh_stats (frames, session); + } + display_message(frames, msg, mention); + } /*else { + log ( "filtered message from " + msg.from_user + "."); + }*/ } }); session.on('nicks', function (nicks) { @@ -341,7 +439,7 @@ function main() { }); resize_nicklist(frames, nicks); redraw_nicklist(frames, nicks, nick_colours); - refresh_stats (frames, session); + refresh_stats(frames, session); }); session.on('sent_privmsg', function (user, msg) { display_message(frames, { body: '--> ' + (nick_colours[user] || '') + user + ' ' + msg }); @@ -356,19 +454,64 @@ function main() { refresh_stats(frames, session); }); + if (settings.startup.splash) display_external_text(frames, "splash"); if (settings.startup.motd) session.motd(); - if (settings.startup.banners) session.banners(); + session.send_notme("|07- |11" + user.alias + " |03has arrived."); + mswait(20); + session.send_command('TERMSIZE:' + console.screen_columns + 'x' + console.screen_rows); + mswait(20); + session.send_command('BBSMETA: SecLevel(' + user.security.level + ') SysOp(' + system.operator + ')'); + mswait(20); + session.send_command('USERIP:' + (bbs.atcode("IP") == "127.0.0.1" ? client.ip_address : bbs.atcode("IP")) ); if (settings.startup.room) session.join(settings.startup.room); + if (settings.startup.commands) { + var startup_cmds = settings.startup.commands.split(','); + for (var startup_cmd in startup_cmds) { + session.send_command(startup_cmds[startup_cmd].toUpperCase()); + } + } + var cmd, line, user_input; while (!js.terminated && !break_loop) { session.cycle(); - frames.divider.gotoxy(frames.divider.width - 16, 1); - frames.divider.putmsg(theme_2nd_color + "[" + theme_fg_color + ("000" + inputline.buffer.length).slice(-3) + theme_2nd_color + '/' + theme_fg_color + inputline.max_buffer + theme_2nd_color + "]"); + if (input_state == 'chat') { + frames.divider.gotoxy(frames.divider.width - 16, 1); + frames.divider.putmsg(theme_2nd_color + "[" + theme_fg_color + ("000" + inputline.buffer.length).slice(-3) + theme_2nd_color + '/' + theme_fg_color + inputline.max_buffer + theme_2nd_color + "]"); + + if (inputline.buffer.search(/^\/(identify|register|roompass|update password|roomconfig password) ./i) == 0) { + inputline.frame.left(); + inputline.frame.write("*"); // mask text after commands involving passwords + } + } user_input = inputline.getkey(); if (typeof user_input == 'string') { if (input_state == 'chat') { - if (user_input.substring(0, 1) == '/') { // It's a command + // Define some hotkeys + if (user_input == KEY_LEFT || user_input == KEY_RIGHT) { // Change user's text color + session.msg_color = session.msg_color + (user_input == KEY_LEFT ? -1 : 1); + if (session.msg_color < 1) { + session.msg_color = 15; + } else if (session.msg_color > 15) { + session.msg_color = 1; + } + inputline.cursor_attr = BG_BLACK|session.msg_color; + frames.input_color.clear(); + frames.input_color.putmsg( pipeToCtrlA( format( "|%02d>", session.msg_color) ) ); + save_setting(user.alias, "msg_color", session.msg_color); + } else if (user_input == KEY_PAGEUP) { // Shortcut for scroll + input_state = 'scroll'; + if (frames.output.offset.y > 0) { + frames.output.scroll(0, -1 * frames.output.height); + frames.output_scroll.cycle(); + } + frames.divider.clear(); + frames.divider.putmsg(theme_fg_color + 'UP/DOWN, PGUP/PGDN, HOME/END to scroll, ENTER to return'); + } else if (user_input == KEY_UP) { + browse_mentions(frames); + } else if (user_input == CTRL_D) { + inputline.clear(); + } else if (user_input.substring(0, 1) == '/') { // It's a command cmd = user_input.split(' '); cmd[0] = cmd[0].substr(1).toLowerCase(); switch (cmd[0]) { @@ -435,20 +578,38 @@ function main() { redraw_nicklist(frames, session.nicks, nick_colours); save_setting(user.alias, "show_nicks", show_nicks); break; - case "toggle_stats": + case "toggle_stats": stat_mode = stat_mode === 0 ? 1 : 0; refresh_stats(frames, session); break; case "theme": - set_theme( cmd[1] ); - frames.title.attr = theme_bg_color|theme_fg_color; - frames.divider.attr = theme_bg_color|theme_fg_color; - display_title(frames, session.room, session.room_topic); - refresh_stats(frames, session); + if (cmd.length == 2) { + set_theme( cmd[1] ); + frames.title.attr = theme_bg_color|theme_fg_color; + frames.divider.attr = theme_bg_color|theme_fg_color; + display_title(frames, session.room, session.room_topic); + refresh_stats(frames, session); + } + break; + case "mentions": + browse_mentions(frames); + break; + case "twit": + if (cmd.length >= 2) { + manage_twits( cmd[1].toLowerCase(), cmd.slice(2).join(' ').toLowerCase(), session.twit_list, frames ); + } break; default: if (typeof session[cmd[0]] == 'function') { session[cmd[0]](cmd.slice(1).join(' ')); + // This check doesn't let commands out unless they're + // strictly defined in mrc-session.js, meaning we'd have + // to constantly chase down new server commands each time + // they're added/removed. + } else { + // Just send the command to the server if it's not defined as + // a local command. The server will tell the user if it's not valid. + session.send_command(user_input.substr(1)); } break; } @@ -475,10 +636,32 @@ function main() { } else if ( // Regular message typeof user_input == 'string' // Could be bool at this point && user_input != '' - && [KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT].indexOf(user_input) < 0 + && [KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, + KEY_HOME, KEY_END, KEY_INSERT, KEY_PAGEDN].indexOf(user_input) < 0 ) { session.send_room_message(user_input); } + } else if (input_state == 'mentions') { + if (user_input == KEY_UP || user_input == KEY_DOWN) { + mention_index = mention_index + (user_input == KEY_UP ? -1 : 1); + if (mention_index < 0) { + mention_index = 0; + } else if (mention_index > mentions.length-1) { + mention_index = mentions.length-1; + } + if (frames.output.offset.y !== mentions[mention_index] ) { + frames.output.scrollTo(1, mentions[mention_index]); + frames.output_scroll.cycle(); + frames.divider.clear(); + frames.divider.putmsg(theme_fg_color + format('UP/DOWN to browse mentions (%d of %d), ENTER to return', mention_index+1, mentions.length )); + } + } else if (user_input == '' || user_input == 'q') { + frames.output.scrollTo(1, frames.output.data_height - frames.output.height-1); + frames.output_scroll.cycle(); + input_state = 'chat'; + session.mention_count = 0; + refresh_stats(frames, session); + } } else if (input_state == 'scroll' || input_state == 'scroll_nicks') { var sframe = input_state == 'scroll' ? frames.output : frames.nicks; if (user_input == KEY_UP && sframe.offset.y > 0) { @@ -510,8 +693,12 @@ function main() { frames.nick_scroll.cycle(); console.gotoxy(inputline.frame.__relations__.child[0].x, inputline.frame.y); } + + display_system_messages(frames); + yield(); } + console.clear(autopause=false); // prevent an unintended auto-pause when quitting } main(); diff --git a/xtrn/mrc/mrc-connector.js b/xtrn/mrc/mrc-connector.js index 3de85ea00c..380c1cbf54 100644 --- a/xtrn/mrc/mrc-connector.js +++ b/xtrn/mrc/mrc-connector.js @@ -28,8 +28,8 @@ f = undefined; if (!settings.ssl) settings.ssl=false; -const PROTOCOL_VERSION = '1.3.1'; -const MAX_LINE = 256; +const PROTOCOL_VERSION = '1.3.2'; +const MAX_LINE = 512; const FROM_SITE = system.name.replace(/ /g, "_"); const SYSTEM_NAME = system_info.system_name || system.name; @@ -221,8 +221,10 @@ function mrc_receive(sock) { line = sock.recvline(MAX_LINE, settings.timeout); if (!line || line == '') break; latency_tracker.forEach(function(m) { - if (m.line===line.trim()) { + if (m.line===line.trim() || + (m.line.indexOf("~STATS") >= 0 && line.indexOf("~STATS") >= 0 ) ) { client_send({ from_user: "SERVER", to_user: 'CLIENT', body: 'LATENCY:' + (Date.now() - m.time) }); + log(LOG_DEBUG, 'Latency: ' + (Date.now() - m.time) ); latency_tracker = []; } }); @@ -239,14 +241,12 @@ function mrc_receive(sock) { fMa.write(message.body.substr(message.body.indexOf(':')+1).trim()); fMa.close(); } - if (['', 'ALL', FROM_SITE].indexOf(message.to_site) > -1) { - if (['', 'CLIENT', 'ALL', 'NOTME'].indexOf(message.to_user) > -1) { - // Forward to all clients - client_send(message); - } else { - // Send to this user - client_send(message, message.to_user); - } + if (['', 'CLIENT', 'ALL', 'NOTME'].indexOf(message.to_user) > -1) { + // Forward to all clients + client_send(message); + } else { + // Send to this user + client_send(message, message.to_user); } yield(); } @@ -274,6 +274,7 @@ function main() { Object.keys(clients).forEach(function (e, i) { if (!clients[e].socket.is_connected) { + mrc_send(mrc_sock, clients[e].username, "", "NOTME", "", "", "|07- |12" + clients[e].username + " |04has left chat|08."); mrc_send(mrc_sock, clients[e].username, '', 'SERVER', '', '', 'LOGOFF'); client_send({ from_user: clients[e].username, to_user: 'SERVER', body: 'LOGOFF' }); // Notify local clients delete clients[e]; diff --git a/xtrn/mrc/mrc-help-main.msg b/xtrn/mrc/mrc-help-main.msg new file mode 100644 index 0000000000..2b82190c7b --- /dev/null +++ b/xtrn/mrc/mrc-help-main.msg @@ -0,0 +1,21 @@ +NB__[ HWList of available commandsN: B]_____________________________ +WH/CinfoN�HK:N View information about a BBSHK:N H/CinfoN H# +WH/Ct Kor W/Cmsg�K:N Send a direct messageHK:N�H/CtN HnickN HBmessage +WH/CrN�HK:N Reply to last direct messageHK:N H/CrB message +WH/CjoinN�HK:N Move to a new roomHK:W�/CjoinN Hroom_name +WH/CtopicN�HK:N Change room topic:�H/CtopicW topic +WH/CroomsN�HK:N List available rooms +WH/CusersN�HK:N List users +WH/CwhoonN�HK:N List users and BBSes +WH/CmotdN�HK:N Display the Message of the Day +WH/Cscroll Kor CPGUPK:N Scroll the chat window +WH/Cmentions Kor CUPK:N Reset mention counter and review mentions +WH/Cscroll_nicksN HK:N Scroll the nicklist +WH/Ctoggle_nicksN HK:N Show/hide the nicklist +WH/Ctoggle_statsN HK:N Toggle MRC stats view +WH/CthemeN�HK:N Select a theme HK..........N seeHK:N H/ChelpN Htheme +WH/CtwitN�HK:N Twit List management HK....N seeHK:N H/ChelpN Htwit +WH/CquitN�HK:N Exit the program +WHCLEFTK/CRIGHTW�K:N Change your text color HK..N seeHK:N H/ChelpN Hnick +WHCCTRLK+CDN�HK:N Clear the input line +NFor a list of additional HSERVERN commands, see H/CquoteN Hhelp \ No newline at end of file diff --git a/xtrn/mrc/mrc-help-nick.msg b/xtrn/mrc/mrc-help-nick.msg new file mode 100644 index 0000000000..b0941fcadb --- /dev/null +++ b/xtrn/mrc/mrc-help-nick.msg @@ -0,0 +1,12 @@ +NB__[ HWNick customization optionsN: B]_____________________________ +HW/Cnick_prefix K:W NSet a single-character prefix for your handle +N�eg.H /Cnick_prefixN H@ +WH/Cnick_colorN HK:W NSet your nick color +H�NG02HW NC03HW NR04HW NM05HW NY06HW B09W G10W C11W R12M 13W Y14W 15 +WH/Cnick_suffix K:W NSet an eight-character suffix for your handle +N�eg.H N/HCnick_suffix W<afilz> + +N You can include pipe color codes in your nick_prefix and +N nick_suffix. Get as creative as you want! + +HB NExample: HB@CUserNameB<GaFiLZB> diff --git a/xtrn/mrc/mrc-help-theme.msg b/xtrn/mrc/mrc-help-theme.msg new file mode 100644 index 0000000000..77bbc7042b --- /dev/null +++ b/xtrn/mrc/mrc-help-theme.msg @@ -0,0 +1,5 @@ +NB__[ HWHow to set a custom themeN: B]______________________________ + +N Usage: + WH/Ctheme NHnameoftheme +N \ No newline at end of file diff --git a/xtrn/mrc/mrc-help-twit.msg b/xtrn/mrc/mrc-help-twit.msg new file mode 100644 index 0000000000..b2ba22b373 --- /dev/null +++ b/xtrn/mrc/mrc-help-twit.msg @@ -0,0 +1,10 @@ +NB__[ HWHow to use the twit listN: B]_______________________________ + +N Basic twit list implemented! Hides messages from problematic +N chatters. + +N Usage: + WH/Ctwit NHadd HBnameoftwit K:W NFilters out nameoftwit's messages + WH/Ctwit NHdel HBnameoftwit K:W Nnameoftwit's messages are re-allowed + WH/Ctwit NHlist K:W NLists twits the user has added + WH/Ctwit NHclear K:W NEmpty's the user's twit list diff --git a/xtrn/mrc/mrc-session.js b/xtrn/mrc/mrc-session.js index 6699434858..ca6e5e3516 100644 --- a/xtrn/mrc/mrc-session.js +++ b/xtrn/mrc/mrc-session.js @@ -16,7 +16,10 @@ function MRC_Session(host, port, user, pass, alias) { alias: alias || user, stats: ['-','-','-','0'], mention_count: 0, - latency: '-' + latency: '-', + msg_color: 7, + twit_list: [], + last_private_msg_from: "" }; const callbacks = { @@ -41,10 +44,10 @@ function MRC_Session(host, port, user, pass, alias) { function send_message(to_user, to_room, body) { if (body.length + state.alias.length + 1 > 250) { word_wrap(body, 250 - 1 - state.alias.length).split(/\n/).forEach(function (e) { - send(to_user, '', to_room, state.alias + ' ' + e); + send(to_user, '', to_room, state.alias + ' ' + format("|%02d", state.msg_color) + e); }); } else { - send(to_user, '', to_room, state.alias + ' ' + body); + send(to_user, '', to_room, state.alias + ' ' + format("|%02d", state.msg_color) + body); } } @@ -80,7 +83,7 @@ function MRC_Session(host, port, user, pass, alias) { break; case 'STATS': state.stats = params.split(' '); - emit('stats'); //, state.stats); + emit('stats'); break; case 'LATENCY': state.latency = params; @@ -90,6 +93,10 @@ function MRC_Session(host, port, user, pass, alias) { emit('message', msg); break; } + if (msg.body.search(/just joined room/) > -1 || + msg.body.search(/session has timed-out/) > -1) { + send_command('USERLIST', 'ALL'); + } } else if (msg.to_user == 'SERVER') { if (msg.body == 'LOGOFF') { uidx = state.nicks.indexOf(msg.from_user); @@ -97,8 +104,13 @@ function MRC_Session(host, port, user, pass, alias) { state.nicks.splice(uidx, 1); emit('nicks', state.nicks); } - } + } } else if (msg.to_user == '' || user.toLowerCase() == msg.to_user.toLowerCase()) { + + if (user.toLowerCase() == msg.to_user.toLowerCase()) { + state.last_private_msg_from = msg.from_user; + } + if (msg.to_room == '' || msg.to_room == state.room) { emit('message', msg); } @@ -112,9 +124,9 @@ function MRC_Session(host, port, user, pass, alias) { state.nicks.splice(uidx, 1); emit('nicks', state.nicks); } - } else if (msg.body.search(/just joined room/) > -1) { - send_command('USERLIST', 'ALL'); - } + } /*else if (msg.body.search(/just joined room/) > -1) { + send_command('USERLIST', 'ALL'); // moved above to msg.from_user == 'SERVER' + }*/ emit('message', msg); } } @@ -122,6 +134,10 @@ function MRC_Session(host, port, user, pass, alias) { this.send_room_message = function (msg) { send_message('', state.room, msg); } + + this.send_notme = function (msg) { + send("NOTME", "", "", msg); + } this.send_private_messsage = function (user, msg) { msg = '|11(|03Private|11)|07 ' + msg; @@ -140,7 +156,11 @@ function MRC_Session(host, port, user, pass, alias) { password: pass, alias: state.alias }) + '\r\n'); + + this.send_command('IAMHERE'); + mswait(20); this.send_command('USERLIST', 'ALL'); + mswait(20); this.send_command('STATS', 'ALL'); } @@ -181,50 +201,53 @@ function MRC_Session(host, port, user, pass, alias) { } const commands = { - banners: { - help: 'List of banners from server' // Doesn't do anything? - }, - chatters: { - help: 'List current users' - }, - bbses: { - help: 'List connected BBSs', - command: 'CONNECTED' - }, + + // This section has been cleaned up significantly. + // + // Server commands no longer need to be strictly defined + // here. + // + // Some commands are still defined if they have specific + // function calls attached to them in mrc-client.js + // (e.g. /motd) and or are local command shortcuts + // (e.g. /t and /r). + // + // Any command not defined here (or in mrc-client.js) will + // be assumed to be a server command. The server will + // inform the user if a command is invalid. + // + // The "help" properties are no longer needed, since + // help is contained in external .msg file. They have been + // left commended out to provide reference. + // + help: { - help: 'Display this help message', + //help: 'Display this help message', callback: function (str) { - emit('help', 'List of available commands:', ''); - for (var c in commands) { - emit('help', c, commands[c].help, commands[c].ars); - } - emit('local_help'); + emit('local_help', str); } }, info: { - help: 'View information about a BBS (/info #)', + //help: 'View information about a BBS (/info #)', callback: function (str) { this.send_command('INFO ' + str); } }, - join: { - help: 'Move to a new room: /join room_name', + join: { + //help: 'Move to a new room: /join room_name', callback: function (str) { // validate valid room name? - str = str.replace(/^#/, ''); + str = str.replace(/^#/, ''); this.send_command(format('NEWROOM:%s:%s', state.room, str)); state.room = str; state.nicks = []; this.send_command('USERLIST', 'ALL'); } }, - meetups: { - help: 'Display information about upcoming meetups' - }, motd: { - help: 'Display the Message of the Day' + //help: 'Display the Message of the Day' }, - msg: { - help: 'Send a private message: /msg nick message goes here', + msg: { // This is largely overtaken by /t, but we'll still handle it. + //help: 'Send a private message: /msg nick message goes here', callback: function (str) { const cmd = str.split(' '); if (cmd.length > 1 && state.nicks.indexOf(cmd[0]) > -1) { @@ -232,8 +255,8 @@ function MRC_Session(host, port, user, pass, alias) { } } }, - t: { // added as shorthand for /msg - help: 'Shorthand for /msg: /t nick message goes here', + t: { + //help: 'Send a private message: /t nick message goes here', callback: function (str) { const cmd = str.split(' '); if (cmd.length > 1 && state.nicks.indexOf(cmd[0]) > -1) { @@ -241,70 +264,41 @@ function MRC_Session(host, port, user, pass, alias) { } } }, + r: { + //help: 'Reply to last private message: /r message goes here', + callback: function (str) { + if (state.last_private_msg_from) { + this.send_private_messsage(state.last_private_msg_from, str); + } + } + }, quote: { - help: 'Send a raw command to the server', + //help: 'Send a raw command to the server', callback: function (str) { this.send_command(str); } }, quit: { - help: 'Quit the program', + //help: 'Quit the program', callback: function () { - handle.close(); emit('disconnect'); + handle.close(); } }, rooms: { - help: 'List available rooms', + //help: 'List available rooms', command: 'LIST' }, stats: { - help: 'Return anonymous server stats', + //help: 'Return anonymous server stats', command: 'statistics' }, topic: { - help: 'Change the topic of the current room', + //help: 'Change the topic of the current room', callback: function (str) { this.send_command(format('NEWTOPIC:%s:%s', state.room, str)); } - }, - users: { - help: 'Display list of users' - }, - whoon: { - help: 'Display list of users and BBSs' - }, - afk: { - help: "Set yourself AFK (Shortcut for STATUS AFK)", - callback: function (str) { - this.send_command('AFK ' + str); - } - }, - register: { - help: "Register handle on server (MRC Trust)", - callback: function (str) { - this.send_command('REGISTER ' + str); - } - }, - identify: { - help: "Identify as a registered user (MRC Trust)", - callback: function (str) { - this.send_command('IDENTIFY ' + str); - } - }, - update: { - help: "Update user registration (MRC Trust)", - callback: function (str) { - this.send_command('UPDATE ' + str); - } - }, - trust: { - help: "MRC Trust Info (MRC Trust)", - callback: function (str) { - this.send_command('TRUST ' + str); - } - } - + } }; Object.keys(commands).forEach(function (e) { @@ -312,11 +306,11 @@ function MRC_Session(host, port, user, pass, alias) { this[e] = commands[e].callback; } else if (commands[e].command) { this[e] = function () { - this.send_command(commands[e].command.toUpperCase()); + this.send_command(commands[e].command); } } else { this[e] = function () { - this.send_command(e.toUpperCase()); + this.send_command(e); } } }, this); diff --git a/xtrn/mrc/mrc-splash.msg b/xtrn/mrc/mrc-splash.msg new file mode 100644 index 0000000000..a47724bd3e --- /dev/null +++ b/xtrn/mrc/mrc-splash.msg @@ -0,0 +1,8 @@ +N�HK.N HK.N . HK~N HK~N HK~N HK.N HK~N HK~N HK.N HK~N HK~N HK~N HK~N HK.N HK~N HK~N HK~N . HK.N HK. +N HK.N HK.N . H.N HB88888b.d88b.N HB888d888 .d8888b W.N .HK .N HK. + .N .HK W.K NB.W B8H88N HB"888N HB"88b NB8H88P"NB dH88P"N�B.HK W.N . HK. + N.HK W.K NB.W B.W B8H88N B8H88 NB8H88 NB8H88N�B8H88NB�.W B.HK W.N . +HK . N.HK W.K NB.W B8H88N B8H88 NB8H88 NB8H88N�BY8H8b.N�B.HK W.N . HK. + . .N . H.N B8H88N B8H88 NB8H88 NB8H88N�B"Y8H888 W.N . HK.N HK. + ` .N HK.N HK=N==H=========================N==HK=N HK.N HK. ' +N�HK-=[N HCMNCulti HRNCelay HCNChat forW HCSynchronetN HK]=- diff --git a/xtrn/mrc/readme.txt b/xtrn/mrc/readme.txt index 4fb29996f6..8f8482e669 100644 --- a/xtrn/mrc/readme.txt +++ b/xtrn/mrc/readme.txt @@ -60,7 +60,6 @@ colour codes are permitted: http://wiki.mysticbbs.com/doku.php?id=displaycodes#color_codes_pipe_colors -I may write a CTRL-A to pipe converter at some later date. mrc-connector.ini: @@ -77,11 +76,16 @@ mrc-client.ini: above instructions while editing '/sbbs/ctrl/services.ini'. - The 'ping_interval' setting should be left at the default value unless you have a good reason for changing it. - - The values in the [startup] section determine which room the client joins - on startup, and whether the Message of the Day and banners are displayed. - - Change show_nicks in the [client] section to always display the nick list - when connecting to the MRC server. - + - The values in the [startup] section control the following: + room: Which room the client joins on startup. + motd: Whether the Message of the Day is displayed. + commands: Any additional commands to execute, eg: "chatters" to display + the list of active users in the room on startup. Use commas + to separate multiple commands. + splash: Whether the "Multi Relay Chat for Synchronet" splash is shown + on startup. + - The [aliases], [theme], [msg_color], [show_nicks], and [twit_list] sections + are for user settings which get added or updated any time a user uses them. 4) MRC Stats @@ -123,19 +127,35 @@ see "Yes" next to your BBS in the SSL column. 6) Themes MRC comes with several customizable theme files. These can be added/edited as -the system wishes. The client automatically detects any available theme files +the sysop wishes. The client automatically detects any available theme files matching the pattern: mrc-theme-<theme_name>.ini -Available themes are listed in /help. User selects a theme by typing +Available themes are listed in /help theme. User selects a theme by typing /theme <name>, and the selected theme gets saved to settings for future sessions. If a theme file is not available, the classic blue theme gets used by default. +To customize the splash that gets shown on startup, edit the mrc-splash.msg +file. Or just disable it by setting splash = false in mrc-client.ini -7) Support + +7) Known issues + +- Outgoing messages beginning with a semicolon do not get sent out to the MRC + server. For example, a winking face emoticon: ;) or ;-) + + This is because the ; character is used by the inputline.js library to + indicate the start of a sysop command. In the case of the MRC client, such + text is simply ignored and does not get sent over. + + As a workround, you may simply insert a space in front of the message you + want to send that starts with a semicolon. + + +8) Support - Post a message to 'echicken' in the Synchronet Sysops area on DOVE-Net - Find me on irc.synchro.net in #synchronet -- GitLab