diff --git a/README.md b/README.md index 840389f..9c8397d 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,9 @@ A: Use `` to send messages. Use `` to autocomplete usernames, exampl Q: How to find the date and time of the messages? A: Hover over the messages, and a tooltip will show the date and time. +Q: Is there a flood control feature? +A: The chat has a very basic flood control: a user cannot send more than 10 messages in 5 seconds. + Q: Is there a way to prevent a particular username from being used by anyone except me? A: The username `admin` is available *if and only if* the username `adminxyz` is entered in the input box. Change `adminxyz` to a private password in the beginning of `talktalktalk.py`, and as a result *noone else than you will be able to use the username `admin`.* diff --git a/talktalktalk.html b/talktalktalk.html index d96c0e2..b8686f2 100644 --- a/talktalktalk.html +++ b/talktalktalk.html @@ -58,6 +58,7 @@ var users; var firstId; var lastId; +var loop; function is_scrolled_end() { // See http://stackoverflow.com/a/40370876/1422096 return ((window.innerHeight + window.pageYOffset) >= document.body.offsetHeight); @@ -117,18 +118,15 @@ $('#notifications').hide(); } - function disconnected_message () { - $('#popup').text('Click anywhere or press any key to reload the page.').removeClass('hidden'); + function popup_message (message, clicktoclosepopup, clicktoreload) { + $('#popup').text(message).removeClass('hidden'); $(':not(.popup)').addClass('opaque'); - $(document).on('click keydown', function() { $(document).off('click keydown'); $(document).off('keydown'); ws.close(); ready(); }) + if (clicktoclosepopup) + $(document).on('click keydown', function() { $(document).off('click keydown'); $('#popup').addClass('hidden'); $(':not(#popup)').removeClass('opaque'); }); + else if (clicktoreload) + $(document).on('click keydown', function() { $(document).off('click keydown'); $(document).off('keydown'); ws.close(); ready(); }); } - function usernamechanged_message () { - $('#popup').text('Your username has been changed because the one you entered is reserved.').removeClass('hidden'); - $(':not(.popup)').addClass('opaque'); - $(document).on('click keydown', function() { $(document).off('click keydown'); $('#popup').addClass('hidden'); $(':not(#popup)').removeClass('opaque'); }) - } - if (!window.WebSocket) { $('#popup').text('Your browser is not supported, please use another browser.').removeClass('hidden'); $('#writing').prop('disabled', true); @@ -164,17 +162,17 @@ ws.onclose = function() { setTimeout(function() { if (ws.readyState != 0 && ws.readyState != 1) - disconnected_message(); + popup_message('Click anywhere or press any key to reload the page.', false, true); }, 2000); }; - setInterval(function() { + loop = setInterval(function() { ws.send('ping'); if (Date.now() - lastPong > 10000) { // disconnected from internet or phone idle mode; in both cases, ws.readyState can be 1 (OPEN) so we can't use that ws.send('ping'); // this is your last chance ! we send a packet now, and if in 1 second, nothings happen, this means we're dead setTimeout( function() { if (Date.now() - lastPong > 10000) // you missed your last chance ! - disconnected_message(); // disconnected from internet + popup_message('Click anywhere or press any key to reload the page.', false, true); // disconnected from internet else // ok the phone was just idle, let's resume ws.send(JSON.stringify({ "type" : "username", "username" : $('#username').text() })); // let's send username again, so that other users see me }, 1000); @@ -245,7 +243,13 @@ else if (data['type'] === 'usernameunavailable') { $('#username').text(data['username']); localStorage.setItem("username", $('#username').text()); - usernamechanged_message(); + popup_message('Your username has been changed because the one you entered is reserved.', true, false); + } + else if (data['type'] === 'flood') { + popup_message('Please do not flood this chat.', false, false); + ws.onclose = undefined; + clearInterval(loop); + ws.close(); } else if (data['type'] === 'displayeduser') { displayeduser = data['username']; @@ -266,7 +270,8 @@ $('#writing').keydown(function(e) { if (e.keyCode == 13 && !e.shiftKey) { - ws.send(JSON.stringify({ type: 'message', username: $('#username').text(), message: $('#writing').val().trim() })); + if ($('#writing').val().trim() !== '') + ws.send(JSON.stringify({ type: 'message', username: $('#username').text(), message: $('#writing').val().trim() })); $('#writing').val(''); e.preventDefault(); } diff --git a/talktalktalk.py b/talktalktalk.py index a1a360a..0dfc65f 100644 --- a/talktalktalk.py +++ b/talktalktalk.py @@ -16,6 +16,7 @@ from gevent import pywsgi from geventwebsocket.handler import WebSocketHandler from geventwebsocket.exceptions import WebSocketError +from collections import deque from config import PORT, HOST, ADMINNAME, ADMINHIDDENNAME, ALLOWEDTAGS idx = 0 @@ -37,6 +38,7 @@ def main(): users = {} pings = {} + usermessagetimes = {} def send_userlist(): for u in users.keys(): @@ -76,20 +78,32 @@ def dbworker(): # when a user disappears during more than 30 seconds (+/- @get('/ws', apply=[websocket]) def chat(ws): global idx + usermessagetimes[ws] = deque(maxlen=10) while True: try: receivedmsg = ws.receive() if receivedmsg is not None: + receivedmsg = receivedmsg.decode('utf8') if len(receivedmsg) > 4096: # this user is probably a spammer + ws.send(json.dumps({'type' : 'flood'})) break + pings[ws] = time.time() + if receivedmsg == 'ping': # ping/pong packet to make sure connection is still alive ws.send('id' + str(idx-1)) # send the latest message id in return if ws not in users: # was deleted by dbworker ws.send(json.dumps({'type' : 'username'})) else: + usermessagetimes[ws].append(time.time()) # flood control + if len(usermessagetimes[ws]) == usermessagetimes[ws].maxlen: + if usermessagetimes[ws][-1] - usermessagetimes[ws][0] < 5: # if more than 10 messages in 5 seconds (including ping messages) + ws.send(json.dumps({'type' : 'flood'})) # disconnect the spammer + break + msg = json.loads(receivedmsg) + if msg['type'] == 'message': message = (bleach.clean(msg['message'], tags=ALLOWEDTAGS, strip=True)).strip()