diff --git a/xtrn/mrc/mrc-client.js b/xtrn/mrc/mrc-client.js index 8a3bc1e91f31620877b998f47b2ff552be033853..d38a91c9c8a5fed0aaa448171074082b7ccb395b 100644 --- a/xtrn/mrc/mrc-client.js +++ b/xtrn/mrc/mrc-client.js @@ -48,6 +48,7 @@ js.on_exit("js.counter = 0"); js.time_limit=0; var input_state = 'chat'; +var paused_msg_buffer = []; var show_nicks = false; var stat_mode = 0; // 0 = Local Session stats (Chatters, Latency, & Mentions); 1 = Global MRC Stats @@ -68,6 +69,7 @@ if (!f.open('r')) { alert("Error " + f.error + " (" + strerror(f.error) + ") opening " + f.name); exit(1); } + const settings = { root: f.iniGetObject(), startup: f.iniGetObject('startup'), @@ -140,7 +142,7 @@ function init_display(msg_color) { 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_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.input = new Frame(2, h, w-2, 1, BG_BLACK|WHITE, f.top); f.output_scroll = new ScrollBar(f.output, { autohide: true }); f.nick_scroll = new ScrollBar(f.nicks, { autohide: true }); f.output.word_wrap = true; @@ -180,7 +182,15 @@ function refresh_stats(frames, session) { } } -function append_message(frames, msg, mention) { +function append_message(frames, msg, mention, when) { + + if (input_state !== "chat") { // pause incoming messages while scrolling. + paused_msg_buffer.push({"msg": msg, // we'll capture any incoming messages in the meantime + "mention": mention, // and display them when done scrolling. + "when": new Date()}); + return; + } + const top = frames.output.offset.y; if (frames.output.data_height > frames.output.height) { while (frames.output.down()) { @@ -196,7 +206,7 @@ function append_message(frames, msg, mention) { } frames.output.putmsg( - (mention ? "\x01k\x017" : "\x01k\x01h") + getShortTime(new Date()) + // timestamp formatting + (mention ? "\x01k\x017" : "\x01k\x01h") + getShortTime(when || new Date()) + // timestamp formatting (mention ? ( "\x01n\x01r\x01h\x01i" + MENTION_MARKER ) : " ") + // mention formatting "\x01n" + msg + '\r\n' // message itself ); @@ -428,7 +438,7 @@ function main() { console.beep(); mention = true; session.mention_count = session.mention_count + 1; - refresh_stats (frames, session); + refresh_stats(frames, session); } display_message(frames, msg, mention); } /*else { @@ -456,6 +466,9 @@ function main() { session.on('latency', function () { refresh_stats(frames, session); }); + session.on('ctcp-msg', function (msg) { + display_server_message(frames, pipeToCtrlA( msg ) ); + }); if (settings.startup.splash) display_external_text(frames, "splash"); if (settings.startup.motd) session.motd(); @@ -478,14 +491,14 @@ function main() { var cmd, line, user_input; var lastnodechk = time(); while (!js.terminated && !break_loop) { - if ((time() - lastnodechk) >= 10) { - // Check the node "interrupt flag" once every 10 seconds - if (system.node_list[bbs.node_num - 1].misc & NODE_INTR) { - bbs.nodesync(); // this will display a message to to the user and disconnect - break; - } - lastnodechk = time(); - } + if ((time() - lastnodechk) >= 10) { + // Check the node "interrupt flag" once every 10 seconds + if (system.node_list[bbs.node_num - 1].misc & NODE_INTR) { + bbs.nodesync(); // this will display a message to to the user and disconnect + break; + } + lastnodechk = time(); + } session.cycle(); if (input_state == 'chat') { frames.divider.gotoxy(frames.divider.width - 16, 1); @@ -611,6 +624,9 @@ function main() { manage_twits( cmd[1].toLowerCase(), cmd.slice(2).join(' ').toLowerCase(), session.twit_list, frames ); } break; + case "?": // shortcut for "help", because why not? + session.send_command("help"); + break; default: if (typeof session[cmd[0]] == 'function') { session[cmd[0]](cmd.slice(1).join(' ')); @@ -673,6 +689,12 @@ function main() { input_state = 'chat'; session.mention_count = 0; refresh_stats(frames, session); + if (paused_msg_buffer.length > 0) { + for (var pmb in paused_msg_buffer) { + append_message(frames, paused_msg_buffer[pmb].msg, paused_msg_buffer[pmb].mention, paused_msg_buffer[pmb].when); + } + paused_msg_buffer = []; + } } } else if (input_state == 'scroll' || input_state == 'scroll_nicks') { var sframe = input_state == 'scroll' ? frames.output : frames.nicks; @@ -697,6 +719,12 @@ function main() { frames.output_scroll.cycle(); input_state = 'chat'; refresh_stats(frames, session); + if (paused_msg_buffer.length > 0) { + for (var pmb in paused_msg_buffer) { + append_message(frames, paused_msg_buffer[pmb].msg, paused_msg_buffer[pmb].mention, paused_msg_buffer[pmb].when); + } + paused_msg_buffer = []; + } } } } diff --git a/xtrn/mrc/mrc-connector.js b/xtrn/mrc/mrc-connector.js index 380c1cbf54735b39dc1095ed18d330413a23dcec..8eafd7c6e81d861b1a4d084910baeab651641c99 100644 --- a/xtrn/mrc/mrc-connector.js +++ b/xtrn/mrc/mrc-connector.js @@ -28,7 +28,7 @@ f = undefined; if (!settings.ssl) settings.ssl=false; -const PROTOCOL_VERSION = '1.3.2'; +const PROTOCOL_VERSION = '1.3.3'; const MAX_LINE = 512; const FROM_SITE = system.name.replace(/ /g, "_"); const SYSTEM_NAME = system_info.system_name || system.name; diff --git a/xtrn/mrc/mrc-help-ctcp.msg b/xtrn/mrc/mrc-help-ctcp.msg new file mode 100644 index 0000000000000000000000000000000000000000..9e8a2285fea229711dcbdbc292d966b9004722a4 --- /dev/null +++ b/xtrn/mrc/mrc-help-ctcp.msg @@ -0,0 +1,14 @@ +NBH__[ HWHow to use CTCP commandsN: BH]_______________________________ + +N CTCP (Client-to-Client Protocol) commands are special messages +N that can be sent to a channel or other clients. + +N Usage: + WH/Cctcp NHtarget HBcommand K + +N A Htarget Nis a user or room name. HB* Ntargets all. + +N Supported commands: +HB VERSION TIME PING CLIENTINFO + +N Type WH/Ctoggle_ctcp Nto hide/show incoming requests. \ No newline at end of file diff --git a/xtrn/mrc/mrc-help-main.msg b/xtrn/mrc/mrc-help-main.msg index 2b82190c7bb9f130bca968c50d0d3d41a6fc1faa..a263fdbedeb87bcec7780a6b589df19f147d14aa 100644 --- a/xtrn/mrc/mrc-help-main.msg +++ b/xtrn/mrc/mrc-help-main.msg @@ -1,21 +1,21 @@ -NB__[ HWList of available commandsN: B]_____________________________ +NBH__[ HWList of available commandsN: BH]_____________________________ 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/Cmsg Kor W/Ct�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/Cjoin Kor W/CjN HK :N Join a new roomHK:W� /CjN Hroom_name +WH/CtopicN�HK:N Change room topicHK:�HW/CtopicW topic +WH/CmeN HK:N Perform an actionHK: HW/Cme NHwaves 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 +WH/CctcpN�HK:N Send a CTCP command HK.....N seeHK:N H/ChelpN Hctcp +WH/Cquit Kor W/CqN 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 +WH/ChelpN�HK:N Shows this help list HK....N seeHK:N H/ChelpN Hmore +NFor a list of additional HSERVERN commands, see H/CquoteN Hhelp Nor H/C? \ No newline at end of file diff --git a/xtrn/mrc/mrc-help-more.msg b/xtrn/mrc/mrc-help-more.msg new file mode 100644 index 0000000000000000000000000000000000000000..5f82e4becf2fd5aeff45b6bf3fdfe88d01f00f3a --- /dev/null +++ b/xtrn/mrc/mrc-help-more.msg @@ -0,0 +1,4 @@ +NBH__[ HWList of additional commandsN: BH]____________________________ +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 diff --git a/xtrn/mrc/mrc-help-twit.msg b/xtrn/mrc/mrc-help-twit.msg index b2ba22b3731e7a7c5b1c97bc6bd7e3d262812aea..ba3b5d70df6c4895fb98316604da8f16199f813c 100644 --- a/xtrn/mrc/mrc-help-twit.msg +++ b/xtrn/mrc/mrc-help-twit.msg @@ -1,10 +1,9 @@ -NB__[ HWHow to use the twit listN: B]_______________________________ +NBH__[ HWHow to use the twit listN: B]H_______________________________ -N Basic twit list implemented! Hides messages from problematic -N chatters. +N The Htwit list Nhides messages from problematic 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 + WH/Ctwit NHlist K:W NLists twits you have added + WH/Ctwit NHclear K:W NEmpties your twit list diff --git a/xtrn/mrc/mrc-session.js b/xtrn/mrc/mrc-session.js index ca6e5e351681fd014a2bcfd317885d7ca26ff07d..049d2d1cd3a7006c9c6bbda160214614100e7358 100644 --- a/xtrn/mrc/mrc-session.js +++ b/xtrn/mrc/mrc-session.js @@ -3,7 +3,10 @@ // Passes traffic between an mrc-connector.js server and a client application // See mrc-client.js for a bad example. function MRC_Session(host, port, user, pass, alias) { - + + const MRC_VER = "Multi Relay Chat JS v1.3.3 2025-04-11 [cf]"; + const CTCP_ROOM = "ctcp_echo_channel"; + const handle = new Socket(); const state = { @@ -19,7 +22,8 @@ function MRC_Session(host, port, user, pass, alias) { latency: '-', msg_color: 7, twit_list: [], - last_private_msg_from: "" + last_private_msg_from: "", + show_ctcp_req: true }; const callbacks = { @@ -50,6 +54,63 @@ function MRC_Session(host, port, user, pass, alias) { send(to_user, '', to_room, state.alias + ' ' + format("|%02d", state.msg_color) + body); } } + + function send_ctcp(to, p, s) { + state.output_buffer.push({ + from_room: CTCP_ROOM, + to_user: to, + to_site: "", + to_room: CTCP_ROOM, + body: p + " " + user + " " + s + }); + mswait(20); + } + + function ctcp_time(d) { + return format("%02d/%02d/%02d %02d:%02d", d.getMonth()+1, d.getDate(), d.getFullYear().toString().substr(-2), d.getHours(), d.getMinutes()); + } + + function ctcp_reply(cmd) { + // Future ctcp commands can be added easily by adding another + // string to this array, and then adding the response logic + // to the switch/case structure below. + const CTCP_CMDS = [ + /* 0 */ "VERSION", + /* 1 */ "TIME", + /* 2 */ "PING", + /* 3 */ "CLIENTINFO" + ]; + var reply = ""; + switch (CTCP_CMDS.indexOf(cmd)) { + case 0: + reply = MRC_VER; + break; + case 1: + reply = ctcp_time(new Date()); + break; + case 2: + // It's not clear what's supposed to be happening here. + // In the mystic client, PING returns the message string minus + // the sum of the length of the substrings within the string... + // which is an empty string... i.e.: no response + // + // According to https://en.wikipedia.org/wiki/Client-to-client_protocol#PING, + // it's /supposed/ to be the latency between two clients, + // not taking the server into account. Such communication + // within MRC does not presently exist. + // + // So for now, we're just going to return a "PONG" in response. + reply = "PONG"; + break; + case 3: + reply = CTCP_CMDS.join(" "); + break; + default: + reply = "Unsupported ctcp command"; + break; + } + return reply.trim(); + } function emit() { if (!Array.isArray(callbacks[arguments[0]])) return; @@ -129,6 +190,24 @@ function MRC_Session(host, port, user, pass, alias) { }*/ emit('message', msg); } + if (msg.to_room === CTCP_ROOM) { + + const ctcp_data = msg.body.split(' '); + if (ctcp_data[0] === "[CTCP]" && ctcp_data.length >= 4) { + + if (state.show_ctcp_req) { + emit('ctcp-msg', '* |14[CTCP-REQUEST] |15' + ctcp_data[3] + ' |07on |15' + ctcp_data[2] + ' |07from |10' + msg.from_user); + } + if (ctcp_data[2] === "*" || ctcp_data[2].toUpperCase()===user.toUpperCase() || ctcp_data[2].toUpperCase()==="#"+state.room.toUpperCase() ) { + send_ctcp(ctcp_data[1], "[CTCP-REPLY]", ctcp_data[3].toUpperCase() + " " + ctcp_reply(ctcp_data[3].toUpperCase()) ); + } + + } else if (ctcp_data[0] === "[CTCP-REPLY]" && ctcp_data.length >= 3) { + if (msg.to_user.toUpperCase()===user.toUpperCase()) { + emit('ctcp-msg', '* |14[CTCP-REPLY] |10' + ctcp_data[1] + ' |15' + ctcp_data.slice(2).join(' ').trim()); + } + } + } } this.send_room_message = function (msg) { @@ -138,6 +217,10 @@ function MRC_Session(host, port, user, pass, alias) { this.send_notme = function (msg) { send("NOTME", "", "", msg); } + + this.send_action = function (msg) { + send("", "", state.room, msg); + } this.send_private_messsage = function (user, msg) { msg = '|11(|03Private|11)|07 ' + msg; @@ -243,6 +326,17 @@ function MRC_Session(host, port, user, pass, alias) { this.send_command('USERLIST', 'ALL'); } }, + j: { // shorthand for join + // TODO: is there an easier way to duplicate command functionality? + //help: 'Move to a new room: /join room_name', + callback: function (str) { // validate valid room name? + str = str.replace(/^#/, ''); + this.send_command(format('NEWROOM:%s:%s', state.room, str)); + state.room = str; + state.nicks = []; + this.send_command('USERLIST', 'ALL'); + } + }, motd: { //help: 'Display the Message of the Day' }, @@ -255,7 +349,8 @@ function MRC_Session(host, port, user, pass, alias) { } } }, - t: { + t: { // shorthand for msg + // TODO: is there an easier way to duplicate command functionality? //help: 'Send a private message: /t nick message goes here', callback: function (str) { const cmd = str.split(' '); @@ -263,6 +358,7 @@ function MRC_Session(host, port, user, pass, alias) { this.send_private_messsage(cmd[0], cmd.slice(1).join(' ')); } } + }, r: { //help: 'Reply to last private message: /r message goes here', @@ -285,6 +381,14 @@ function MRC_Session(host, port, user, pass, alias) { handle.close(); } }, + q: { // shorthand for quit + // TODO: is there an easier way to duplicate command functionality? + //help: 'Quit the program', + callback: function () { + emit('disconnect'); + handle.close(); + } + }, rooms: { //help: 'List available rooms', command: 'LIST' @@ -298,7 +402,32 @@ function MRC_Session(host, port, user, pass, alias) { callback: function (str) { this.send_command(format('NEWTOPIC:%s:%s', state.room, str)); } - } + }, + me: { + //me: 'Send an action to the server', + callback: function (str) { + this.send_action('|15* |13' + user + ' ' + str); + } + }, + ctcp: { + // outgoing ctcp request + callback: function (str) { + const cmd = str.split(' '); + var u = cmd[0]; + if (u) { + if (u === "*" || u.indexOf("#") === 0) { + u = ""; + } + send_ctcp(u, '[CTCP]', str.trim().toUpperCase()); + } + } + }, + toggle_ctcp: { + callback: function() { + state.show_ctcp_req = !state.show_ctcp_req; + emit('ctcp-msg', '* |14Incoming CTCP requests are now ' + (state.show_ctcp_req ? "|15shown" : "|12hidden") + '.'); + } + } }; Object.keys(commands).forEach(function (e) {