declare var setInterval: any; type type_conf = ( null | { port: int; verbosity: int; cleaning: { timeout_in_seconds: int; worker_interval_in_seconds: int; }; } ); type type_event = { timestamp: int; kind: string; data: any; }; type type_user = { name: string; role: string; }; type type_connection = { client: any; eventqueue: Array; termination: (null | int); }; type type_id = string; type type_model = { counter: int; connections: Record; }; type type_internal_request = { id: (null | type_id); action: string; data: any; }; type type_internal_response = any; /** * the node module "irc" */ var nm_irc: any; /** * gets the current UNIX timestamp */ function get_timestamp ( ): int { return Math.floor(Date.now()/1000); } /** * generates a unique 8 digit long string */ function generate_id ( model: type_model ): type_id { model.counter += 1; return model.counter.toFixed(0).padStart(8, '0'); } /** * writes a message to stderr */ function log ( conf: type_conf, level: int, incident: string, details: Record = {} ): void { if (level <= conf.verbosity) { process.stderr.write(`-- ${incident} | ${lib_json.encode(details)}\n`); } else { // do nothing } } /** * updates the termination timestamp of a connection, prolonging its lifetime */ function connection_touch ( conf: type_conf, connection: type_connection ): void { if (conf.cleaning.timeout_in_seconds !== null) { connection.termination = (get_timestamp() + conf.cleaning.timeout_in_seconds); } else { connection.termination = null; } } /** * gets a connection by its ID */ function get_connection ( conf: type_conf, model: type_model, id: type_id ): type_connection { if (! model.connections.hasOwnProperty(id)) { throw (new Error(`no connection for ID '${id}'`)); } else { const connection: type_connection = model.connections[id]; connection_touch(conf, connection); return connection; } } /** * executes a request */ async function execute ( conf: type_conf, model: type_model, internal_request: type_internal_request, ip_address: string ): Promise { switch (internal_request.action) { default: { throw (new Error(`unhandled action '${internal_request.action}'`)); break; } case "connect": { if (model.connections.hasOwnProperty(internal_request.id)) { throw (new Error("already connected")); } else { const id: type_id = generate_id(model); const client = new nm_irc.Client ( internal_request.data["server"], internal_request.data["nickname"], { "realName": "webirc", "userName": lib_sha256.get(ip_address).slice(0, 8), "channels": internal_request.data["channels"], "showErrors": true, "autoConnect": false, } ); let connection: type_connection = { "client": client, "eventqueue": [], "termination": null, }; client.addListener ( "message#", (nick, to, text, message) => { connection.eventqueue.push ({ "timestamp": get_timestamp(), "kind": "message_channel", "data": {"channel": to, "sender": nick, "content": text} }); } ); client.addListener ( "pm", (nick, text, message) => { connection.eventqueue.push ({ "timestamp": get_timestamp(), "kind": "message_query", "data": {"user_name": nick, "sender": nick, "content": text} }); } ); client.addListener ( "selfMessage", (to, text) => { if (to.charAt(0) === "#") { connection.eventqueue.push ({ "timestamp": get_timestamp(), "kind": "message_channel", "data": {"channel": to, "sender": null, "content": text} }); } else { connection.eventqueue.push ({ "timestamp": get_timestamp(), "kind": "message_query", "data": {"user_name": to, "sender": null, "content": text} }); } } ); client.addListener ( "topic", (channel, topic, nick, message) => { connection.eventqueue.push ({ "timestamp": get_timestamp(), "kind": "topic", "data": {"channel": channel, "content": topic} }); } ); client.addListener ( "names", (channel, users) => { connection.eventqueue.push ({ "timestamp": get_timestamp(), "kind": "user_list", "data": {"channel": channel, "users": Object.entries(users).map(([name, role]) => ({"name": name, "role": role}))} }); } ); client.addListener ( "nick", (oldnick, newnick, channels, message) => { connection.eventqueue.push ({ "timestamp": get_timestamp(), "kind": "user_renamed", "data": {"user_name_old": oldnick, "user_name_new": newnick} }); } ); client.addListener ( "join", (channel, nick, message) => { connection.eventqueue.push ({ "timestamp": get_timestamp(), "kind": "user_joined", "data": {"channel": channel, "user_name": nick} }); } ); client.addListener ( "part", (channel, nick, reason, message) => { connection.eventqueue.push ({ "timestamp": get_timestamp(), "kind": "user_parted", "data": {"channel": channel, "user_name": nick} }); } ); client.addListener ( "kick", (channel, nick, by, reason, message) => { connection.eventqueue.push ({ "timestamp": get_timestamp(), "kind": "user_kicked", "data": {"channel": channel, "user_name": nick, "op_name": by, "reason": reason} }); } ); client.addListener ( "quit", (nick, reason, channels, message) => { connection.eventqueue.push ({ "timestamp": get_timestamp(), "kind": "user_quit", "data": {"user_name": nick, "channels": channels} }); } ); client.addListener ( "error", (error) => { log(conf, 0, "irc_error", {"reason": error.message}); } ); client.connect ( 3, () => { model.connections[id] = connection; connection_touch(conf, connection); } ); return Promise.resolve(id); } break; } case "check": { try { get_connection(conf, model, internal_request.id); return Promise.resolve(true); } catch (error) { return Promise.resolve(false); } break; } case "disconnect": { const connection: type_connection = get_connection(conf, model, internal_request.id); delete model.connections[internal_request.id]; connection.client.disconnect("", () => {}); return Promise.resolve(null); break; } case "send_channel": { const connection: type_connection = get_connection(conf, model, internal_request.id); connection.client.say(internal_request.data["channel"], internal_request.data["content"]); return Promise.resolve(null); break; } case "send_query": { const connection: type_connection = get_connection(conf, model, internal_request.id); connection.client.say(internal_request.data["receiver"], internal_request.data["content"]); return Promise.resolve(null); break; } case "fetch": { const connection: type_connection = get_connection(conf, model, internal_request.id); const internal_response: type_internal_response = connection.eventqueue; connection.eventqueue = []; return Promise.resolve(internal_response); break; } } } /** * sets up the worker for terminating old connections */ function setup_cleaner ( conf: type_conf, model: type_model ): Promise { setInterval ( () => { const now: int = get_timestamp(); for (const [id, connection] of Object.entries(model.connections)) { if ((connection.termination !== null) && (now > connection.termination)) { delete model.connections[id]; connection.client.disconnect("timeout", () => {}); log(conf, 1, "connection terminated after timeout", {"id": id}); } } }, (conf.cleaning.worker_interval_in_seconds * 1000) ); return Promise.resolve(undefined); } /** * sets up the server, accepting HTTP request */ function setup_server ( conf: type_conf, model: type_model ): Promise { const server: lib_server.class_server = new lib_server.class_server ( conf.port, async (input: string, metadata?: lib_server.type_metadata): Promise => { const http_request: lib_http.type_request = lib_http.decode_request(input); log(conf, 2, "http_request", http_request); const internal_request: type_internal_request = lib_json.decode(http_request.body); log(conf, 1, "internal_request", internal_request); let internal_response: type_internal_response; let error: (null | Error); try { internal_response = await execute(conf, model, internal_request, metadata.ip_address); error = null; } catch (error_) { internal_response = null; error = error_; } let http_response: lib_http.type_response; if (error !== null) { log(conf, 0, "error_in_execution", {"reason": error.toString()}); http_response = { "statuscode": 500, "headers": {"Access-Control-Allow-Origin": "*", "Content-Type": "text/plain"}, "body": "error executing the request; check the server logs for details", }; } else { log(conf, 1, "internal_response", {"value": internal_response}); http_response = { "statuscode": 200, "headers": {"Access-Control-Allow-Origin": "*", "Content-Type": "application/json"}, "body": lib_json.encode(internal_response) } } log(conf, 2, "http_response", http_response); const output: string = lib_http.encode_response(http_response); return Promise.resolve(output); } ); server.start(); return Promise.resolve(undefined); } /** * initializes and starts the whole system */ async function main ( ): Promise { nm_irc = require("irc"); const conf: type_conf = await lib_plankton.file.read("conf.json").then(lib_json.decode); let model: type_model = { "counter": 0, "connections": {}, }; await Promise.all([ setup_cleaner(conf, model), setup_server(conf, model), ]); return Promise.resolve(undefined); } main().then(() => {}).catch((reason) => process.stderr.write(reason.toString()));