From d118c08aee5e955b948bfd40725818d30da5f686 Mon Sep 17 00:00:00 2001 From: Evert Date: Sat, 24 Sep 2016 19:09:03 +0300 Subject: [PATCH] some user input support --- public/css/main.css | 5 ++ public/js/main.js | 210 +++++++++++++++++++++++++++++++++++++++++--- public/main.styl | 4 + server/irc.js | 153 ++++++++++++++++++++++++++++++-- teemant.js | 3 +- 5 files changed, 357 insertions(+), 18 deletions(-) diff --git a/public/css/main.css b/public/css/main.css index 456ffbb..f34476a 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -204,6 +204,11 @@ body { display: none; overflow: hidden; } +.ircclient #chat .ircwrapper .chatarea .topicbar:hover { + height: auto; + overflow: visible; + word-wrap: break-word; +} .ircclient #chat .ircwrapper .chatarea .letterbox { position: absolute; top: 0; diff --git a/public/js/main.js b/public/js/main.js index 265795b..b578e7a 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -141,6 +141,48 @@ Date.prototype.format = function (format, utc){ return format; }; +irc.whoisMessage = function(whoisData, buffer) { + let messages = []; + for(let key in whoisData) { + switch(key) { + case "hostmask": + messages.push(""+whoisData[key]+": "+whoisData['realname']); + break; + case "idleSeconds": + let msgs = "is idle for "+whoisData[key]+" seconds"; + if(whoisData['signonTime']) + msgs += ", signed on at "+new Date(parseInt(whoisData['signonTime'])*1000); + messages.push(msgs); + break; + case "loggedIn": + case "registered": + case "title": + messages.push(whoisData[key]); + break; + case "channels": + messages.push(whoisData[key].join(" ")); + break; + case "server": + let adfd = "is on "+whoisData[key]+""; + if(whoisData['server_name']) + adfd += " "+whoisData['server_name']+""; + messages.push(adfd); + break; + case "secure": + messages.push("is using a secure connection."); + break; + case "bot": + messages.push("is a bot on "+irc.serverData[buffer.server].network); + break; + } + } + + for(let i in messages) { + let mesg = "["+whoisData.nickname+"] "+messages[i]; + buffer.addMessage(mesg, null, "whois"); + } +} + function rand(min, max) { return parseInt(Math.random() * (max-min+1), 10) + min; } @@ -235,17 +277,18 @@ let composer = { break; } - if(sender){ + if(sender) { let sndr1 = element.querySelector('.sender'); - let sndr2 = element.querySelectorAll('.nick'); if(sndr1) sndr1.style.color = colorizer.get_random_color(sndr1.innerHTML); - else if(sndr2.length > 0) - for(let a in sndr2) - if(sndr2[a] && sndr2[a]['style']) - sndr2[a].style.color = colorizer.get_random_color(sndr2[a].innerHTML); } + let sndr2 = element.querySelectorAll('.nick'); + if(sndr2.length > 0) + for(let a in sndr2) + if(sndr2[a] && sndr2[a]['style']) + sndr2[a].style.color = colorizer.get_random_color(sndr2[a].innerHTML); + return element; } } @@ -479,6 +522,12 @@ class Tab { } } + setTitle(title) { + let titleEl = this.element.querySelector('#title'); + if(!titleEl) + titleEl.innerHTML = title; + } + close() { this.closeRequested = true; this.buffer.closeBuffer(); @@ -499,6 +548,7 @@ class Buffer { this.title = tabname; this.type = type; this.active = false; + this.alive = true; this.tab = new Tab(this); this.tab.renderTab(clientdom.tabby); @@ -584,6 +634,14 @@ class Buffer { this.active = false; } + setAliveStatus(status) { + this.alive = status; + if(this.alive) + this.tab.setTitle(this.title); + else + this.tab.setTitle('('+this.title+')'); + } + closeBuffer() { irc.chat.closeBuffer(this); } @@ -700,15 +758,92 @@ class InputHandler { if(!buf) return; if(inp.trim() == "") return; + let listargs = inp.split(' '); - if(listargs[0].indexOf('/') == 0) - return; + if(listargs[0].indexOf('/') == 0) { + let cmd = listargs[0].substring(1).toLowerCase(); + switch(cmd) { + case "join": + if (!listargs[1]) { + if(!buf.alive) { + irc.socket.emit("userinput", {command: "join", server: buf.server, message: "", arguments: [buf.name]}); + } else { + this.commandError(buf, listargs[0].toUpperCase()+': Missing parameters!'); + } + } else { + irc.socket.emit("userinput", {command: "join", server: buf.server, message: "", arguments: [listargs[1]]}); + } + break; + case "part": + if (!listargs[1] && buf.type == "channel") { + inpcommand = "part"; + listargs = [buf.name]; + } else if(buf.type != "channel") { + this.commandError(buf, listargs[0].toUpperCase()+': Buffer is not a channel.'); + } else if(listargs[1]) { + if(listargs[1].indexOf('#')) { + let msg = ""; + if(listargs[2]) + msg = listargs.slice(2).join(" "); + irc.socket.emit("userinput", {command: "part", server: buf.server, message: msg, arguments: [buf.name]}); + } else { + if(buf.type == "channel") { + irc.socket.emit("userinput", {command: "part", server: buf.server, message: listargs.slice(1).join(" "), arguments: [buf.name]}); + } else { + this.commandError(buf, listargs[0].toUpperCase()+': Buffer is not a channel.'); + } + } + } + break; + case "msg": + case "privmsg": + case "say": + if(!listargs[1] || !listargs[2]) + return this.commandError(buf, listargs[0].toUpperCase()+': Missing parameters!'); + if(listargs[1] == '*') + listargs[1] = buf.name; + irc.socket.emit("userinput", {command: "privmsg", server: buf.server, message: listargs.slice(2).join(" "), arguments: [listargs[1]]}); + break; + case "notice": + if(!listargs[1] || !listargs[2]) + return this.commandError(buf, listargs[0].toUpperCase()+': Missing parameters!'); + if(listargs[1] == '*') + listargs[1] = buf.name; + irc.socket.emit("userinput", {command: "notice", server: buf.server, message: listargs.slice(2).join(" "), arguments: [listargs[1]]}); + break; + case "me": + case "action": + irc.socket.emit("userinput", {command: "privmsg", server: buf.server, message: "\x01ACTION "+inp.substring(cmd.length+2)+"\x01", arguments: [buf.name]}); + break; + case "list": + irc.socket.emit("userinput", {command: cmd, server: buf.server, message: "", arguments: listargs}); + break; + case "quote": + case "raw": + irc.socket.emit("userinput", {command: listargs[1], server: buf.server, message: listargs.slice(2).join(" "), arguments: listargs.splice(2)}); + break; + case "whois": + if(!listargs[1]) + return this.commandError(buf, listargs[0].toUpperCase()+': Missing parameters!'); + + irc.socket.emit("userinput", {command: "whois", server: buf.server, message: "", arguments: [listargs[1]]}); + break; + default: + this.commandError(buf, listargs[0].toUpperCase()+': Unknown command!'); + } + } else { + irc.socket.emit("userinput", {command: "privmsg", server: buf.server, message: inp, arguments: [buf.name]}); + } - irc.socket.emit("userinput", {target: buf.name, targetType: buf.type, server: buf.server, message: inp, splitup: inp.split(" ")}); this.history.push(inp); clientdom.input.value = ""; } + + commandError(buffer, message) { + buffer.addMessage(message, null, "error"); + return true; + } } class IRCChatWindow { @@ -798,7 +933,8 @@ class IRCChatWindow { modeTranslation: serverinfo.supportedModes, supportedPrefixes: prefixes, network: serverinfo.network, - my_nick: serverinfo.nickname + my_nick: serverinfo.nickname, + max_channel_length: serverinfo.max_channel_length } let newServer = new Buffer(serverinfo.address, serverinfo.address, serverinfo.network, "server"); @@ -835,7 +971,9 @@ class IRCChatWindow { closeBuffer(buffer) { if(buffer.type == "server") return; // Don't close server buffers, lol - if(buffer.type == "channel") console.log("TODO: PART"); + if(buffer.type == "channel" && buffer.alive) + irc.socket.emit("userinput", {command: "part", server: buffer.server, message: "Tab closed", arguments: [buffer.name]}); + let bufIndex = this.buffers.indexOf(buffer); if(buffer.active) { @@ -859,6 +997,11 @@ class IRCChatWindow { if(buf == null) buf = this.createBuffer(server, name, "message", false); + if(message.type == "privmsg" && message.message.indexOf('\x01ACTION') == 0) { + message.message = message.message.substring(8); + message.type = "action"; + } + buf.addMessage(message.message, message.from, message.type); } @@ -942,6 +1085,9 @@ class IRCChatWindow { if(!buffer) return; + if(user.nickname == irc.serverData[server].my_nick) + buffer.setAliveStatus(true); + buffer.addMessage(""+user.username+"@"+user.hostname+" has joined "+channel, user.nickname, "join"); buffer.nicklist.nickAdd(user.nickname); } @@ -951,6 +1097,14 @@ class IRCChatWindow { if(!buffer) return; + if(user['nickname']) { + if(user.nickname == irc.serverData[server].my_nick) + buffer.setAliveStatus(false); + } else { + if(user == irc.serverData[server].my_nick) + buffer.setAliveStatus(false); + } + if(kicker) buffer.addMessage("has kicked "+user+" "+reason+"", kicker.nickname, "part"); else @@ -986,6 +1140,32 @@ class IRCChatWindow { } } + joinChannels(server, channel) { + if (channel.indexOf(",") !== -1) { + channel = channel.trim().split(","); + + for (let t in channel) { + let chan = channel[t]; + + channel[t] = chan.trim(); + + if (chan.indexOf("#") != 0) { + channel[t] = "#"+chan; + } + } + } else if(channel != "") { + channel = channel.trim(); + if (channel.indexOf("#") != 0) { + channel = "#"+channel; + } + channel = [channel]; + } else { + channel = []; + } + + irc.socket.emit("userinput", {command: "join", server: server, message: "", arguments: channel}); + } + render(buffer) { let activeNow = this.getActiveBuffer(); @@ -1098,6 +1278,14 @@ window.onload = function() { case "connect_message": irc.auther.authMessage(data.message, data.error); break; + case "whoisResponse": + irc.whoisMessage(data.whois, irc.chat.getActiveBuffer()); + break; + case "listedchan": + irc.chat.messageBuffer(data.server, data.server, {message: ""+data.channel+""+ + " "+data.users+" "+data.topic+"", + type: "listentry", from: data.from}); + break; } }); } diff --git a/public/main.styl b/public/main.styl index ccc90fe..d15b707 100644 --- a/public/main.styl +++ b/public/main.styl @@ -184,6 +184,10 @@ body padding: 12px; display: none; overflow: hidden; + &:hover + height: auto + overflow: visible + word-wrap: break-word .letterbox position: absolute; top: 0; diff --git a/server/irc.js b/server/irc.js index 5ce75b5..6b6b833 100644 --- a/server/irc.js +++ b/server/irc.js @@ -18,13 +18,55 @@ class IRCConnectionHandler { } handleUserLine(data) { + console.log(data); + switch(data.command) { + case "kick": + case "part": + this.conn.write('{0} {1} :{2}'.format(data.command.toUpperCase(), data.arguments[0], data.message)); + break; + case "nick": + case "whois": + case "who": + case "join": + this.conn.write('{0} {1}'.format(data.command.toUpperCase(), data.arguments[0])); + break; + case "quit": + this.conn.write('{0} :{1}'.format(data.command.toUpperCase(), data.message)); + break; + case "privmsg": + this.conn.write('PRIVMSG {0} :{1}'.format(data.arguments[0], data.message)); + this.conn.emit('pass_to_client', {type: "message", messageType: "privmsg", to: data.arguments[0], + user: {nickname: this.conn.config.nickname}, message: data.message, server: data.server}); + break; + case "notice": + this.conn.write('NOTICE {0} :{1}'.format(data.arguments[0], data.message)); + this.conn.emit('pass_to_client', {type: "message", messageType: "notice", to: data.arguments[0], + user: {nickname: this.conn.config.nickname}, message: data.message, server: data.server}); + break; + case "list": + this.conn.write(data.command.toUpperCase()); + break; + default: + this.conn.write(data.command.toUpperCase()+' '+data.message); + } if(data.targetType == "channel" || data.targetType == "message") { - this.conn.socket.write('PRIVMSG {0} :{1}\r\n'.format(data.target, data.message)); + this.conn.write('PRIVMSG {0} :{1}'.format(data.target, data.message)); this.conn.emit('pass_to_client', {type: "message", messageType: "privmsg", to: data.target, user: {nickname: this.conn.config.nickname}, message: data.message, server: data.server}); } } + whoisManage(whom, list) { + if(!this.conn.queue.whois) + this.conn.queue.whois = {}; + + if(!this.conn.queue.whois[whom]) + this.conn.queue.whois[whom] = list; + else + for(let a in list) + this.conn.queue.whois[whom][a] = list[a]; + } + handleServerLine(line) { console.log(line); if(this.conn.queue["supportsmsg"] && line.command != "005") { @@ -33,7 +75,7 @@ class IRCConnectionHandler { 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.write('JOIN '+this.conn.config.autojoin[t]); this.conn.emit('authenticated', {}); } @@ -41,6 +83,7 @@ class IRCConnectionHandler { let serverName = this.conn.config.server; let realServerName = this.conn.data.actualServer; + let list = null; switch(line.command) { case "error": this.conn.emit("connerror", {type: "irc_error", raw: line.raw}); @@ -67,6 +110,8 @@ class IRCConnectionHandler { this.conn.data.supportedModes[r[b]] = aa[b]; } else if(t[0] === 'NETWORK') { this.conn.data.network = t[1]; + } else if(t[0] === 'CHANNELLEN') { + this.conn.data.max_channel_length = parseInt(t[1]); } this.conn.data.serverSupports[t[0]] = t[1]; @@ -113,9 +158,7 @@ class IRCConnectionHandler { 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"; + // TODO: remove once proper CTCP handling is done; } else if(line.trailing.indexOf('\x01') == 0) { // TODO: handle CTCPs return; @@ -135,6 +178,9 @@ class IRCConnectionHandler { this.conn.emit('pass_to_client', {type: "server_message", messageType: "notice", message: line.trailing, server: serverName, from: realServerName}); break; case "NICK": + if(line.user.nickname == this.conn.config.nickname) + this.conn.config.nickname = line.arguments[0]; + this.conn.emit('pass_to_client', {type: "nick_change", nick: line.user.nickname, newNick: line.arguments[0], server: serverName}); break; case "KICK": @@ -155,6 +201,8 @@ class IRCConnectionHandler { this.conn.emit('pass_to_client', {type: "server_message", messageType: "motd", message: line.trailing, server: serverName, from: realServerName}); break; case "251": + case "290": + case "292": case "255": this.conn.emit('pass_to_client', {type: "server_message", messageType: "regular", message: line.trailing, server: serverName, from: realServerName}); break; @@ -191,6 +239,91 @@ class IRCConnectionHandler { this.conn.emit('pass_to_client', {type: "mode", target: line.arguments[0], message: line.arguments.slice(1).join(" "), server: serverName, user: line.user}); break; + case "433": + let newNick = this.conn.config.nickname + "_"; + this.conn.write('NICK '+newNick); + this.conn.config.nickname = newNick; + break; + case "401": + case "402": + this.conn.emit('pass_to_client', {type: "message", to: line.arguments[1], message: line.trailing, + server: serverName, user: {nickname: realServerName}, messageType: "error"}); + break; + case "311": + // start whois queue + list = { + nickname: line.arguments[1], + hostmask: "{0}!{1}@{2}".format(line.arguments[1], line.arguments[2], line.arguments[3]), + realname: line.trailing || "" + }; + this.whoisManage(line.arguments[1], list); + break; + case "319": + // whois: channels + list = { + channels: line.trailing.split(" "), + }; + this.whoisManage(line.arguments[1], list); + break; + case "312": + list = { + server: line.arguments[2], + server_name: line.trailing || "" + } + this.whoisManage(line.arguments[1], list); + break; + case "313": + list = { + title: line.trailing + } + this.whoisManage(line.arguments[1], list); + break; + case "330": + list = { + loggedIn: line.trailing+' '+line.arguments[2] + } + this.whoisManage(line.arguments[1], list); + break; + case "335": + list = { + bot: true + } + this.whoisManage(line.arguments[1], list); + break; + case "307": + list = { + registered: line.trailing + } + this.whoisManage(line.arguments[1], list); + break; + case "671": + list = { + secure: true + } + this.whoisManage(line.arguments[1], list); + break; + case "317": + list = { + signonTime: line.arguments[3], + idleSeconds: line.arguments[2] + } + this.whoisManage(line.arguments[1], list); + break; + case "318": + if(!this.conn.queue.whois || !this.conn.queue.whois[line.arguments[1]]) + return; + this.conn.emit('pass_to_client', {type: "whoisResponse", whois: this.conn.queue.whois[line.arguments[1]], + server: serverName, from: realServerName}); + delete this.conn.queue.whois[line.arguments[1]]; + break; + case "321": + this.conn.emit('pass_to_client', {type: "listedchan", channel: "Channel", users: "Users", topic: "Topic", + server: serverName, from: realServerName}); + break; + case "322": + this.conn.emit('pass_to_client', {type: "listedchan", channel: line.arguments[1], users: line.arguments[2], topic: line.trailing, + server: serverName, from: realServerName}); + break; } } } @@ -225,6 +358,7 @@ class IRCConnection extends EventEmitter { serverSupports: {}, network: this.config.server, actualServer: this.config.server, + max_channel_length: 64, supportedModes: {} }; this.queue = {}; @@ -297,7 +431,7 @@ class IRCConnection extends EventEmitter { disconnect(message) { if(!this.connected) { - this.emit('error', {type: "sock_closed", message: "Connection already closed."}); + this.emit('connerror', {type: "sock_closed", message: "Connection already closed."}); return; } @@ -305,6 +439,13 @@ class IRCConnection extends EventEmitter { this.socket.write('QUIT :{0}\r\n'.format(message != null ? message : configuration.client.default_quit_msg)); } + write(message) { + if(!this.connected) { + this.emit('connerror', {type: "sock_closed", message: "Connection is closed."}); + return; + } + this.socket.write(message+'\r\n'); + } } diff --git a/teemant.js b/teemant.js index ec4e3c8..ea8bc85 100755 --- a/teemant.js +++ b/teemant.js @@ -71,7 +71,8 @@ io.sockets.on('connection', function (socket) { newConnection.on('authenticated', () => { socket.emit('act_client', {type: "event_connect", address: connectiondata.server, network: newConnection.data.network, - supportedModes: newConnection.data.supportedModes, nickname: newConnection.config.nickname}); + supportedModes: newConnection.data.supportedModes, nickname: newConnection.config.nickname, + max_channel_length: newConnection.data.max_channel_length}); }); newConnection.on('connerror', (data) => {