Skip to content
Snippets Groups Projects
Commit 75364a3c authored by echicken's avatar echicken
Browse files

An MRC client. A bit rough around the edges, but it'll do.

parent 47554591
Branches
Tags
No related merge requests found
server = localhost
port = 5000
ping_interval = 60
[startup]
room = lobby
motd = true
banners = true
[aliases]
/* $Id$ */
/**
* Multi Relay Chat Client Module
* echicken -at- bbs.electronicchicken.com
*
* I started out with good intentions. At least it works.
*/
load('sbbsdefs.js');
load('frame.js');
load('scrollbar.js');
load('inputline.js');
load(js.startup_dir + 'mrc-session.js');
var f = new File(js.startup_dir + 'mrc-client.ini');
f.open('r');
const settings = {
root: f.iniGetObject(),
startup: f.iniGetObject('startup'),
aliases: f.iniGetObject('aliases') || {}
};
f.close();
f = undefined;
const NICK_COLOURS = [
'\1h\1r',
'\1h\1g',
'\1h\1y',
'\1h\1b',
'\1h\1m',
'\1h\1c',
'\1h\1w',
// Low colours with reasonable contrast
'\1n\1r',
'\1n\1g',
'\1n\1y',
'\1n\1m',
'\1n\1c',
'\1n\1w'
];
const PIPE_COLOURS = [2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15];
function pipe_to_ctrl_a(str) {
str = str.replace(/\|00/g, "\1N\1K");
str = str.replace(/\|01/g, "\1N\1B");
str = str.replace(/\|02/g, "\1N\1G");
str = str.replace(/\|03/g, "\1N\1C");
str = str.replace(/\|04/g, "\1N\1R");
str = str.replace(/\|05/g, "\1N\1M");
str = str.replace(/\|06/g, "\1N\1Y");
str = str.replace(/\|07/g, "\1N\1W");
str = str.replace(/\|08/g, "\1H\1K");
str = str.replace(/\|09/g, "\1H\1B");
str = str.replace(/\|10/g, "\1H\1G");
str = str.replace(/\|11/g, "\1H\1C");
str = str.replace(/\|12/g, "\1H\1R");
str = str.replace(/\|13/g, "\1H\1M");
str = str.replace(/\|14/g, "\1H\1Y");
str = str.replace(/\|15/g, "\1H\1W");
str = str.replace(/\|16/g, "\001" + 0);
str = str.replace(/\|17/g, "\001" + 4);
str = str.replace(/\|18/g, "\001" + 2);
str = str.replace(/\|19/g, "\001" + 6);
str = str.replace(/\|20/g, "\001" + 1);
str = str.replace(/\|21/g, "\001" + 5);
str = str.replace(/\|22/g, "\001" + 3);
str = str.replace(/\|23/g, "\001" + 7);
return str;
}
function resize_nicklist(frames, nicks) {
const maxlen = Math.max(1, nicks.reduce(function (a, c) {
return c.length > a ? c.length : a;
}, 0));
frames.nicklist.moveTo(frames.top.x + frames.top.width - 1 - maxlen - 1, 2);
frames.nicklist_divider.moveTo(frames.nicklist.x, 2);
frames.nicks.moveTo(frames.nicklist.x + 1, 2);
frames.nicklist.width = maxlen + 2;
frames.nicks.width = maxlen + 1;
frames.output.width = frames.top.width - frames.nicklist.width - 1;
}
function redraw_nicklist(frames, nicks, colours) {
frames.nicks.clear();
nicks.forEach(function (e, i) {
frames.nicks.gotoxy(1, i + 1);
frames.nicks.putmsg(colours[e] + e + '\1n\1w');
});
}
function init_display() {
const w = console.screen_columns;
const h = console.screen_rows;
const f = { top: new Frame(1, 1, w, h, BG_BLACK|LIGHTGRAY) };
f.title = new Frame(1, 1, w, 1, BG_BLUE|WHITE, f.top);
f.output = new Frame(1, 2, 1, h - 3, BG_BLACK|LIGHTGRAY, f.top);
f.divider = new Frame(1, h - 1, w, 1, BG_BLUE|WHITE, f.top);
f.nicklist = new Frame(w - 2, 2, 2, h - 3, BG_BLACK|LIGHTGRAY, f.top);
f.nicklist_divider = new Frame(w - 2, 2, 1, h - 3, BG_BLACK|LIGHTGRAY, f.nicklist);
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);
for (var n = 0; n < f.nicklist_divider.height; n++) {
f.nicklist_divider.gotoxy(1, n + 1);
f.nicklist_divider.putmsg(ascii(179));
}
f.output_scroll = new ScrollBar(f.output, { autohide: true });
f.nick_scroll = new ScrollBar(f.nicks, { autohide: true });
f.output.word_wrap = true;
f.divider.gotoxy(f.divider.width - 5, 1);
f.divider.putmsg('/help');
f.top.open();
return f;
}
function display_message(frames, msg, colour) {
const body = pipe_to_ctrl_a(msg.body || '').split(' ');
frames.output.putmsg(body[0] + '\1n\1w: ' + body.slice(1).join(' ') + '\1n\1w\r\n');
}
function display_server_message(frames, msg) {
frames.output.putmsg('\1h\1w' + pipe_to_ctrl_a(msg || '') + '\r\n');
}
function display_title(frames, room, title) {
frames.title.clear();
frames.title.putmsg('MRC - #' + room + ' - ' + title);
}
function set_alias(alias) {
const f = new File(js.startup_dir + 'mrc-client.ini');
f.open('r+');
f.iniSetValue('aliases', user.alias, alias);
f.close();
}
function new_alias() {
const alias = format(
'|%02d%s',
PIPE_COLOURS[Math.floor(Math.random() * PIPE_COLOURS.length)],
user.alias.replace(/\s/g, '_')
);
set_alias(alias);
settings.aliases[user.alias] = alias;
}
function main() {
var msg;
var input_state = 'chat';
var break_loop = false;
const nick_colours = {};
const session = new MRC_Session(
settings.root.server,
settings.root.port,
user.alias,
user.security.password,
settings.aliases[user.alias] || new_alias(user.alias)
);
const frames = init_display();
const inputline = new InputLine(frames.input);
inputline.show_cursor = true;
inputline.max_buffer = 140 - settings.aliases[user.alias].length - 1;
resize_nicklist(frames, []);
redraw_nicklist(frames, []);
session.connect();
session.on('banner', function (msg) {
display_server_message(frames, msg);
});
session.on('disconnect', function () {
break_loop = true;
});
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('\1h\1w/\1h\1c%s \1h\1w- \1n\1w%s', cmd, help));
}
});
session.on('local_help', function (msg) {
display_server_message(frames, '\1h\1w/\1h\1cscroll \1h\1w- \1n\1wScroll the output area');
display_server_message(frames, '\1h\1w/\1h\1cscroll_nicks \1h\1w- \1n\1wScroll the nicklist');
display_server_message(frames, '\1h\1w/\1h\1cnick_prefix \1h\1w- \1n\1wSet a single-character prefix for your handle, eg. /nick_prefix @');
display_server_message(
frames,
'\1h\1w/\1h\1cnick_color \1h\1w- \1n\1wSet 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, '\1h\1w/\1h\1cnick_suffix \1h\1w- \1n\1wSet an eight-character suffix for your handle, eg. /nick_suffix <poop>');
display_server_message(frames, '\1h\1w/\1h\1cquit \1n\1w- \1h\1wExit the program');
});
session.on('message', function (msg) {
if (msg.from_user == 'SERVER') {
display_server_message(frames, msg.body);
} else {
display_message(frames, msg, nick_colours[msg.from_user] || '');
}
});
session.on('nicks', function (nicks) {
nicks.forEach(function (e, i) {
nick_colours[e] = NICK_COLOURS[i % NICK_COLOURS.length];
});
resize_nicklist(frames, nicks);
redraw_nicklist(frames, nicks, nick_colours);
});
session.on('sent_privmsg', function (user, msg) {
display_message(frames, { body: '--> ' + (nick_colours[user] || '') + user + ' ' + msg });
});
session.on('topic', function (room, topic) {
display_title(frames, room, topic);
});
if (settings.startup.motd) session.motd();
if (settings.startup.banners) session.banners();
if (settings.startup.room) session.join(settings.startup.room);
var cmd, line, user_input;
while (!js.terminated && !break_loop) {
session.cycle();
user_input = inputline.getkey();
if (typeof user_input != 'undefined') {
if (input_state == 'chat') {
if (user_input.substring(0, 1) == '/') { // It's a command
cmd = user_input.split(' ');
cmd[0] = cmd[0].substr(1).toLowerCase();
switch (cmd[0]) {
case 'rainbow':
line = user_input.replace(/^\/rainbow\s/, '').split('').reduce(function (a, c) {
var cc = PIPE_COLOURS[Math.floor(Math.random() * PIPE_COLOURS.length)];
a += format('|%02d%s', cc, c);
return a;
}, '').substr(0, inputline.max_buffer).replace(/\\s\|\d*$/, '');
session.send_room_message(line);
break;
case 'scroll':
input_state = 'scroll';
frames.divider.clear();
frames.divider.putmsg('UP/DOWN to scroll, ENTER to return');
break;
case 'scroll_nicks':
input_state = 'scroll_nicks';
frames.divider.clear();
frames.divider.putmsg('UP/DOWN to scroll nicklist, ENTER to return');
break;
case 'nick_prefix':
if (cmd.length == 2) {
if (cmd[1].length == 1 || cmd[1].search(/^\|\d\d\S$/) == 0) {
var re = new RegExp('^(\\|\\d\\d)*\\S*' + user.alias, 'i');
settings.aliases[user.alias] = settings.aliases[user.alias].replace(re, cmd[1] + user.alias);
set_alias(settings.aliases[user.alias]);
session.alias = settings.aliases[user.alias];
}
}
break;
case 'nick_color':
case 'nick_colour':
if (cmd.length == 2) {
if (PIPE_COLOURS.indexOf(parseInt(cmd[1])) >= 0) {
var re = new RegExp('(\\|\\d\\d)*' + user.alias, 'i');
settings.aliases[user.alias] = settings.aliases[user.alias].replace(re, format('|%02d%s', cmd[1], user.alias));
set_alias(settings.aliases[user.alias]);
session.alias = settings.aliases[user.alias];
}
}
break;
case 'nick_suffix':
if (cmd.length == 2) {
if (cmd[1].replace(/(\\|\\d\\d)/g, '').length <= 8) {
var re = new RegExp(user.alias + '.*$', 'i');
settings.aliases[user.alias] = settings.aliases[user.alias].replace(re, user.alias + cmd[1]);
set_alias(settings.aliases[user.alias]);
session.alias = settings.aliases[user.alias];
}
} else { // Clearing a nick suffix
var re = new RegExp(user.alias + '.*$', 'i');
settings.aliases[user.alias] = settings.aliases[user.alias].replace(re, user.alias);
set_alias(settings.aliases[user.alias]);
session.alias = settings.aliases[user.alias];
}
break;
default:
if (typeof session[cmd[0]] == 'function') {
session[cmd[0]](cmd.slice(1).join(' '));
}
break;
}
} else if (user_input == '\t') { // Nick completion
cmd = inputline.buffer.split(' ').pop().toLowerCase();
var nick = '';
session.nicks.some(function (e) {
if (e.substring(0, cmd.length).toLowerCase() != cmd) return false;
nick = e;
return true;
});
if (nick != '') {
// lol this is horrible
// to do: add some methods to inputline
for (var n = 0; n < cmd.length; n++) {
console.ungetstr('\b');
inputline.getkey();
}
for (var n = 0; n < nick.length; n++) {
console.ungetstr(nick[n]);
inputline.getkey();
}
}
} 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
) {
session.send_room_message(user_input);
}
} else if (input_state == 'scroll' || input_state == 'scroll_nicks') {
var sframe = input_state == 'scroll' ? frames.output : frames.nicks;
// to do: page up, page down
if (user_input == KEY_UP && sframe.offset.y > 0) {
sframe.scroll(0, -1);
} else if (user_input == KEY_DOWN && sframe.offset.y + sframe.height < sframe.data_height) {
sframe.scroll(0, 1);
} else if (user_input == '' || user_input == 'q') {
frames.output.scrollTo(1, frames.output.data_height - frames.output.height);
frames.output_scroll.cycle();
frames.divider.clear();
frames.divider.gotoxy(frames.divider.width - 5, 1);
frames.divider.putmsg('/help');
input_state = 'chat';
}
}
}
if (frames.top.cycle()) {
frames.output_scroll.cycle();
frames.nick_scroll.cycle();
console.gotoxy(console.screen_columns, console.screen_rows);
}
yield();
}
}
main();
server = mrc.bottomlessabyss.net
port = 5000
timeout = 10
ping_interval = 60
reconnect_delay = 30
[info]
;web = https://vurt.synchro.net:8443/
;telnet = vurt.synchro.net:23
;ssh = vurt.synchro.net:22
;sysop = Bob Swindle
;description = Vurtrowen
;system_name = |17|11my custom |03bbs name
;platform = Not_Synchronet_6.6.6z/Amiga_OS/1.2.9
/* $Id$ */
/**
* Multi Relay Chat connector (multiplexer) service
* echicken -at- bbs.electronicchicken.com
*/
load('sbbsdefs.js');
load('sockdefs.js');
if (!js.global.server) {
server = {};
server.socket = new Socket(SOCK_STREAM, 'MRC_PROXY');
server.socket.bind(5000, '127.0.0.1');
server.socket.listen();
}
js.branch_limit = 0;
server.socket.nonblocking = true;
var f = new File(js.exec_dir + 'mrc-connector.ini');
f.open('r');
const settings = f.iniGetObject();
const system_info = f.iniGetObject('info') || {};
f.close();
f = undefined;
const PROTOCOL_VERSION = '1.2.9';
const MAX_LINE = 256;
const FROM_SITE = system.qwk_id.toLowerCase();
const SYSTEM_NAME = system_info.system_name || system.name;
const clients = {};
var last_connect = 0;
// User / site name must be ASCII 33-125, no MCI, 30 chars max, underscores
function sanitize_name(str) {
return str.replace(
/\s/g, '_'
).replace(
/[^\x21-\x7D]|(\|\w\w)/g, '' // Non-printable & MCI
).substr(
0, 30
);
}
// Room name must be ASCII 33-125, no MCI, 30 chars max
function sanitize_room(str) {
return str.replace(/[^\x21-\x7D]|(\|\w\w)/g, '').substr(0, 30);
}
// Message text must be ASCII 32-125
function sanitize_message(str) {
return str.replace(/[^\x20-\x7D]/g, '');
}
function strip_mci(str) {
return str.replace(/\|\w\w/g, '');
}
function parse_message(line) {
const msg = line.split('~');
if (msg.length < 7) {
log(LOG_ERR, 'Invalid MRC line: ' + line);
return;
}
return {
from_user: msg[0],
from_site: msg[1],
from_room: msg[2],
to_user: msg[3],
to_site: msg[4],
to_room: msg[5],
body: msg[6]
};
}
function validate_message(msg) {
return (
typeof msg == 'object'
&& typeof msg.from_room == 'string'
&& typeof msg.to_user == 'string'
&& typeof msg.to_site == 'string'
&& typeof msg.to_room == 'string'
&& typeof msg.body == 'string'
);
}
function client_receive(c, no_log) {
const line = c.socket.recvline(1024, settings.timeout);
if (!line || line == '') return;
try {
if (!no_log) log(LOG_DEBUG, 'From client: ' + line);
const msg = JSON.parse(line);
return msg;
} catch (err) {
log(LOG_ERR, 'Invalid message from client: ' + line);
}
}
function client_close(sock, message) {
sock.send(JSON.stringify(message) + '\n');
sock.close();
}
function client_accept() {
if (!server.socket.poll()) return;
var c = server.socket.accept();
if (!c) return;
c.nonblocking = true;
const msg = client_receive({ socket: c }, true);
if (!msg) return;
if (!msg.username || !msg.password) {
log(LOG_ERR, 'Invalid handshake from client: ' + JSON.stringify(msg));
client_close(c, { error: 'Invalid handshake' });
return;
}
const un = system.matchuser(msg.username);
if (!un) {
log(LOG_ERR, 'Invalid username from client: ' + msg.username);
client_close(c, { error: 'Invalid username' }); // Leaks user existence
return;
}
var u = new User(un);
if (msg.password != u.security.password) {
log(LOG_ERR, 'Invalid password for: ' + msg.username);
client_close(c, { error: 'Invalid password' });
return;
}
clients[c.descriptor] = {
username: sanitize_name(msg.username),
socket : c,
ping: 0,
alias: msg.alias.replace(/\s/g, '_')
};
c.sendline(JSON.stringify({ error: null }));
}
// Forward a message to all applicable clients
function client_send(message, username) {
Object.keys(clients).forEach(function (e) {
if (!username || clients[e].username == username) {
log(LOG_DEBUG, 'Forwarding message to ' + clients[e].username);
clients[e].socket.sendline(JSON.stringify(message));
}
});
}
function mrc_connect(host, port) {
if (time() - last_connect < settings.reconnect_delay) return false;
last_connect = time();
const sock = new Socket();
sock.nonblocking = true;
log(LOG_INFO, 'Connecting to ' + host + ':' + port);
if (!sock.connect(host, port, settings.timeout)) return false;
const platform = format(
'Synchronet %s%s/%s/%s',
system.version, system.revision, system.platform, PROTOCOL_VERSION
).replace(/\s/g, '_');
const line = SYSTEM_NAME + '~' + (system_info.platform || platform);
log(LOG_DEBUG, 'To MRC: ' + line);
sock.send(line + '\n');
while (sock.is_connected) {
yield();
// Just waiting for the HELLO line; we could verify, but meh
if (!sock.data_waiting) continue;
log(LOG_DEBUG, 'From MRC: ' + sock.recvline(512, settings.timeout));
break;
}
// Could check if HTTPS is available and if SSH is enabled (sbbs.ini)
mrc_send_info(sock, 'WEB', system_info.web || 'http://' + system.inet_addr + '/');
mrc_send_info(sock, 'TEL', system_info.telnet || system.inet_addr);
mrc_send_info(sock, 'SSH', system_info.ssh || system.inet_addr);
mrc_send_info(sock, 'SYS', system_info.sysop || system.operator);
mrc_send_info(sock, 'DSC', system_info.description || system.name);
return sock;
}
function mrc_send(sock, from_user, from_room, to_user, to_site, to_room, msg) {
if (!sock.is_connected) throw new Error('Not connected.');
const m = sanitize_message(msg);
const line = [
from_user,
FROM_SITE,
sanitize_room(from_room),
sanitize_name(to_user || ''),
sanitize_name(to_site || ''),
sanitize_room(to_room || ''),
m
].join('~') + '~';
log(LOG_DEBUG, 'To MRC: ' + line);
return sock.send(line + '\n');
}
function mrc_send_info(sock, field, str) {
mrc_send(sock, 'CLIENT', '', 'SERVER', 'ALL', '', 'INFO' + field + ':' + str);
}
function mrc_receive(sock) {
var line, message;
while (sock.data_waiting) {
line = sock.recvline(MAX_LINE, settings.timeout);
if (!line || line == '') break;
log(LOG_DEBUG, 'From MRC: ' + line);
message = parse_message(line);
if (!message) continue;
if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') {
mrc_send(sock, 'CLIENT', '', 'SERVER', 'ALL', '', 'IMALIVE:' + SYSTEM_NAME);
return;
}
if (['', 'ALL', FROM_SITE].indexOf(message.to_site) > -1) {
if (['', 'CLIENT', 'ALL'].indexOf(message.to_site) > -1) {
// Forward to all clients
client_send(message);
} else {
// Send to this user
client_send(message, message.to_user);
}
}
yield();
}
}
function main() {
var mrc_sock;
var die = false;
while (!die && !js.terminated) {
yield();
if (!mrc_sock || !mrc_sock.is_connected) {
mrc_sock = mrc_connect(settings.server, settings.port);
continue;
}
client_accept();
Object.keys(clients).forEach(function (e, i) {
if (!clients[e].socket.is_connected) {
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];
} else {
var msg;
while (clients[e].socket.data_waiting) {
msg = client_receive(clients[e]);
if (!msg) break;
if (!validate_message(msg)) break; // Log
mrc_send(
mrc_sock,
clients[e].username,
msg.from_room,
msg.to_user,
msg.to_site,
msg.to_room,
msg.body
);
yield();
}
}
});
mrc_receive(mrc_sock);
}
log(LOG_INFO, 'Disconnecting from MRC');
mrc_send(mrc_sock, 'CLIENT', '', 'SERVER', 'ALL', '', 'SHUTDOWN');
const stime = time();
while (mrc_sock.is_connected && time() - stime > settings.timeout) {
yield();
}
if (mrc_sock.is_connected) mrc_sock.close();
log(LOG_INFO, 'Disconnected. Exiting.');
}
main();
/* $Id$ */
// 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 handle = new Socket();
const state = {
room: '',
room_topic: '',
nicks: [],
output_buffer: [],
last_ping: 0,
last_send: 0,
alias: alias || user
};
const callbacks = {
error: []
};
function send(to_user, to_site, to_room, body) {
if (body == '' || body == state.alias) return;
state.output_buffer.push({
from_room: state.room,
to_user: to_user,
to_site: to_site,
to_room: to_room,
body: body
});
}
function send_command(command, to_site) {
send('SERVER', to_site || '', state.room, command);
}
function send_message(to_user, to_room, body) {
if (body.length + state.alias.length + 1 > 140) {
word_wrap(body, 140 - 1 - state.alias.length).split(/\n/).forEach(function (e) {
send(to_user, '', to_room, state.alias + ' ' + e);
});
} else {
send(to_user, '', to_room, state.alias + ' ' + body);
}
}
function emit() {
if (!Array.isArray(callbacks[arguments[0]])) return;
const rest = Array.prototype.slice.call(arguments, 1);
callbacks[arguments[0]].forEach(function (e) {
if (typeof e != 'function') return;
e.apply(null, rest);
});
}
function handle_message(msg) {
if (msg.from_user == 'SERVER') {
const params = msg.body.split(':');
switch (params[0]) {
case 'BANNER':
emit('banner', params[1].replace(/^\s+/, ''));
break;
case 'ROOMTOPIC':
emit('topic', params[1], params.slice(2).join(' '));
break;
case 'USERLIST':
state.nicks = params[1].split(',');
emit('nicks', state.nicks);
break;
default:
emit('message', msg);
break;
}
} else if (msg.to_user == 'SERVER') {
if (msg.body == 'LOGOFF') {
const uidx = state.nicks.indexOf(msg.from_user);
if (uidx > -1) {
state.nicks.splice(uidx, 1);
emit('nicks', state.nicks);
}
}
} else if (msg.to_user == '' || user.toLowerCase() == msg.to_user.toLowerCase()) {
if (msg.to_room == '' || msg.to_room == state.room) {
emit('message', msg);
}
if (msg.to_room == state.room
&& state.nicks.indexOf(msg.from_user) < 0
) {
send_command('USERLIST', 'ALL');
}
} else if (msg.to_user == 'NOTME') {
if (msg.body.search(/left\ the\ (room|server)\.*$/ > -1)) {
const udix = state.nicks.indexOf(msg.from_user);
if (uidx > -1) {
state.nicks.splice(uidx, 1);
emit('nicks', state.nicks);
}
} else if (msg.body.search(/just joined room/) > -1) {
send_command('USERLIST', 'ALL');
}
}
}
this.send_room_message = function (msg) {
send_message('', state.room, msg);
}
this.send_private_messsage = function (user, msg) {
msg = '|11(|03Private|11)|07 ' + msg;
send_message(user, state.room, msg);
emit('sent_privmsg', user, msg);
}
this.send_command = function (command, to_site) {
send_command(command, to_site || '');
}
this.connect = function () {
handle.connect(host || 'localhost', port || 50000);
handle.send(JSON.stringify({
username: user,
password: pass,
alias: state.alias
}) + '\r\n');
this.send_command('USERLIST', 'ALL');
}
this.disconnect = function () {
handle.close();
}
this.on = function (evt, callback) {
if (typeof callbacks[evt] == 'undefined') callbacks[evt] = [];
callbacks[evt].push(callback);
return callbacks[evt].length;
}
this.off = function (evt, id) {
if (Array.isArray(callbacks[evt])) callbacks[evt][id] = null;
}
this.cycle = function () {
var msg;
while (handle.data_waiting) {
try {
msg = JSON.parse(handle.recvline(), 1024, 10);
if (msg.body) handle_message(msg);
} catch (err) {
callbacks.error.forEach(function (e) {
e(err);
});
}
}
if (time() - state.last_ping >= 60) {
this.send_command('IAMHERE');
state.last_ping = time();
}
if (state.output_buffer.length && time() - state.last_send >= 1) {
handle.send(JSON.stringify(state.output_buffer.shift()) + '\r\n');
state.last_send = time();
}
}
const commands = {
banners: {
help: 'List of banners from server'
},
chatters: {
help: 'List current users'
},
connected: {
help: 'List connected BBSs'
},
help: {
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');
}
},
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',
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');
}
},
meetups: {
help: 'Display information about upcoming meetups'
},
motd: {
help: 'Display the Message of the Day'
},
msg: {
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) {
this.send_private_messsage(cmd[0], cmd.slice(1).join(' '));
}
}
},
notify: {
help: 'Send a notification message to the server (what?)',
callback: function (str) {
this.send_command('NOTIFY:' + str, 'ALL');
}
},
quit: {
help: 'Quit the program',
callback: function () {
handle.close();
emit('disconnect');
}
},
rooms: {
help: 'List available rooms',
command: 'LIST'
},
stats: {
help: 'Return anonymous server stats'
},
topic: {
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'
}
};
Object.keys(commands).forEach(function (e) {
if (commands[e].callback) {
this[e] = commands[e].callback;
} else if (commands[e].command) {
this[e] = function () {
this.send_command(commands[e].command.toUpperCase());
}
} else {
this[e] = function () {
this.send_command(e.toUpperCase());
}
}
}, this);
Object.keys(state).forEach(function (e) {
Object.defineProperty(this, e, {
get: function () {
return state[e];
},
set: function (v) {
state[e] = v;
}
});
}, this);
}
Multi Relay Chat (MRC) client for Synchronet BBS 3.17b+
echicken -at- bbs.electronicchicken.com
1) Quick Start
2) Client -> Server -> Server
3) Customization
4) Support
1) Quick Start
The following assumes that Synchronet is installed at /sbbs or c:\sbbs. Adjust
paths accordingly if your setup differs.
- Copy mrc-connector.example.ini to mrc-connector.ini
- Copy mrc-client.example.ini to mrc-client.ini
- Edit '/sbbs/ctrl/services.ini' and paste in the following section:
[MRC-Connector]
Port=5000
Options=STATIC | LOOP
Command=../xtrn/mrc/mrc-connector.js
- In SCFG, add a new External Program with the following options, leaving all
other options at their default values:
Name: MRC
Internal Code: MRC
Start-up Directory: /sbbs/xtrn/mrc
Command Line: ?mrc-client.js
- Your services and BBS threads should automatically recycle once they are free
to do so, but you may want to restart your BBS now to force these changes to
take effect.
2) Client -> Server -> Server
MRC-Connector is a service that runs locally on your BBS. It maintains a
connection with a remote MRC server, and passes traffic between the remote
server and local clients. Local clients (users running mrc-client.js) connect
to MRC-Connector rather than connecting to the remote MRC server directly.
You do not have to, and should not, open the MRC-Connector port to the outside
world. Clients are connecting to it from within your BBS, and in the typical
Synchronet configuration, that means they're coming from the same machine.
3) Customization
Because MRC was originally a Mystic thing, you can use pipe codes wherever
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:
- You can adjust the 'server' and 'port' values if you wish to connect to
something other than the default MRC server.
- You can uncomment and edit the fields in the [info] section to alter what
information is shown about your BBS via the /info command. If you do not
uncomment these fields, defaults will be read from your system config.
- Spaces and colour codes are not allowed in the 'platform' value, and editing
this field is generally discouraged. The trailing MRC version number may be
critical.
mrc-client.ini:
- The 'server' and 'port' values dictate where mrc-client.js will connect to
MRC-Connector. The defaults should suffice unless you deviated from the
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.
4) Support
Nope. ;)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment