From aa7fc8eea837448ce9fea553fb8b5dfcc2239bb6 Mon Sep 17 00:00:00 2001 From: Sebastian Korotkiewicz Date: Tue, 22 Nov 2016 23:17:46 +0100 Subject: [PATCH 1/4] mendments aesthetic --- popsound.mp3 => static/popsound.mp3 | Bin static/styles.css | 16 ++ static/talktalktalk.js | 272 +++++++++++++++++++++ talktalktalk.html | 351 +++------------------------- talktalktalk.py | 17 +- 5 files changed, 337 insertions(+), 319 deletions(-) rename popsound.mp3 => static/popsound.mp3 (100%) create mode 100644 static/styles.css create mode 100644 static/talktalktalk.js diff --git a/popsound.mp3 b/static/popsound.mp3 similarity index 100% rename from popsound.mp3 rename to static/popsound.mp3 diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..4aea638 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,16 @@ +* { margin: 0; padding: 0; font-family: monospace; } +html { overflow-y: scroll; } +#writing { width: 100%; height: 10%; position: fixed; height: 100px; bottom: 0; resize: none; border-top: 1px solid #aaa; } +#wrapper { width: 75%; margin: 10px 10px 110px 10px; word-wrap: break-word; } +#main a { text-decoration: none; color: inherit; } +#main b { display: inline-block; } /* to prevent right to left utf8 characters */ +#right { width: 20%; top: 0; left: 80%; position: fixed; height: calc(100% - 120px); padding: 10px 0 10px 0; overflow-y: auto; overflow-x: hidden; } +#username { padding: 2px 0 0px 1px; } +.dashed { display: inline-block; border-bottom: 1px dashed #CCC; cursor: pointer; margin-bottom: 1px; } +#usernameinput { display: none; width: 80%; } +#fork { z-index: 10; } +#previous { margin-bottom: 20px; display: block; } +#popup { z-index: 10; position: fixed; left: 20%; width:60%; top:40%; border: solid black 1px; background-color: white; text-align: center; padding: 30px 10px 30px 10px; color: black !important; } +.hidden { display: none !important; } +.opaque { color: #AAA; } +@media (max-width: 500px) { #right { width: 120px; left: calc(100% - 120px); } #wrapper { width: calc(100% - 130px); } #banner { display: none; } } \ No newline at end of file diff --git a/static/talktalktalk.js b/static/talktalktalk.js new file mode 100644 index 0000000..15b5791 --- /dev/null +++ b/static/talktalktalk.js @@ -0,0 +1,272 @@ +var popsound = new Audio('/static/popsound.mp3'); +var randomname = 'user' + Math.floor((Math.random() * 1000) + 1); +var displayeduser; +var ws; +var lastPong; +var users; +var firstId; +var lastId; + +function is_scrolled_end() { // See http://stackoverflow.com/a/40370876/1422096 + return ((window.innerHeight + window.pageYOffset) >= document.body.offsetHeight); +} + +function scroll_end () { + document.body.scrollTop = document.body.scrollHeight; // both useful because this one not working in FF47 + document.documentElement.scrollTop = document.documentElement.scrollHeight; // both useful because this one not working in Chrome 43 Android +} + +function scroll_top () { + document.body.scrollTop = 0; + document.documentElement.scrollTop = 0; +} + +function add_urls(s) { + return s.replace(/(https?:\/\/[^ ]+)/g, '$1'); +} + +function timeConverter(t) { // http://stackoverflow.com/a/40449006/1422096 + if (t == null) + return ''; + var a = new Date(t * 1000); + var today = new Date(); + var yesterday = new Date(Date.now() - 86400000); + var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + var year = a.getFullYear(); + var month = months[a.getMonth()]; + var date = a.getDate(); + var hour = a.getHours(); + var min = a.getMinutes(); + var time = ("0" + hour).slice(-2) + ':' + ("0" + min).slice(-2); + if (a.setHours(0,0,0,0) == today.setHours(0,0,0,0)) + return 'today, ' + time; + else if (a.setHours(0,0,0,0) == yesterday.setHours(0,0,0,0)) + return 'yesterday, ' + time; + else if (year == today.getFullYear()) + return date + ' ' + month + ', ' + time; + else + return date + ' ' + month + ' ' + year + ', ' + time; +} + +function mediaPlaybackRequiresUserGesture() { // http://stackoverflow.com/a/40474415/1422096 + var audio = document.createElement('audio'); + audio.play(); + return audio.paused; +} + +function ready() { + lastPong = undefined; + users = []; + firstId = Infinity; + lastId = -1; + + if (mediaPlaybackRequiresUserGesture()) { // mobile devices require user gesture to trigger a play. "Notifications" off for them. + $('#notificationswitch').text('off'); + $('#notifications').hide(); + } + + function disconnected_message () { + $('#popup').text('Click anywhere or press any key to reload the page.').removeClass('hidden'); + $(':not(.popup)').addClass('opaque'); + $(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); + $(':not(#popup)').addClass('opaque'); + } + else { + $('#main').text(''); + $('#writing').val(''); + } + + if (localStorage.getItem("username") === null || localStorage.getItem("username") === '') { + $('#username').text(randomname); + localStorage.setItem("username", $('#username').text()); + } + else + $('#username').text(localStorage.getItem("username")); + + ws = new WebSocket(location.href.replace('http', 'ws') + 'ws'); // replace replaces only the first occurence + + $(window).on('beforeunload', function() { + ws.close(); + }); + + ws.onopen = function() { + ws.send(JSON.stringify({ "type" : "username", "username" : $('#username').text() })); + lastPong = Date.now(); + $('#popup').addClass('hidden'); $(':not(#popup)').removeClass('opaque'); + $('#writing').focus(); + scroll_end(); + setTimeout(function() { scroll_end(); }, 200); // useful on touch devices, when keyboard opens + }; + + ws.onclose = function() { + setTimeout(function() { + if (ws.readyState != 0 && ws.readyState != 1) + disconnected_message(); + }, 2000); + }; + + 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 + 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); + } + }, 5000); + + $('#previous').click(function(e) { + ws.send(JSON.stringify({'type': 'messagesbefore', 'id': firstId})); + e.preventDefault(); + return false; + }); + + ws.onmessage = function(e) { + lastPong = Date.now(); + if (e['data'].slice(0,2) === 'id') { + var serverlastid = e['data'].slice(2); + if (serverlastid != lastId) { // some messages were lost + ws.send(JSON.stringify({'type': 'messagesafter', 'id': lastId})); + } + return; + } + var data = JSON.parse(e['data']); + if (data['type'] === 'message') { + if (data['id'] > lastId + 1) { // some messages were lost + ws.send(JSON.stringify({'type': 'messagesafter', 'id': lastId})); + return; + } + lastId = data['id']; + var isscrolledend = is_scrolled_end(); + $('#main').append('

