aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
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";
+ }
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage