backend work

This commit is contained in:
Evert Prants 2016-09-24 00:38:09 +03:00
parent 2b44cec752
commit 7d90a97620
11 changed files with 471 additions and 45 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/node_modules/
/build/
/client.config.toml

View File

@ -0,0 +1,9 @@
[server]
port=8080
[client]
username="teemant"
realname="A Teemant User"
default_quit_msg="Teemant IRC"
default_part_msg="Bye!"
secure_by_default=false

View File

@ -13,9 +13,9 @@
"license": "MIT",
"dependencies": {
"express": "^4.14.0",
"jade": "^1.11.0",
"morgan": "^1.7.0",
"socketio": "^1.0.0"
"socketio": "^1.0.0",
"toml": "^2.3.0"
},
"repository": {
"type": "git",

View File

@ -376,7 +376,11 @@ body {
color: #03a9f4;
font-weight: bold;
}
.message.m_nick .content {
.message.m_motd .content {
font-family: monospace;
}
.message.m_nick .content,
.message.m_notice .content {
color: #ff9800;
font-weight: bold;
}

View File

@ -27,4 +27,5 @@
<path d="M239.4,136.001c-57,0-103.3,46.3-103.3,103.3s46.3,103.3,103.3,103.3s103.3-46.3,103.3-103.3S296.4,136.001,239.4,136.001
z M239.4,315.601c-42.1,0-76.3-34.2-76.3-76.3s34.2-76.3,76.3-76.3s76.3,34.2,76.3,76.3S281.5,315.601,239.4,315.601z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -4,6 +4,7 @@ window.irc = {
timestamps: true,
timestampFormat: "HH:mm:ss",
serverData: {},
serverChatQueue: {},
chatType: "simple"
};
@ -125,6 +126,20 @@ Date.prototype.format = function (format, utc){
return format;
};
function linkify(text) {
// see http://daringfireball.net/2010/07/improved_regex_for_matching_urls
let re = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/gi;
let parsed = text.replace(re, function(url) {
// turn into a link
let href = url;
if (url.indexOf('http') !== 0) {
href = 'http://' + url;
}
return '<a href="' + href + '" target="_blank">' + url + '</a>';
});
return parsed;
}
function removeClass(element, cl) {
let classList = element.className.split(" ");
remove_str(classList, cl);
@ -154,6 +169,8 @@ let composer = {
if(irc.timestamps)
element.innerHTML += "<span class='timestamp'>"+time.format(irc.timestampFormat)+"</span>&nbsp;";
message = linkify(message);
switch(type) {
case "action":
element.innerHTML += "<span class='asterisk'>*</span>&nbsp;<span class='actionee'>"+sender+"</span>&nbsp;";
@ -250,6 +267,7 @@ class Nicklist {
}
nickAddObject(obj) {
if(this.getNickIndex(obj.nickname) != null) return;
this.nicks.push(obj);
}
@ -424,7 +442,7 @@ class Buffer {
if(this.topic != null && this.topic != "") {
addClass(clientdom.chat, 'vtopic');
clientdom.topicbar.innerHTML = this.topic;
clientdom.topicbar.innerHTML = linkify(this.topic);
}
this.renderMessages();
@ -455,7 +473,7 @@ class Buffer {
topicChange(topic) {
if(this.active) {
clientdom.topicbar.innerHTML = topic;
clientdom.topicbar.innerHTML = linkify(topic);
if(this.topic == null)
addClass(clientdom.chat, "vtopic");
@ -463,8 +481,8 @@ class Buffer {
this.topic = topic;
}
addMessage(message, sender, type) {
let mesg = {message: message, sender: sender, type: type, time: new Date()}
addMessage(message, sender, type, time) {
let mesg = {message: message, sender: sender, type: type, time: time || new Date()}
this.messages.push(mesg);
if(this.active)
@ -689,6 +707,15 @@ class IRCChatWindow {
this.buffers.push(newServer);
this.render(newServer);
this.firstServer = false;
if(irc.serverChatQueue[serverinfo.address]) {
for(let a in irc.serverChatQueue[serverinfo.address]) {
let mesg = irc.serverChatQueue[serverinfo.address][a];
newServer.addMessage(mesg.message, mesg.from, mesg.type, mesg.time);
}
delete irc.serverChatQueue[serverinfo.address];
}
}
createBuffer(server, name, type, autoswitch) {
@ -918,16 +945,24 @@ window.onload = function() {
else if(data['topic'])
irc.chat.topicChange(data.channel, data.server, data.topic, null);
else if(data['set_by'])
irc.chat.messageBuffer(data.channel, data.server, {message: "Topic set by "+data.set_by+" on "+new Date(data.time), type: "topic", from: null});
irc.chat.messageBuffer(data.channel, data.server, {message: "Topic set by "+data.set_by+" on "+new Date(data.time*1000), type: "topic", from: null});
break;
case "nick_change":
irc.chat.nickChange(data.server, data.nick, data.newNick);
break;
case "server_message":
irc.chat.messageBuffer(data.server, data.server, {message: data.message, type: data.messageType, from: null});
if(irc.chat.getBuffersByServer(data.server).length == 0) {
if(!irc.serverChatQueue[data.server]) {
irc.serverChatQueue[data.server] = [];
} else {
irc.serverChatQueue[data.server].push({type: data.messageType, message: data.message, from: data.from || null, time: new Date()});
}
} else {
irc.chat.messageBuffer(data.server, data.server, {message: data.message, type: data.messageType, from: data.from || null});
}
break;
case "connect_message":
irc.auther.authMessage(data.data, data.error);
irc.auther.authMessage(data.message, data.error);
break;
}
});

View File

@ -322,7 +322,9 @@ body
&.m_topic .content
color: #03A9F4;
font-weight: bold;
&.m_nick .content
&.m_motd .content
font-family: monospace;
&.m_nick .content, &.m_notice .content
color: #FF9800;
font-weight: bold;
&.m_action .actionee

14
server/config.js Normal file
View File

@ -0,0 +1,14 @@
let fs = require('fs');
let toml = require('toml');
let filename = __dirname+'/../client.config.toml';
let config;
try {
config = toml.parse(fs.readFileSync(filename));
} catch (e) {
throw 'config.toml parse error: ' + e;
console.error(e.stack);
}
module.exports = config;

284
server/irc.js Normal file
View File

@ -0,0 +1,284 @@
let EventEmitter = require('events').EventEmitter;
let net = require('net');
let configuration = require(__dirname+"/config");
let parse = require(__dirname+"/parser");
let defaultConfig = {
nickname: "teemant",
username: configuration.client.username,
realname: configuration.client.realname,
server: "localhost",
port: 6667,
autojoin: [],
secure: configuration.client.secure_by_default,
password: "",
address: "0.0.0.0"
}
if (!String.prototype.format) {
String.prototype.format = function() {
var args = arguments;
return this.replace(/{(\d+)}/g, function(match, number) {
return typeof args[number] != undefined ? args[number] : match;
});
};
}
class ConfigPatcher {
constructor(provided, defaults) {
this.result = defaults;
this.patches = provided;
}
patch() {
for(let a in this.patches) {
this.result[a] = this.patches[a];
}
return this.result;
}
}
class IRCConnectionHandler {
constructor(connection) {
this.conn = connection;
}
handleServerLine(line) {
console.log(line);
if(this.conn.queue["supportsmsg"] && line.command != "005") {
delete this.conn.queue["supportsmsg"];
if(this.conn.config.autojoin.length > 0)
for(let t in this.conn.config.autojoin)
this.conn.socket.write('JOIN {0}\r\n'.format(this.conn.config.autojoin[t]));
this.conn.emit('authenticated', {});
}
let serverName = this.conn.config.server;
let realServerName = this.conn.data.actualServer;
switch(line.command) {
case "error":
this.conn.emit("error", {type: "irc_error", raw: line.raw});
break;
case "001":
this.conn.data.actualServer = line.user.host;
break
case "005":
if(!this.conn.queue["supportsmsg"])
this.conn.queue["supportsmsg"] = true;
let argv = line.arguments.slice(1);
for(let a in argv) {
let t = argv[a];
if(t.indexOf('=') != -1) {
t = t.split('=');
if(t[0] === 'PREFIX') {
let d = t[1].match(/\((\w+)\)(.*)/);
let r = d[1].split('');
let aa = d[2].split('');
for(let b in r)
this.conn.data.supportedModes[r[b]] = aa[b];
} else if(t[0] === 'NETWORK') {
this.conn.data.network = t[1];
}
this.conn.data.serverSupports[t[0]] = t[1];
} else {
this.conn.data.serverSupports[t] = true;
}
}
break;
case "JOIN":
this.conn.emit('pass_to_client', {type: "event_join_channel", user: line.user, channel: line.trailing, server: serverName });
break;
case "PART":
this.conn.emit('pass_to_client', {type: "event_part_channel", user: line.user, channel: line.arguments[0], reason: line.trailing, server: serverName });
break;
case "QUIT":
this.conn.emit('pass_to_client', {type: "event_quit", user: line.user, reason: line.trailing, server: serverName });
break;
case "353":
if(!this.conn.queue["names"])
this.conn.queue['names'] = {};
let splittrail = line.trailing.split(' ');
for(let a in splittrail) {
let nick = splittrail[a];
if(nick.trim() == "") continue;
if(this.conn.queue["names"][line.arguments[2]])
this.conn.queue["names"][line.arguments[2]].push(nick);
else
this.conn.queue["names"][line.arguments[2]] = [nick];
}
break;
case "366":
if(!this.conn.queue["names"]) break;
if(this.conn.queue["names"][line.arguments[1]]) {
this.conn.emit('pass_to_client', {type: "channel_nicks", channel: line.arguments[1], nicks: this.conn.queue["names"][line.arguments[1]], server: serverName});
delete this.conn.queue["names"][line.arguments[1]];
}
if(Object.keys(this.conn.queue["names"]).length == 0)
delete this.conn.queue["names"];
break;
case "PRIVMSG":
let type = "privmsg";
if(line.trailing.indexOf('\x01ACTION') == 0) {
line.trailing = line.trailing.substring(8);
line.trailing.substring(0, line.trailing.length-1);
type = "action";
}
if(line.user.nickname != "")
this.conn.emit('pass_to_client', {type: "message", messageType: type, to: line.arguments[0],
user: line.user, message: line.trailing, server: serverName});
else
this.conn.emit('pass_to_client', {type: "server_message", messageType: type, message: line.trailing, server: serverName, from: realServerName});
break;
case "NOTICE":
if(line.user.nickname != "")
this.conn.emit('pass_to_client', {type: "message", messageType: "notice", to: line.arguments[0],
user: line.user, message: line.trailing, server: serverName});
else
this.conn.emit('pass_to_client', {type: "server_message", messageType: "notice", message: line.trailing, server: serverName, from: realServerName});
break;
case "NICK":
this.conn.emit('pass_to_client', {type: "nick_change", nick: line.user.nickname, newNick: line.arguments[0], server: serverName});
break;
case "KICK":
this.conn.emit('pass_to_client', {type: "event_kick_channel", user: line.user, channel: line.arguments[0], kickee: line.arguments[1], server: serverName});
break;
case "TOPIC":
this.conn.emit('pass_to_client', {type: "channel_topic", channel: line.arguments[0], set_by: line.user.nickname, topic: line.trailing, server: serverName});
break;
case "332":
this.conn.emit('pass_to_client', {type: "channel_topic", channel: line.arguments[1], topic: line.trailing, server: serverName});
break;
case "333":
this.conn.emit('pass_to_client', {type: "channel_topic", channel: line.arguments[1], set_by: line.arguments[2], time: line.arguments[3], server: serverName});
break;
case "375":
case "372":
case "376":
this.conn.emit('pass_to_client', {type: "server_message", messageType: "motd", message: line.trailing, server: serverName, from: realServerName});
break;
case "251":
case "255":
this.conn.emit('pass_to_client', {type: "server_message", messageType: "regular", message: line.trailing, server: serverName, from: realServerName});
break;
case "252":
case "254":
case "042":
this.conn.emit('pass_to_client', {type: "server_message", messageType: "regular", message: line.arguments[1] +" "+ line.trailing, server: serverName, from: realServerName});
break;
}
}
}
class IRCConnection extends EventEmitter {
constructor(providedInfo) {
super();
let config_u = new ConfigPatcher(providedInfo, defaultConfig);
this.config = config_u.patch();
this.socket = null;
this.connected = false;
this.authenticated = false;
this.handler = new IRCConnectionHandler(this);
this.data = {
serverSupports: {},
network: this.config.server,
actualServer: this.config.server,
supportedModes: {}
};
this.queue = {};
}
on(...args) {
return super.on(...args)
}
emit(...args) {
return super.emit(...args);
}
connect() {
this.socket = net.createConnection(this.config.port, this.config.server, () => {
this.connected = true;
this.authenticate();
});
this.socket.setEncoding('utf8');
this.socket.setTimeout(3000);
this.socket.on('error', (data) => {
this.emit('error', {type: "sock_error", message: "A socket error occured.", raw: data});
});
this.socket.on('lookup', (err, address, family, host) => {
if(err) {
this.emit('error', {type: "resolve_error", message: "Failed to resolve host."});
} else {
this.emit('lookup', {address: address, family: address, host: host});
this.config.address = address;
}
});
let buffer = "";
this.socket.on('data', (chunk) => {
buffer += chunk;
let data = buffer.split('\r\n');
buffer = data.pop();
data.forEach((line) => {
if(line.indexOf('PING') === 0) {
this.socket.write('PONG'+line.substring(4)+'\r\n');
return
}
this.emit('raw', line);
let parsed = parse(line);
this.emit('line', parsed);
this.handler.handleServerLine(parsed);
});
});
this.socket.on('close', (data) => {
if(this.queue['close'])
this.emit('closed', {type: "sock_closed_success", raw: data, message: "Connection closed."});
else
this.emit('closed', {type: "sock_closed", raw: data, message: "Connection closed."});
this.connected = false;
this.authenticated = false;
})
}
authenticate() {
if (this.config.password)
this.socket.write('PASS {0}\r\n'.format(this.config.password));
// TODO: WebIRC
this.socket.write('USER {0} 8 * :{1}\r\n'.format(this.config.username, this.config.realname));
this.socket.write('NICK {0}\r\n'.format(this.config.nickname));
}
disconnect(message) {
if(!this.connected) {
this.emit('error', {type: "sock_closed", message: "Connection already closed."});
return;
}
this.queue['close'] = true;
this.socket.write('QUIT :{0}\r\n'.format(message != null ? message : configuration.client.default_quit_msg));
}
}
module.exports = IRCConnection;

44
server/parser.js Normal file
View File

@ -0,0 +1,44 @@
// :nickname!username@hostname command arg ume nts :trailing
// or
// :hostname command arg ume nts :trailing
module.exports = function(rawline) {
let final = {
user: {
nickname: "",
username: "",
hostname: ""
},
command: "error",
arguments: [],
trailing: "",
raw: rawline
}
let pass1 = (rawline.indexOf(':') == 0 ? rawline.substring(1).split(" ") : rawline.split(" "));
if (pass1[0] === "ERROR")
return final;
if(pass1[0].indexOf("!") != -1) {
let nickuser = pass1[0].split('!');
final.user.nickname = nickuser[0];
let userhost = nickuser[1].split('@');
final.user.username = userhost[0];
final.user.hostname = userhost[1];
} else {
final.user.hostname = pass1[0];
}
final.command = pass1[1];
let pass2 = pass1.slice(2).join(" ");
if(pass2.indexOf(":") != -1) {
final.arguments = pass2.substring(0, pass2.indexOf(' :')).split(" ");
final.trailing = pass2.substring(pass2.indexOf(':')+1);
} else {
final.arguments = pass2.split(" ");
}
return final;
}

View File

@ -2,10 +2,15 @@
'use strict';
let express = require("express");
let path = require("path");
let sockio = require('socket.io');
let sockio = require("socket.io");
let dns = require("dns");
let app = express();
let pubdir = path.join(__dirname+"/public");
let port = 8080;
let config = require(__dirname+'/server/config');
let ircclient = require(__dirname+'/server/irc');
let port = config.server.port || 8080;
let connections = {}
@ -19,6 +24,16 @@ let io = sockio.listen(app.listen(port, function() {
console.log("*** Listening on port " + port);
}));
function resolveHostname(hostname) {
let promise = new Promise(function(resolve, reject) {
dns.lookup(hostname, function(err, address, family) {
if(err != null) return reject(err);
resolve(address);
});
});
return promise;
}
io.sockets.on('connection', function (socket) {
console.log('clientID: '+socket.id+' connection: ', socket.request.connection._peername);
connections[socket.id] = {}
@ -28,52 +43,69 @@ io.sockets.on('connection', function (socket) {
})
socket.on('disconnect', function() {
for (let d in connections[socket.id]) d.disconnect("Client exited");
for (let d in connections[socket.id])
connections[socket.id][d].disconnect();
delete connections[socket.id];
console.log('clientID: '+socket.id+' disconnect');
});
socket.on('error', (e) => {
console.log(e);
})
socket.on('irc_create', function(connectiondata) {
console.log(socket.id+" created irc connection: ", connectiondata);
socket.emit('act_client', {type: 'connect_message', message: "Connecting to server..", error: false});
socket.emit('act_client', {type: 'connect_message', data: "Attempting connection..", error: false});
let newConnection = new ircclient(connectiondata);
newConnection.connect();
setTimeout(function() {
console.log("fake connect");
socket.emit('act_client', {type: 'event_connect', address: connectiondata.server, network: "IcyNet", supportedModes: {"o": "@", "h": "%", "v": "+"}, nickname: connectiondata.nickname, raw: connectiondata});
socket.emit('act_client', {type: 'server_message', messageType: "notice", server: connectiondata.server, message: "Connection established"});
}, 2000);
connections[socket.id][connectiondata.server] = newConnection;
setTimeout(function() {
console.log("fake channel");
socket.emit('act_client', {type: 'event_join_channel', server: connectiondata.server, channel: "#channel", user: {nickname: connectiondata.nickname, username: "teemant", hostname: socket.request.connection._peername.address}});
socket.emit('act_client', {type: 'channel_nicks', channel: "#channel", server: connectiondata.server, nicks: ["+horse", "@scoper", "@aspire", "+random", "lol"]});
socket.emit('act_client', {type: 'channel_topic', channel: "#channel", server: connectiondata.server, topic: "This channel is the best."});
socket.emit('act_client', {type: 'channel_topic', channel: "#channel", server: connectiondata.server, set_by: "horse", time: Date.now()});
socket.emit('act_client', {type: 'message', messageType: "privmsg", server: connectiondata.server, to: "#channel", user: {nickname: "horse"}, message: "I like ponies"});
newConnection.on('authenticated', () => {
console.log("******** AUTH DONE **********");
console.log("******** AUTH DONE **********");
console.log("******** AUTH DONE **********");
console.log("******** AUTH DONE **********");
console.log("******** AUTH DONE **********");
socket.emit('act_client', {type: "event_connect", address: connectiondata.server, network: newConnection.data.network,
supportedModes: newConnection.data.supportedModes, nickname: newConnection.config.nickname});
});
setTimeout(function() {
socket.emit('act_client', {type: 'nick_change', server: connectiondata.server, nick: "horse", newNick: "pony"});
}, 2000)
newConnection.on('error', (data) => {
let message = "An error occured";
let inconnect = false;
setTimeout(function() {
socket.emit('act_client', {type: 'message', messageType: "action", server: connectiondata.server, to: "#channel", user: {nickname: "pony"}, message: "Is the greatest pony fan"});
}, 3000)
if(!newConnection.authenticated) {
message = "Failed to connect to the server!";
inconnect = true;
}
setTimeout(function() {
socket.emit('act_client', {type: 'event_join_channel', server: connectiondata.server, channel: "#poni", user: {nickname: connectiondata.nickname, username: "teemant", hostname: socket.request.connection._peername.address}});
socket.emit('act_client', {type: 'channel_nicks', channel: "#poni", server: connectiondata.server, nicks: ["+horse", "@Diamond", "@aspire", "+random", "lol"]});
socket.emit('act_client', {type: 'channel_topic', channel: "#poni", server: connectiondata.server, topic: "This channel is the second best."});
socket.emit('act_client', {type: 'channel_topic', channel: "#poni", server: connectiondata.server, set_by: "Diamond", time: Date.now()});
}, 5000)
socket.emit('act_client', {type: (inconnect ? 'server_message' : 'connect_message'), message: message, type: 'error', error: true});
});
setTimeout(function() {
socket.emit('act_client', {type: 'event_kick_channel', server: connectiondata.server, channel: "#channel", user: {nickname: "scoper", username: "teemant", hostname: socket.request.connection._peername.address}, kickee: "random", reason: "Get out."});
socket.emit('act_client', {type: 'event_quit', server: connectiondata.server, user: {nickname: "lol", username: "teemant", hostname: socket.request.connection._peername.address}, reason: "Sleep."});
socket.emit('act_client', {type: 'event_part_channel', server: connectiondata.server, channel: "#poni", user: {nickname: "aspire", username: "teemant", hostname: socket.request.connection._peername.address}, reason: "Bye, lol."});
}, 6000);
}, 4000);
newConnection.on('pass_to_client', (data) => {
socket.emit('act_client', data);
});
newConnection.on('closed', (data) => {
let message = "Connection closed";
let inconnect = false;
switch(data.type) {
case "sock_closed_success":
inconnect = true;
break;
}
if(!newConnection.authenticated) {
message = "Failed to connect to the server!";
inconnect = true;
}
socket.emit('act_client', {type: (inconnect ? 'server_message' : 'connect_message'), message: message, type: 'error', error: true});
});
});
});