' + data['username'] + ': ' + add_urls(data['message']) + '

'); + if (isscrolledend) + scroll_end(); + if (data['username'] !== $('#username').text() && data['username'] !== displayeduser && $('#notificationswitch').text() === 'on') { + popsound.load(); popsound.play(); + } + } + else if (data['type'] === 'messages') { + var newmessages = document.createDocumentFragment(); + var messagelist = data['messages']; + for (var i = 0; i < messagelist.length; i++) { + var msg = JSON.parse(messagelist[i]); + if (data['before'] != 1 && msg['id'] <= lastId) // message already added + continue; + $(newmessages).append('

' + msg['username'] + ': ' + add_urls(msg['message']) + '

'); + firstId = Math.min(firstId, msg['id']) + lastId = Math.max(lastId, msg['id']); + } + if (data['before'] == 1) + $('#main').prepend(newmessages); + else + $('#main').append(newmessages); + if (firstId > 0 && firstId != Infinity) + $('#previous').removeClass('hidden'); + else + $('#previous').addClass('hidden'); + if (data['before'] == 1) + scroll_top(); + else + scroll_end(); + } + else if (data['type'] === 'userlist') { + users = data['connected']; + $('#connected').html(users.join('
')); // or .map(x => `${x}
`).join('') + } + else if (data['type'] === 'username') { // server asks to (re)send username + ws.send(JSON.stringify({ "type" : "username", "username" : $('#username').text() })); + } + else if (data['type'] === 'usernameunavailable') { + $('#username').text(data['username']); + localStorage.setItem("username", $('#username').text()); + usernamechanged_message(); + } + else if (data['type'] === 'displayeduser') { + displayeduser = data['username']; + } + }; + + $('#username').on('click', function(e) { + $('#username').hide(); + $('#usernameinput').val($('#username').text()).show().focus(); + e.preventDefault(); + return false; + }); + + $('#writing').on('click', function(e) { // trick for touch devices: when phone keyboard opens, we won't be scrolled-end anymore, so force it + if (is_scrolled_end()) + setTimeout(function() { scroll_end(); }, 200); + }); + + $('#writing').keydown(function(e) { + if (e.keyCode == 13 && !e.shiftKey) { + ws.send(JSON.stringify({ type: 'message', username: $('#username').text(), message: $('#writing').val().trim() })); + $('#writing').val(''); + e.preventDefault(); + } + else if (e.keyCode == 9) { // see http://stackoverflow.com/a/40319943/1422096 + var input = document.getElementById('writing'); + var patt = /\b@?(\S+)$/; + e.preventDefault(); + var start = input.selectionStart; + var seg = input.value.slice(0, start); + var match = (seg.match(patt) || [])[0]; + if (!match) { return; } + var idx = users.findIndex(function (x) { return x.startsWith(match); }); + if (idx < 0) { return; } + var replace = users[users[idx] === match ? (idx + 1) % users.length : idx]; + var newSeg = seg.replace(patt, replace); + input.value = newSeg + input.value.slice(start); + input.setSelectionRange(newSeg.length, newSeg.length); + } + else if (e.keyCode == 33 || e.keyCode == 34) { // alows to scroll with PAGE UP / DOWN even if textarea has focus + $('#writing').blur(); + setTimeout(function() { $('#writing').focus(); }, 10); + } + }); + + $('#usernameinput').on('blur', function(e) { + var usr = $('#usernameinput').val(); + usr = usr.replace(/[:\s]/g, ''); + if (usr === '') + usr = randomname; + $('#username').text(usr); + localStorage.setItem("username", usr); + $('#usernameinput').hide(); + $('#username').show(); + ws.send(JSON.stringify({ "type" : "username", "username" : usr })); + e.preventDefault(); + }); + + $('#usernameinput').keydown(function(e) { + if (e.keyCode == 13) { + $('#usernameinput').blur(); + } + if (e.keyCode == 27) { + $('#usernameinput').val($('#username').text()).hide(); + $('#username').show(); + e.preventDefault(); + return false; + } + }); + + $('#notificationswitch').click(function() { + $('#notificationswitch').text($('#notificationswitch').text() === 'on' ? 'off' : 'on'); + }); +} + +$(document).ready(ready); \ No newline at end of file diff --git a/talktalktalk.html b/talktalktalk.html index d96c0e2..0303515 100644 --- a/talktalktalk.html +++ b/talktalktalk.html @@ -7,322 +7,39 @@ # url: http://github.com/josephernest/talktalktalk # license: MIT license --> - - - -TalkTalkTalk - - - - - - - - -
- -
-
-
- - - - + + + + + + + + + TalkTalkTalk + + + + + + + + + +
+ +
+
+ + + + diff --git a/talktalktalk.py b/talktalktalk.py index a1a360a..383a376 100644 --- a/talktalktalk.py +++ b/talktalktalk.py @@ -138,9 +138,22 @@ def index(): context = {'request': request} return (context) - @route('/popsound.mp3') + @route('/static/popsound.mp3') def popsound(): - return static_file('popsound.mp3', root='.') + return static_file('static/popsound.mp3', root='.') + + @route('/static/styles.css') + def css(): + return static_file('static/styles.css', root='.') + + @route('/static/talktalktalk.js') + def js(): + return static_file('static/talktalktalk.js', root='.') + +## dunno why this doesn't work +# @route('/static/') +# def server_static(filepath): +# return static_file(filepath, root='.') run(host=HOST, port=PORT, debug=True, server=GeventWebSocketServer) From ddca2e2f59eb874c652f2166b71b57926213e83c Mon Sep 17 00:00:00 2001 From: Sebastian Korotkiewicz Date: Sat, 3 Dec 2016 07:26:51 +0100 Subject: [PATCH 2/4] Basic flood control --- .gitignore | 5 +++++ README.md | 3 +++ static/talktalktalk.js | 35 ++++++++++++++++++++--------------- talktalktalk.html | 4 ++-- talktalktalk.py | 16 +++++++++++++++- 5 files changed, 45 insertions(+), 18 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e596dbb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +_.pid +talktalktalk.db.dat +talktalktalk.db.dir +log.txt diff --git a/README.md b/README.md index bc9ed43..d55b85e 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/static/talktalktalk.js b/static/talktalktalk.js index 15b5791..3625597 100644 --- a/static/talktalktalk.js +++ b/static/talktalktalk.js @@ -6,6 +6,7 @@ var lastPong; 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); @@ -65,18 +66,15 @@ function ready() { $('#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); @@ -112,17 +110,17 @@ function ready() { 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); @@ -193,7 +191,13 @@ function ready() { 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']; @@ -214,7 +218,8 @@ function ready() { $('#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(); } @@ -269,4 +274,4 @@ function ready() { }); } -$(document).ready(ready); \ No newline at end of file +$(document).ready(ready); diff --git a/talktalktalk.html b/talktalktalk.html index 0303515..a7c940e 100644 --- a/talktalktalk.html +++ b/talktalktalk.html @@ -22,8 +22,8 @@ - - +