aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--build/client.js214
-rw-r--r--build/server.js107
-rw-r--r--default-config.json17
-rw-r--r--res/css/cytube.css4
-rw-r--r--res/index.html6
-rw-r--r--res/langs/en.json2
-rw-r--r--res/langs/ru.json2
-rw-r--r--src/Types.hx31
-rw-r--r--src/client/Main.hx117
-rw-r--r--src/server/Main.hx51
-rw-r--r--src/server/Utils.hx30
12 files changed, 480 insertions, 102 deletions
diff --git a/.gitignore b/.gitignore
index ad43d51..5037965 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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";
+ }
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage