diff options
| author | RblSb <msrblsb@gmail.com> | 2020-02-19 12:06:37 +0300 |
|---|---|---|
| committer | RblSb <msrblsb@gmail.com> | 2020-02-19 12:06:37 +0300 |
| commit | 2ecf9b8d31457175c6cc4ca5a441a165556afb77 (patch) | |
| tree | ac78bb6e4432d239af3f752972c03c8ef473cadb /src | |
| parent | 07d1955cefc093ffb12002902ed45e963030746e (diff) | |
More buttons
Emotes, filters, config, server history, autologin, tab blinking
Diffstat (limited to 'src')
| -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 |
4 files changed, 202 insertions, 27 deletions
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"; + } +} |
