diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | build/client.js | 214 | ||||
| -rw-r--r-- | build/server.js | 107 | ||||
| -rw-r--r-- | default-config.json | 17 | ||||
| -rw-r--r-- | res/css/cytube.css | 4 | ||||
| -rw-r--r-- | res/index.html | 6 | ||||
| -rw-r--r-- | res/langs/en.json | 2 | ||||
| -rw-r--r-- | res/langs/ru.json | 2 | ||||
| -rw-r--r-- | src/Types.hx | 31 | ||||
| -rw-r--r-- | src/client/Main.hx | 117 | ||||
| -rw-r--r-- | src/server/Main.hx | 51 | ||||
| -rw-r--r-- | src/server/Utils.hx | 30 |
12 files changed, 480 insertions, 102 deletions
@@ -1,2 +1,3 @@ /node_modules
/res/temp
+/config.json
diff --git a/build/client.js b/build/client.js index 9ceba8f..6a6b5f8 100644 --- a/build/client.js +++ b/build/client.js @@ -106,6 +106,25 @@ Lambda.find = function(it,f) { } return null; }; +var haxe_ds_StringMap = function() { + this.h = { }; +}; +haxe_ds_StringMap.__name__ = true; +haxe_ds_StringMap.prototype = { + setReserved: function(key,value) { + if(this.rh == null) { + this.rh = { }; + } + this.rh["$" + key] = value; + } + ,getReserved: function(key) { + if(this.rh == null) { + return null; + } else { + return this.rh["$" + key]; + } + } +}; var Lang = function() { }; Lang.__name__ = true; Lang.request = function(path,callback) { @@ -218,14 +237,27 @@ Std.__name__ = true; Std.string = function(s) { return js_Boot.__string_rec(s,""); }; +var StringTools = function() { }; +StringTools.__name__ = true; +StringTools.startsWith = function(s,start) { + if(s.length >= start.length) { + return s.lastIndexOf(start,0) == 0; + } else { + return false; + } +}; +StringTools.replace = function(s,sub,by) { + return s.split(sub).join(by); +}; var client_Main = function(host,port) { if(port == null) { port = 4201; } - this.pageTitle = window.document.title; this.onTimeGet = new haxe_Timer(2000); this.isConnected = false; this.personalHistoryId = -1; + this.filters = []; + this.pageTitle = window.document.title; this.personalHistory = []; this.clients = []; var _gthis = this; @@ -241,6 +273,14 @@ var client_Main = function(host,port) { _gthis.send({ type : "GetTime"}); return; }; + window.document.onvisibilitychange = function() { + if(!window.document.hidden && _gthis.onBlinkTab != null) { + window.document.title = _gthis.getPageTitle(); + _gthis.onBlinkTab.stop(); + _gthis.onBlinkTab = null; + } + return; + }; Lang.init("langs",function() { _gthis.openWebSocket(host,port); return; @@ -273,17 +313,31 @@ client_Main.prototype = { } ,initListeners: function() { var _gthis = this; + window.document.querySelector("#smilesbtn").onclick = function(e) { + var smilesWrap = window.document.querySelector("#smileswrap"); + if(smilesWrap.style.display == "") { + return smilesWrap.style.display = "block"; + } else { + return smilesWrap.style.display = ""; + } + }; var guestName = window.document.querySelector("#guestname"); - guestName.onkeydown = function(e) { - if(e.keyCode == 13) { + guestName.onkeydown = function(e1) { + if(guestName.value.length == 0) { + return; + } + if(e1.keyCode == 13) { _gthis.send({ type : "Login", login : { clientName : guestName.value}}); } return; }; var chatLine = window.document.querySelector("#chatline"); - chatLine.onkeydown = function(e1) { - switch(e1.keyCode) { + chatLine.onkeydown = function(e2) { + switch(e2.keyCode) { case 13: + if(chatLine.value.length == 0) { + return; + } _gthis.send({ type : "Message", message : { clientName : "", text : chatLine.value}}); _gthis.personalHistory.push(chatLine.value); if(_gthis.personalHistory.length > 50) { @@ -320,29 +374,33 @@ client_Main.prototype = { }; client_MobileView.init(); var leaderBtn = window.document.querySelector("#leader_btn"); - leaderBtn.onclick = function(e2) { + leaderBtn.onclick = function(e3) { if(_gthis.personal == null) { return; } - leaderBtn.classList.toggle("label-success"); + if(!_gthis.personal.isLeader) { + leaderBtn.classList.add("label-success"); + } else { + leaderBtn.classList.remove("label-success"); + } _gthis.send({ type : "SetLeader", setLeader : { clientName : _gthis.personal.isLeader ? "" : _gthis.personal.name}}); return; }; - window.document.querySelector("#showmediaurl").onclick = function(e3) { + window.document.querySelector("#showmediaurl").onclick = function(e4) { window.document.querySelector("#showmediaurl").classList.toggle("collapsed"); window.document.querySelector("#showmediaurl").classList.toggle("active"); return window.document.querySelector("#addfromurl").classList.toggle("collapse"); }; - window.document.querySelector("#queue_next").onclick = function(e4) { + window.document.querySelector("#queue_next").onclick = function(e5) { _gthis.addVideoUrl(); return; }; - window.document.querySelector("#queue_end").onclick = function(e5) { + window.document.querySelector("#queue_end").onclick = function(e6) { _gthis.addVideoUrl(); return; }; - window.document.querySelector("#mediaurl").onkeydown = function(e6) { - if(e6.keyCode == 13) { + window.document.querySelector("#mediaurl").onkeydown = function(e7) { + if(e7.keyCode == 13) { _gthis.addVideoUrl(); } }; @@ -371,7 +429,7 @@ client_Main.prototype = { var video = window.document.createElement("video"); video.src = src; video.onloadedmetadata = function() { - haxe_Log.trace(video.duration,{ fileName : "src/client/Main.hx", lineNumber : 162, className : "client.Main", methodName : "getRemoteVideoDuration"}); + haxe_Log.trace(video.duration,{ fileName : "src/client/Main.hx", lineNumber : 185, className : "client.Main", methodName : "getRemoteVideoDuration"}); player.removeChild(video); callback(video.duration); return; @@ -389,7 +447,7 @@ client_Main.prototype = { var data = JSON.parse(e.data); var t = data.type; var t1 = t.charAt(0).toLowerCase() + HxOverrides.substr(t,1,null); - haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 178, className : "client.Main", methodName : "onMessage", customParams : [data[t1]]}); + haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 201, className : "client.Main", methodName : "onMessage", customParams : [data[t1]]}); switch(data.type) { case "AddVideo": if(this.player.isListEmpty()) { @@ -398,6 +456,7 @@ client_Main.prototype = { this.player.addVideoItem(data.addVideo.item); break; case "Connected": + this.setConfig(data.connected.config); if(data.connected.isUnknownClient) { this.updateClients(data.connected.clients); window.document.querySelector("#guestlogin").style.display = "block"; @@ -405,16 +464,27 @@ client_Main.prototype = { } else { this.onLogin(data.connected.clients,data.connected.clientName); } + var guestName = window.document.querySelector("#guestname"); + if(guestName.value.length > 0) { + this.send({ type : "Login", login : { clientName : guestName.value}}); + } + var _g = 0; + var _g1 = data.connected.history; + while(_g < _g1.length) { + var message = _g1[_g]; + ++_g; + this.addMessage(message.name,message.text,message.time); + } var list = data.connected.videoList; if(list.length == 0) { return; } this.player.setVideo(list[0]); - var _g = 0; - var _g1 = data.connected.videoList; - while(_g < _g1.length) { - var video = _g1[_g]; - ++_g; + var _g2 = 0; + var _g3 = data.connected.videoList; + while(_g2 < _g3.length) { + var video = _g3[_g2]; + ++_g2; this.player.addVideoItem(video); } break; @@ -433,7 +503,8 @@ client_Main.prototype = { this.onLogin(data.login.clients,data.login.clientName); break; case "LoginError": - this.serverMessage(4,Lang.get("usernameError")); + var text = StringTools.replace(Lang.get("usernameError"),"$MAX","" + this.config.maxLoginLength); + this.serverMessage(4,text); break; case "Logout": this.updateClients(data.logout.clients); @@ -491,6 +562,47 @@ client_Main.prototype = { break; } } + ,setConfig: function(config) { + this.config = config; + this.pageTitle = config.channelName; + window.document.querySelector("#guestname").maxLength = config.maxLoginLength; + window.document.querySelector("#chatline").maxLength = config.maxMessageLength; + this.filters.length = 0; + var _g = 0; + var _g1 = config.filters; + while(_g < _g1.length) { + var filter = _g1[_g]; + ++_g; + this.filters.push({ regex : new EReg(filter.regex,filter.flags), replace : filter.replace}); + } + var _g2 = 0; + var _g3 = config.emotes; + while(_g2 < _g3.length) { + var emote = _g3[_g2]; + ++_g2; + this.filters.push({ regex : new EReg(this.escapeRegExp(emote.name),"g"), replace : "<img class=\"channel-emote\" src=\"" + emote.image + "\" title=\"" + emote.name + "\"/>"}); + } + var smilesWrap = window.document.querySelector("#smileswrap"); + smilesWrap.onclick = function(e) { + var el = e.target; + var form = window.document.querySelector("#chatline"); + form.value += " " + el.title; + form.focus(); + return; + }; + smilesWrap.innerHTML = ""; + var _g4 = 0; + var _g5 = config.emotes; + while(_g4 < _g5.length) { + var emote1 = _g5[_g4]; + ++_g4; + var img = window.document.createElement("img"); + img.className = "smile-preview"; + img.src = emote1.image; + img.title = emote1.name; + smilesWrap.appendChild(img); + } + } ,onLogin: function(data,clientName) { this.updateClients(data); this.personal = ClientTools.getByName(this.clients,clientName); @@ -540,7 +652,7 @@ client_Main.prototype = { } ,updateUserList: function() { window.document.querySelector("#usercount").innerHTML = this.clients.length + " " + Lang.get("online"); - window.document.title = "" + this.pageTitle + " (" + this.clients.length + ")"; + window.document.title = this.getPageTitle(); var list_b = ""; var _g = 0; var _g1 = this.clients; @@ -554,26 +666,59 @@ client_Main.prototype = { } window.document.querySelector("#userlist").innerHTML = list_b; } - ,addMessage: function(name,msg) { + ,getPageTitle: function() { + return "" + this.pageTitle + " (" + this.clients.length + ")"; + } + ,addMessage: function(name,text,time) { + var _gthis = this; var msgBuf = window.document.querySelector("#messagebuffer"); var userDiv = window.document.createElement("div"); userDiv.className = "chat-msg-" + name; var tstamp = window.document.createElement("span"); tstamp.className = "timestamp"; - tstamp.innerHTML = "[" + new Date().toTimeString().split(" ")[0] + "] "; + if(time == null) { + time = "[" + new Date().toTimeString().split(" ")[0] + "] "; + } + tstamp.innerHTML = time; var nameDiv = window.document.createElement("strong"); nameDiv.className = "username"; nameDiv.innerHTML = name + ": "; var textDiv = window.document.createElement("span"); - textDiv.innerHTML = msg; + var _g = 0; + var _g1 = this.filters; + while(_g < _g1.length) { + var filter = _g1[_g]; + ++_g; + text = text.replace(filter.regex.r,filter.replace); + } + textDiv.innerHTML = text; var isInChatEnd = msgBuf.scrollHeight - msgBuf.scrollTop == msgBuf.clientHeight; userDiv.appendChild(tstamp); userDiv.appendChild(nameDiv); userDiv.appendChild(textDiv); msgBuf.appendChild(userDiv); if(isInChatEnd) { + while(msgBuf.children.length > 200) msgBuf.removeChild(msgBuf.firstChild); + msgBuf.scrollTop = msgBuf.scrollHeight; + } + if(this.personal != null && this.personal.name == name) { msgBuf.scrollTop = msgBuf.scrollHeight; } + if(window.document.hidden && this.onBlinkTab == null) { + this.onBlinkTab = new haxe_Timer(1000); + this.onBlinkTab.run = function() { + if(StringTools.startsWith(window.document.title,_gthis.pageTitle)) { + return window.document.title = "*Chat*"; + } else { + return window.document.title = _gthis.getPageTitle(); + } + }; + this.onBlinkTab.run(); + } + } + ,escapeRegExp: function(regex) { + var _this_r = new RegExp("([.*+?^${}()|[\\]\\\\])","g".split("u").join("")); + return regex.replace(_this_r,"\\$1"); } }; var client_MobileView = function() { }; @@ -822,25 +967,6 @@ haxe_Timer.prototype = { ,run: function() { } }; -var haxe_ds_StringMap = function() { - this.h = { }; -}; -haxe_ds_StringMap.__name__ = true; -haxe_ds_StringMap.prototype = { - setReserved: function(key,value) { - if(this.rh == null) { - this.rh = { }; - } - this.rh["$" + key] = value; - } - ,getReserved: function(key) { - if(this.rh == null) { - return null; - } else { - return this.rh["$" + key]; - } - } -}; var haxe_http_HttpBase = function(url) { this.url = url; this.headers = []; @@ -1225,10 +1351,10 @@ js_Browser.createXMLHttpRequest = function() { function $getIterator(o) { if( o instanceof Array ) return HxOverrides.iter(o); else return o.iterator(); } function $bind(o,m) { if( m == null ) return null; if( m.__id__ == null ) m.__id__ = $global.$haxeUID++; var f; if( o.hx__closures__ == null ) o.hx__closures__ = {}; else f = o.hx__closures__[m.__id__]; if( f == null ) { f = m.bind(o); o.hx__closures__[m.__id__] = f; } return f; } $global.$haxeUID |= 0; +var __map_reserved = {}; if( String.fromCodePoint == null ) String.fromCodePoint = function(c) { return c < 0x10000 ? String.fromCharCode(c) : String.fromCharCode((c>>10)+0xD7C0)+String.fromCharCode((c&0x3FF)+0xDC00); } String.__name__ = true; Array.__name__ = true; -var __map_reserved = {}; Object.defineProperty(js__$Boot_HaxeError.prototype,"message",{ get : function() { return String(this.val); }}); diff --git a/build/server.js b/build/server.js index 49e666c..14869f0 100644 --- a/build/server.js +++ b/build/server.js @@ -456,9 +456,9 @@ js_Boot.__string_rec = function(o,s) { return String(o); } }; -var js_node_Dns = require("dns"); var js_node_Fs = require("fs"); var js_node_Http = require("http"); +var js_node_Os = require("os"); var js_node_Path = require("path"); var js_npm_ws_Server = require("ws").Server; var server_HttpServer = function() { }; @@ -537,9 +537,12 @@ var server_Main = function(port,wsPort) { port = 4200; } this.loadedClientsCount = 0; + this.messages = []; this.videoTimer = new server_VideoTimer(); this.videoList = []; this.clients = []; + this.rootDir = "" + __dirname + "/.."; + this.config = this.getUserConfig(); this.wss = new js_npm_ws_Server({ port : wsPort}); this.wss.on("connection",$bind(this,this.onConnect)); var exit = function() { @@ -548,19 +551,19 @@ var server_Main = function(port,wsPort) { process.on("exit",exit); process.on("SIGINT",exit); process.on("uncaughtException",function(log) { - haxe_Log.trace(log,{ fileName : "src/server/Main.hx", lineNumber : 34, className : "server.Main", methodName : "new"}); + haxe_Log.trace(log,{ fileName : "src/server/Main.hx", lineNumber : 40, className : "server.Main", methodName : "new"}); return; }); process.on("unhandledRejection",function(reason,promise) { - haxe_Log.trace("Unhandled Rejection at:",{ fileName : "src/server/Main.hx", lineNumber : 37, className : "server.Main", methodName : "new", customParams : [reason]}); + haxe_Log.trace("Unhandled Rejection at:",{ fileName : "src/server/Main.hx", lineNumber : 43, className : "server.Main", methodName : "new", customParams : [reason]}); return; }); - this.getPublicIp(function(ip) { - haxe_Log.trace("Local: http://127.0.0.1:" + port,{ fileName : "src/server/Main.hx", lineNumber : 41, className : "server.Main", methodName : "new"}); - haxe_Log.trace("Global: http://" + ip + ":" + port,{ fileName : "src/server/Main.hx", lineNumber : 42, className : "server.Main", methodName : "new"}); + server_Utils.getGlobalIp(function(ip) { + haxe_Log.trace("Local: http://" + server_Utils.getLocalIp() + ":" + port,{ fileName : "src/server/Main.hx", lineNumber : 48, className : "server.Main", methodName : "new"}); + haxe_Log.trace("Global: http://" + ip + ":" + port,{ fileName : "src/server/Main.hx", lineNumber : 49, className : "server.Main", methodName : "new"}); return; }); - var dir = "" + __dirname + "/../res"; + var dir = "" + this.rootDir + "/res"; server_HttpServer.init(dir); Lang.init("" + dir + "/langs"); js_node_Http.createServer(function(req,res) { @@ -573,37 +576,46 @@ server_Main.main = function() { new server_Main(); }; server_Main.prototype = { - getPublicIp: function(callback) { - js_node_Dns.resolve("google.com",function(err,arr) { - if(err != null) { - callback("ERROR " + err.code); - return; + getUserConfig: function() { + var config = JSON.parse(js_node_Fs.readFileSync("" + this.rootDir + "/default-config.json",{ encoding : "utf8"})); + var customPath = "" + this.rootDir + "/config.json"; + if(!sys_FileSystem.exists(customPath)) { + return config; + } + var customConfig = JSON.parse(js_node_Fs.readFileSync(customPath,{ encoding : "utf8"})); + var _g = 0; + var _g1 = Reflect.fields(customConfig); + while(_g < _g1.length) { + var field = _g1[_g]; + ++_g; + if(Reflect.field(config,field) == null) { + haxe_Log.trace("Warning: config field \"" + field + "\" is unknown",{ fileName : "src/server/Main.hx", lineNumber : 67, className : "server.Main", methodName : "getUserConfig"}); } - js_node_Http.get("http://myexternalip.com/raw",function(r) { - r.setEncoding("utf8"); - return r.on("data",callback); - }); - }); + config[field] = Reflect.field(customConfig,field); + } + return config; } ,onConnect: function(ws,req) { var _gthis = this; - haxe_Log.trace("Client connected (" + req.connection.remoteAddress + ")",{ fileName : "src/server/Main.hx", lineNumber : 69, className : "server.Main", methodName : "onConnect"}); + haxe_Log.trace("Client connected (" + req.connection.remoteAddress + ")",{ fileName : "src/server/Main.hx", lineNumber : 75, className : "server.Main", methodName : "onConnect"}); var client = new Client(ws,"Unknown",false); this.clients.push(client); + var tmp = this.config; + var tmp1 = this.messages; var client1 = client.name; var _g = []; var _g1 = 0; var _g2 = this.clients; while(_g1 < _g2.length) _g.push(_g2[_g1++].getData()); - this.send(client,{ type : "Connected", connected : { isUnknownClient : true, clientName : client1, clients : _g, videoList : this.videoList}}); + this.send(client,{ type : "Connected", connected : { config : tmp, history : tmp1, isUnknownClient : true, clientName : client1, clients : _g, videoList : this.videoList}}); this.sendClientList(); ws.on("message",function(data) { - var tmp = JSON.parse(data); - _gthis.onMessage(client,tmp); + var tmp2 = JSON.parse(data); + _gthis.onMessage(client,tmp2); return; }); ws.on("close",function(err) { - haxe_Log.trace("Client " + client.name + " disconnected",{ fileName : "src/server/Main.hx", lineNumber : 90, className : "server.Main", methodName : "onConnect"}); + haxe_Log.trace("Client " + client.name + " disconnected",{ fileName : "src/server/Main.hx", lineNumber : 98, className : "server.Main", methodName : "onConnect"}); HxOverrides.remove(_gthis.clients,client); _gthis.sendClientList(); if(client.isLeader) { @@ -638,7 +650,7 @@ server_Main.prototype = { break; case "Login": var name = data.login.clientName; - if(name.length == 0 || name.length > 20 || ClientTools.getByName(this.clients,name) != null) { + if(name.length == 0 || name.length > this.config.maxLoginLength || ClientTools.getByName(this.clients,name) != null) { this.send(client,{ type : "LoginError"}); return; } @@ -655,7 +667,20 @@ server_Main.prototype = { this.sendClientList(); break; case "Message": + var text = data.message.text; + if(text.length == 0) { + return; + } + if(text.length > this.config.maxMessageLength) { + text = HxOverrides.substr(text,0,this.config.maxMessageLength); + } + data.message.text = text; data.message.clientName = client.name; + var time = "[" + new Date().toTimeString().split(" ")[0] + "] "; + this.messages.push({ text : text, name : client.name, time : time}); + if(this.messages.length > this.config.serverChatHistory) { + this.messages.pop(); + } this.broadcast(data); break; case "Pause": @@ -775,6 +800,32 @@ server_Main.prototype = { this.videoTimer.start(); } }; +var server_Utils = function() { }; +server_Utils.__name__ = true; +server_Utils.getGlobalIp = function(callback) { + js_node_Http.get("http://myexternalip.com/raw",function(r) { + r.setEncoding("utf8"); + return r.on("data",callback); + }); +}; +server_Utils.getLocalIp = function() { + var ifaces = js_node_Os.networkInterfaces(); + var _g = 0; + var _g1 = Reflect.fields(ifaces); + while(_g < _g1.length) { + var type = Reflect.field(ifaces,_g1[_g++]); + var _g2 = 0; + var _g11 = Reflect.fields(type); + while(_g2 < _g11.length) { + var iface = Reflect.field(type,_g11[_g2++]); + if("IPv4" != iface.family || iface.internal != false) { + continue; + } + return iface.address; + } + } + return "127.0.0.1"; +}; var server_VideoTimer = function() { this.pauseStartTime = 0.0; this.startTime = 0.0; @@ -824,6 +875,16 @@ server_VideoTimer.prototype = { return Date.now() / 1000 - this.pauseStartTime; } }; +var sys_FileSystem = function() { }; +sys_FileSystem.__name__ = true; +sys_FileSystem.exists = function(path) { + try { + js_node_Fs.accessSync(path); + return true; + } catch( _ ) { + return false; + } +}; function $getIterator(o) { if( o instanceof Array ) return HxOverrides.iter(o); else return o.iterator(); } var $_; function $bind(o,m) { if( m == null ) return null; if( m.__id__ == null ) m.__id__ = $global.$haxeUID++; var f; if( o.hx__closures__ == null ) o.hx__closures__ = {}; else f = o.hx__closures__[m.__id__]; if( f == null ) { f = m.bind(o); o.hx__closures__[m.__id__] = f; } return f; } diff --git a/default-config.json b/default-config.json new file mode 100644 index 0000000..efbcf30 --- /dev/null +++ b/default-config.json @@ -0,0 +1,17 @@ +{ + "channelName": "SyncTube", + "maxLoginLength": 20, + "maxMessageLength": 500, + "serverChatHistory": 30, + "videoLimit": 0, + "leaderRequest": "everyone", + "emotes": [ + {"name":":haxe:", "image":"https://haxe.org/favicon.ico"} + ], + "filters": [ + { + "name": "image", "regex": "(http|https)(:\\/\\/.*\\.)(png|jpg|gif|jpeg)", "flags": "g", + "replace": "<a href='$1$2$3' target='_blank'><img src='$1$2$3' style='max-width:200px; max-height:200px' /></a>" + } + ] +} diff --git a/res/css/cytube.css b/res/css/cytube.css index ff4411a..310e72b 100644 --- a/res/css/cytube.css +++ b/res/css/cytube.css @@ -494,8 +494,8 @@ li.ui-sortable-helper, li.ui-sortable-placeholder + li.queue_entry { } .channel-emote, .chat-img { - max-width: 200px; - max-height: 200px; + max-width: 120px; + max-height: 120px; } #cs-emotes td:nth-child(3) { diff --git a/res/index.html b/res/index.html index b174733..9b5d6f4 100644 --- a/res/index.html +++ b/res/index.html @@ -68,7 +68,7 @@ </div> <div id="userlist" style="height: 389px;"></div> <div class="linewrap" id="messagebuffer" style="height: 389px;"></div> - <input class="form-control" id="chatline" type="text" maxlength="500"> + <input class="form-control" id="chatline" type="text"> <div class="input-group" id="guestlogin" style="display: none;"><span class="input-group-addon">${enterAsGuest}</span> <input class="form-control" id="guestname" type="text" placeholder="${yourName}"> </div> @@ -108,9 +108,7 @@ <div class="row" id="playlistrow"> <div class="col-lg-5 col-md-5" id="leftpane"> <div class="row" id="leftpane-inner"> - <div class="col-lg-12 col-md-12" id="smileswrap"> - <!-- <img class="smile-preview" src="css/Sfich1B.png" title=":pinkie:"> --> - </div> + <div class="col-lg-12 col-md-12" id="smileswrap"></div> <div class="col-lg-12 col-md-12" id="playlistmanagerwrap"></div> </div> </div> diff --git a/res/langs/en.json b/res/langs/en.json index 5181f5f..dae7b31 100644 --- a/res/langs/en.json +++ b/res/langs/en.json @@ -5,7 +5,7 @@ "joined": "joined", "online": "online", "nothingPlaying": "Nothing Playing", - "usernameError": "Username must be from 1 to 20 characters and don't repeat another's.", + "usernameError": "Username must be from 1 to $MAX characters and don't repeat another's.", "rawVideo": "Raw video", "videos": "videos", "addedBy": "Added by", diff --git a/res/langs/ru.json b/res/langs/ru.json index d4c4e85..5f5f1fb 100644 --- a/res/langs/ru.json +++ b/res/langs/ru.json @@ -5,7 +5,7 @@ "joined": "вошел", "online": "онлайн", "nothingPlaying": "Ничего не играет", - "usernameError": "Ник должен быть от 1 до 20 символов и не повторять чужие.", + "usernameError": "Ник должен быть от 1 до $MAX символов и не повторять чужие.", "rawVideo": "Исходное видео", "videos": "видео", "addedBy": "Добавлено", diff --git a/src/Types.hx b/src/Types.hx index 3d4ac4f..03f18bd 100644 --- a/src/Types.hx +++ b/src/Types.hx @@ -2,6 +2,35 @@ package; import Client.ClientData; +typedef Config = { + channelName:String, + maxLoginLength:Int, + maxMessageLength:Int, + serverChatHistory:Int, + videoLimit:Int, + leaderRequest:String, + emotes:Array<Emote>, + filters:Array<Filter> +}; + +typedef Emote = { + name:String, + image:String +}; + +typedef Filter = { + name:String, + regex:String, + flags:String, + replace:String +}; + +typedef Message = { + text:String, + name:String, + time:String +} + typedef VideoItem = { url:String, title:String, @@ -12,6 +41,8 @@ typedef VideoItem = { typedef WsEvent = { type:WsEventType, ?connected:{ + config:Config, + history:Array<Message>, clients:Array<ClientData>, isUnknownClient:Bool, clientName:String, diff --git a/src/client/Main.hx b/src/client/Main.hx index 2779202..5084587 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -13,18 +13,23 @@ import js.Browser.document; import js.lib.Date; import Client.ClientData; import Types; +using StringTools; using ClientTools; class Main { final clients:Array<Client> = []; final personalHistory:Array<String> = []; + var pageTitle = document.title; + var config:Null<Config>; + final filters:Array<{regex:EReg, replace:String}> = []; var personal:Null<Client>; var personalHistoryId = -1; var isConnected = false; var ws:WebSocket; final player:Player; final onTimeGet = new Timer(2000); + var onBlinkTab:Null<Timer>; static function main():Void new Main(); @@ -35,6 +40,13 @@ class Main { initListeners(); onTimeGet.run = () -> send({type: GetTime}); + document.onvisibilitychange = () -> { + if (!document.hidden && onBlinkTab != null) { + document.title = getPageTitle(); + onBlinkTab.stop(); + onBlinkTab = null; + } + } Lang.init("langs", () -> { openWebSocket(host, port); }); @@ -58,8 +70,17 @@ class Main { } function initListeners():Void { + final smilesBtn = ge("#smilesbtn"); + smilesBtn.onclick = e -> { + final smilesWrap = ge("#smileswrap"); + if (smilesWrap.style.display == "") + smilesWrap.style.display = "block"; + else smilesWrap.style.display = ""; + } + final guestName:InputElement = cast ge("#guestname"); guestName.onkeydown = (e:KeyboardEvent) -> { + if (guestName.value.length == 0) return; if (e.keyCode == 13) send({ type: Login, login: { @@ -72,6 +93,7 @@ class Main { chatLine.onkeydown = function(e:KeyboardEvent) { switch (e.keyCode) { case 13: // Enter + if (chatLine.value.length == 0) return; send({ type: Message, message: { @@ -107,7 +129,8 @@ class Main { final leaderBtn:InputElement = cast ge("#leader_btn"); leaderBtn.onclick = (e) -> { if (personal == null) return; - leaderBtn.classList.toggle('label-success'); + if (!personal.isLeader) leaderBtn.classList.add('label-success'); + else leaderBtn.classList.remove('label-success'); final name = personal.isLeader ? "" : personal.name; send({ type: SetLeader, @@ -178,6 +201,7 @@ class Main { trace('Event: ${data.type}', untyped data[t]); switch (data.type) { case Connected: + setConfig(data.connected.config); if (data.connected.isUnknownClient) { updateClients(data.connected.clients); ge("#guestlogin").style.display = "block"; @@ -185,6 +209,16 @@ class Main { } else { onLogin(data.connected.clients, data.connected.clientName); } + final guestName:InputElement = cast ge("#guestname"); + if (guestName.value.length > 0) send({ + type: Login, + login: { + clientName: guestName.value + } + }); + for (message in data.connected.history) { + addMessage(message.name, message.text, message.time); + } final list = data.connected.videoList; if (list.length == 0) return; player.setVideo(list[0]); @@ -194,7 +228,9 @@ class Main { case Login: onLogin(data.login.clients, data.login.clientName); case LoginError: - serverMessage(4, Lang.get("usernameError")); + final text = Lang.get("usernameError") + .replace("$MAX", '${config.maxLoginLength}'); + serverMessage(4, text); case Logout: updateClients(data.logout.clients); personal = null; @@ -241,6 +277,44 @@ class Main { } } + function setConfig(config:Config):Void { + this.config = config; + pageTitle = config.channelName; + final login:InputElement = cast ge("#guestname"); + login.maxLength = config.maxLoginLength; + final form:InputElement = cast ge("#chatline"); + form.maxLength = config.maxMessageLength; + + filters.resize(0); + for (filter in config.filters) { + filters.push({ + regex: new EReg(filter.regex, filter.flags), + replace: filter.replace + }); + } + for (emote in config.emotes) { + filters.push({ + regex: new EReg(escapeRegExp(emote.name), "g"), + replace: '<img class="channel-emote" src="${emote.image}" title="${emote.name}"/>' + }); + } + final smilesWrap = ge("#smileswrap"); + smilesWrap.onclick = (e:MouseEvent) -> { + final el:Element = cast e.target; + final form:InputElement = cast ge("#chatline"); + form.value += ' ${el.title}'; + form.focus(); + } + smilesWrap.innerHTML = ""; + for (emote in config.emotes) { + final img = document.createImageElement(); + img.className = "smile-preview"; + img.src = emote.image; + img.title = emote.name; + smilesWrap.appendChild(img); + } + } + function onLogin(data:Array<ClientData>, clientName:String):Void { updateClients(data); personal = clients.getByName(clientName); @@ -285,12 +359,10 @@ class Main { msgBuf.scrollTop = msgBuf.scrollHeight; } - final pageTitle = document.title; - function updateUserList():Void { final userCount = ge("#usercount"); userCount.innerHTML = clients.length + " " + Lang.get("online"); - document.title = '$pageTitle (${clients.length})'; + document.title = getPageTitle(); final list = new StringBuf(); for (client in clients) { @@ -303,28 +375,55 @@ class Main { userlist.innerHTML = list.toString(); } - function addMessage(name:String, msg:String):Void { + function getPageTitle():String { + return '$pageTitle (${clients.length})'; + } + + function addMessage(name:String, text:String, ?time:String):Void { final msgBuf = ge("#messagebuffer"); final userDiv = document.createDivElement(); userDiv.className = 'chat-msg-$name'; final tstamp = document.createSpanElement(); tstamp.className = "timestamp"; - tstamp.innerHTML = "[" + new Date().toTimeString().split(" ")[0] + "] "; + if (time == null) time = "[" + new Date().toTimeString().split(" ")[0] + "] "; + tstamp.innerHTML = time; final nameDiv = document.createElement("strong"); nameDiv.className = "username"; nameDiv.innerHTML = name + ": "; final textDiv = document.createSpanElement(); - textDiv.innerHTML = msg; + for (filter in filters) { + text = filter.regex.replace(text, filter.replace); + } + textDiv.innerHTML = text; final isInChatEnd = msgBuf.scrollHeight - msgBuf.scrollTop == msgBuf.clientHeight; userDiv.appendChild(tstamp); userDiv.appendChild(nameDiv); userDiv.appendChild(textDiv); msgBuf.appendChild(userDiv); - if (isInChatEnd) msgBuf.scrollTop = msgBuf.scrollHeight; + if (isInChatEnd) { + while (msgBuf.children.length > 200) msgBuf.removeChild(msgBuf.firstChild); + msgBuf.scrollTop = msgBuf.scrollHeight; + } + if (personal != null && personal.name == name) { + msgBuf.scrollTop = msgBuf.scrollHeight; + } + if (document.hidden && onBlinkTab == null) { + onBlinkTab = new Timer(1000); + onBlinkTab.run = () -> { + if (document.title.startsWith(pageTitle)) + document.title = "*Chat*"; + else document.title = getPageTitle(); + } + onBlinkTab.run(); + } + } + + function escapeRegExp(regex:String):String { + return ~/([.*+?^${}()|[\]\\])/g.replace(regex, "\\$1"); } public static inline function ge(id:String):Element { diff --git a/src/server/Main.hx b/src/server/Main.hx index 8ec7e87..63af225 100644 --- a/src/server/Main.hx +++ b/src/server/Main.hx @@ -1,5 +1,8 @@ package server; +import js.lib.Date; +import sys.FileSystem; +import sys.io.File; import haxe.Timer; import Client.ClientData; import haxe.Json; @@ -8,21 +11,24 @@ import js.Node.__dirname; import js.npm.ws.Server as WSServer; import js.npm.ws.WebSocket; import js.node.Http; -import js.node.Dns; import Types; using ClientTools; using Lambda; class Main { + final rootDir = '$__dirname/..'; final wss:WSServer; + final config:Config; final clients:Array<Client> = []; final videoList:Array<VideoItem> = []; final videoTimer = new VideoTimer(); + final messages:Array<Message> = []; static function main():Void new Main(); public function new(port = 4200, wsPort = 4201) { + config = getUserConfig(); wss = new WSServer({port: wsPort}); wss.on("connection", onConnect); function exit() { @@ -37,12 +43,13 @@ class Main { trace("Unhandled Rejection at:", reason); }); - getPublicIp(ip -> { - trace('Local: http://127.0.0.1:$port'); + Utils.getGlobalIp(ip -> { + final local = Utils.getLocalIp(); + trace('Local: http://$local:$port'); trace('Global: http://$ip:$port'); }); - final dir = '$__dirname/../res'; + final dir = '$rootDir/res'; HttpServer.init(dir); Lang.init('$dir/langs'); @@ -51,17 +58,16 @@ class Main { }).listen(port); } - function getPublicIp(callback:(ip:String)->Void):Void { - Dns.resolve("google.com", function(err, arr) { - if (err != null) { - callback("ERROR " + err.code); - return; - } - Http.get("http://myexternalip.com/raw", r -> { - r.setEncoding("utf8"); - r.on("data", callback); - }); - }); + function getUserConfig():Config { + final config:Config = Json.parse(File.getContent('$rootDir/default-config.json')); + final customPath = '$rootDir/config.json'; + if (!FileSystem.exists(customPath)) return config; + final customConfig:Config = Json.parse(File.getContent(customPath)); + for (field in Reflect.fields(customConfig)) { + if (Reflect.field(config, field) == null) trace('Warning: config field "$field" is unknown'); + Reflect.setField(config, field, Reflect.field(customConfig, field)); + } + return config; } function onConnect(ws:WebSocket, req):Void { @@ -73,6 +79,8 @@ class Main { send(client, { type: Connected, connected: { + config: config, + history: messages, isUnknownClient: true, clientName: client.name, clients: [ @@ -103,7 +111,7 @@ class Main { sendClientList(); case Login: final name = data.login.clientName; - if (name.length == 0 || name.length > 20 || clients.getByName(name) != null) { + if (name.length == 0 || name.length > config.maxLoginLength || clients.getByName(name) != null) { send(client, {type: LoginError}); return; } @@ -130,9 +138,16 @@ class Main { }); sendClientList(); case Message: - // todo message log, max items - // todo message max length check + var text = data.message.text; + if (text.length == 0) return; + if (text.length > config.maxMessageLength) { + text = text.substr(0, config.maxMessageLength); + } + data.message.text = text; data.message.clientName = client.name; + final time = "[" + new Date().toTimeString().split(" ")[0] + "] "; + messages.push({text: text, name: client.name, time: time}); + if (messages.length > config.serverChatHistory) messages.pop(); broadcast(data); case AddVideo: videoList.push(data.addVideo.item); diff --git a/src/server/Utils.hx b/src/server/Utils.hx new file mode 100644 index 0000000..c3510c9 --- /dev/null +++ b/src/server/Utils.hx @@ -0,0 +1,30 @@ +package server; + +import js.node.Http; +import js.node.Os; + +class Utils { + + public static function getGlobalIp(callback:(ip:String)->Void):Void { + Http.get("http://myexternalip.com/raw", r -> { + r.setEncoding("utf8"); + r.on("data", callback); + }); + } + + public static function getLocalIp():String { + final ifaces = Os.networkInterfaces(); + for (field in Reflect.fields(ifaces)) { + final type = Reflect.field(ifaces, field); + + for (ifname in Reflect.fields(type)) { + final iface = Reflect.field(type, ifname); + // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses + if ('IPv4' != iface.family || iface.internal != false) continue; + // this interface has only one ipv4 adress + return iface.address; + } + } + return "127.0.0.1"; + } +} |
