From 6109de37ec56e32a40d71575d30e3f6a4e46fa68 Mon Sep 17 00:00:00 2001 From: n9k Date: Fri, 18 Feb 2022 12:24:19 +0000 Subject: [PATCH] Nojs chat: ETag, limit scrollback, timeout notice Limiting scrollback is happening for the js chat too. Also reject long comments. --- anonstream/__init__.py | 9 ++- anonstream/chat.py | 10 ++- anonstream/helpers/chat.py | 6 ++ anonstream/helpers/user.py | 4 +- anonstream/routes/nojs.py | 13 +++- anonstream/routes/wrappers.py | 12 +++- anonstream/static/anonstream.js | 41 +++++++++---- anonstream/static/style.css | 6 +- anonstream/templates/home.html | 2 +- anonstream/templates/nojs_chat.html | 95 ++++++++++++++++++++++++++--- anonstream/templates/nojs_form.html | 4 +- anonstream/user.py | 2 +- anonstream/websocket.py | 1 + config.toml | 5 +- 14 files changed, 169 insertions(+), 41 deletions(-) diff --git a/anonstream/__init__.py b/anonstream/__init__.py index 4ba49ac..8d2fbff 100644 --- a/anonstream/__init__.py +++ b/anonstream/__init__.py @@ -31,16 +31,19 @@ async def create_app(): 'MAX_CHAT_SCROLLBACK': config['memory']['chat_scrollback'], 'CHECKUP_PERIOD_USER': config['ratelimits']['user_absence'], 'CHECKUP_PERIOD_CAPTCHA': config['ratelimits']['captcha_expiry'], - 'THRESHOLD_IDLE': config['thresholds']['idle'], - 'THRESHOLD_ABSENT': config['thresholds']['absent'], + 'THRESHOLD_USER_IDLE': config['thresholds']['user_idle'], + 'THRESHOLD_USER_ABSENT': config['thresholds']['user_absent'], + 'THRESHOLD_NOJS_CHAT_TIMEOUT': config['thresholds']['nojs_chat_timeout'], 'CHAT_COMMENT_MAX_LENGTH': config['chat']['max_name_length'], 'CHAT_NAME_MAX_LENGTH': config['chat']['max_name_length'], 'CHAT_NAME_MIN_CONTRAST': config['chat']['min_name_contrast'], 'CHAT_BACKGROUND_COLOUR': color_to_colour(config['chat']['background_color']), }) + assert app.config['MAX_NOTICES'] >= 0 + assert app.config['MAX_CHAT_SCROLLBACK'] >= 0 assert app.config['MAX_CHAT_MESSAGES'] >= app.config['MAX_CHAT_SCROLLBACK'] - assert app.config['THRESHOLD_ABSENT'] >= app.config['THRESHOLD_IDLE'] + assert app.config['THRESHOLD_USER_ABSENT'] >= app.config['THRESHOLD_USER_IDLE'] app.messages_by_id = OrderedDict() app.users_by_token = {} diff --git a/anonstream/chat.py b/anonstream/chat.py index 529c3be..05b3792 100644 --- a/anonstream/chat.py +++ b/anonstream/chat.py @@ -3,9 +3,10 @@ from datetime import datetime from quart import current_app, escape -from anonstream.helpers.chat import generate_nonce_hash +from anonstream.helpers.chat import generate_nonce_hash, get_scrollback from anonstream.utils.chat import message_for_websocket +CONFIG = current_app.config MESSAGES_BY_ID = current_app.messages_by_id MESSAGES = current_app.messages USERS_BY_TOKEN = current_app.users_by_token @@ -25,7 +26,7 @@ def messages_for_websocket(): user=USERS_BY_TOKEN[message['token']], message=message, ), - MESSAGES, + get_scrollback(MESSAGES), )) async def add_chat_message(user, nonce, comment): @@ -35,6 +36,8 @@ async def add_chat_message(user, nonce, comment): raise Rejected('Discarded suspected duplicate message') if len(comment) == 0: raise Rejected('Message was empty') + if len(comment) > 512: + raise Rejected('Message exceeded 512 chars') # add message timestamp_ms = time.time_ns() // 1_000_000 @@ -62,6 +65,9 @@ async def add_chat_message(user, nonce, comment): 'markup': markup, } + while len(MESSAGES_BY_ID) > CONFIG['MAX_CHAT_MESSAGES']: + MESSAGES_BY_ID.pop(last=False) + # broadcast message to websockets await broadcast( USERS, diff --git a/anonstream/helpers/chat.py b/anonstream/helpers/chat.py index 807218f..45fad4f 100644 --- a/anonstream/helpers/chat.py +++ b/anonstream/helpers/chat.py @@ -7,3 +7,9 @@ CONFIG = current_app.config def generate_nonce_hash(nonce): parts = CONFIG['SECRET_KEY'] + b'nonce-hash\0' + nonce.encode() return hashlib.sha256(parts).digest() + +def get_scrollback(messages): + n = CONFIG['MAX_CHAT_SCROLLBACK'] + if len(messages) < n: + return messages + return list(messages)[-n:] diff --git a/anonstream/helpers/user.py b/anonstream/helpers/user.py index 74e488b..b262393 100644 --- a/anonstream/helpers/user.py +++ b/anonstream/helpers/user.py @@ -44,14 +44,14 @@ def get_default_name(user): ) def is_watching(timestamp, user): - return user['watching_last'] >= timestamp - CONFIG['THRESHOLD_IDLE'] + return user['watching_last'] >= timestamp - CONFIG['THRESHOLD_USER_IDLE'] def is_idle(timestamp, user): return is_present(timestamp, user) and not is_watching(timestamp, user) def is_present(timestamp, user): return ( - user['seen']['last'] >= timestamp - CONFIG['THRESHOLD_ABSENT'] + user['seen']['last'] >= timestamp - CONFIG['THRESHOLD_USER_ABSENT'] or len(user['websockets']) > 0 ) diff --git a/anonstream/routes/nojs.py b/anonstream/routes/nojs.py index 43c6845..725ce72 100644 --- a/anonstream/routes/nojs.py +++ b/anonstream/routes/nojs.py @@ -3,8 +3,9 @@ from quart import current_app, request, render_template, redirect, url_for, esca from anonstream.stream import get_stream_title from anonstream.user import add_notice, pop_notice, try_change_appearance from anonstream.chat import add_chat_message, Rejected -from anonstream.routes.wrappers import with_user_from +from anonstream.routes.wrappers import with_user_from, render_template_with_etag from anonstream.helpers.user import get_default_name +from anonstream.helpers.chat import get_scrollback from anonstream.utils.chat import generate_nonce from anonstream.utils.user import concatenate_for_notice @@ -20,14 +21,20 @@ async def nojs_info(user): @current_app.route('/chat/messages.html') @with_user_from(request) async def nojs_chat(user): - return await render_template( + return await render_template_with_etag( 'nojs_chat.html', user=user, users_by_token=current_app.users_by_token, - messages=current_app.messages, + messages=get_scrollback(current_app.messages), + timeout=current_app.config['THRESHOLD_NOJS_CHAT_TIMEOUT'], get_default_name=get_default_name, ) +@current_app.route('/chat/redirect') +@with_user_from(request) +async def nojs_chat_redirect(user): + return redirect(url_for('nojs_chat', _anchor='end')) + @current_app.route('/chat/form.html') @with_user_from(request) async def nojs_form(user): diff --git a/anonstream/routes/wrappers.py b/anonstream/routes/wrappers.py index b50dbec..651d73c 100644 --- a/anonstream/routes/wrappers.py +++ b/anonstream/routes/wrappers.py @@ -1,7 +1,8 @@ +import hashlib import time from functools import wraps -from quart import current_app, request, abort, make_response +from quart import current_app, request, abort, make_response, render_template, request from werkzeug.security import check_password_hash from anonstream.user import sunset, user_for_websocket @@ -97,3 +98,12 @@ def with_user_from(context): return wrapper return with_user_from_context + +async def render_template_with_etag(*args, **kwargs): + rendered_template = await render_template(*args, **kwargs) + tag = hashlib.sha256(rendered_template.encode()).hexdigest() + etag = f'W/"{tag}"' + if request.if_none_match.contains_weak(tag): + return '', 304, {'ETag': etag} + else: + return rendered_template, {'ETag': etag} diff --git a/anonstream/static/anonstream.js b/anonstream/static/anonstream.js index 3e5a66d..bc691a3 100644 --- a/anonstream/static/anonstream.js +++ b/anonstream/static/anonstream.js @@ -1,5 +1,5 @@ /* token */ -const token = document.querySelector("body").dataset.token; +const token = document.body.dataset.token; /* insert js-only markup */ const jsmarkup_style_color = '' @@ -7,7 +7,7 @@ const jsmarkup_style_tripcode_display = '' const jsmarkup_info = '
'; const jsmarkup_info_title = '
'; -const jsmarkup_chat_messages = ''; +const jsmarkup_chat_messages = '
    '; const jsmarkup_chat_form = `\
    @@ -69,7 +69,7 @@ const create_chat_message = (object) => { const chat_message_name = document.createElement("span"); chat_message_name.classList.add("chat-message__name"); - chat_message_name.innerText = user.name || default_name[user.broadcaster]; + chat_message_name.innerText = get_user_name({user}); //chat_message_name.dataset.color = user.color; // not working in any browser const chat_message_tripcode_nbsp = document.createElement("span"); @@ -95,9 +95,17 @@ const create_chat_message = (object) => { return chat_message } +const create_and_add_chat_message = (object) => { + const chat_message = create_chat_message(object); + chat_messages.insertAdjacentElement("beforeend", chat_message); + while (chat_messages.children.length > max_chat_scrollback) { + chat_messages.children[0].remove(); + } +} let users = {}; let default_name = {true: "Broadcaster", false: "Anonymous"}; +let max_chat_scrollback = 256; const tidy_stylesheet = ({stylesheet, selector_regex, ignore_condition}) => { const to_delete = []; const to_ignore = new Set(); @@ -148,11 +156,17 @@ const update_user_colors = (token_hash=null) => { stylesheet_color.deleteRule(index); } } -const update_user_name = (token_hash) => { - const name = users[token_hash].name; +const get_user_name({user=null, token_hash}) { + const user = user || users[token_hash] + return user.name || default_name[user.broadcaster]; +} +const update_user_names = (token_hash=null) => { + const token_hashes = token_hash === null ? Object.keys(users) : [token_hash]; for (const chat_message of chat_messages.children) { - if (token_hash === chat_message.dataset.tokenHash) { - chat_message.querySelector(".chat-message__name").innerText = name; + const this_token_hash = chat_message.dataset.tokenHash; + if (token_hashes.includes(this_token_hash) { + const chat_message_name = chat_message.querySelector(".chat-message__name"); + chat_message_name.innerText = get_user_name({token_hash: this_token_hash}); } } } @@ -233,7 +247,8 @@ const update_user_tripcodes = (token_hash=null) => { const this_token_hash = chat_message.dataset.tokenHash; const tripcode = users[this_token_hash].tripcode; if (token_hashes.includes(this_token_hash)) { - chat_message.querySelector(".tripcode").innerText = tripcode === null ? "" : tripcode.digest; + const chat_message_tripcode = chat_message.querySelector(".tripcode"); + chat_message_tripcode.innerText = tripcode === null ? "" : tripcode.digest; } } } @@ -253,7 +268,9 @@ const on_websocket_message = (event) => { info_title.innerText = receipt.title; default_name = receipt.default; + max_chat_scrollback = receipt.scrollback; users = receipt.users; + update_user_names(); update_user_colors(); update_user_tripcodes(); @@ -273,8 +290,7 @@ const on_websocket_message = (event) => { const last_seq = last === null ? null : parseInt(last.dataset.seq); for (const message of receipt.messages) { if (message.seq > last_seq) { - const chat_message = create_chat_message(message); - chat_messages.insertAdjacentElement("beforeend", chat_message); + create_and_add_chat_message(message); } } @@ -305,8 +321,7 @@ const on_websocket_message = (event) => { case "chat": console.log("ws chat", receipt); - const chat_message = create_chat_message(receipt); - chat_messages.insertAdjacentElement("beforeend", chat_message); + create_and_add_chat_message(receipt); chat_messages.scrollTo({ left: 0, top: chat_messages.scrollTopMax, @@ -327,7 +342,7 @@ const on_websocket_message = (event) => { user.name = receipt.name; user.color = receipt.color; user.tripcode = receipt.tripcode; - update_user_name(receipt.token_hash); + update_user_names(receipt.token_hash); update_user_colors(receipt.token_hash); update_user_tripcodes(receipt.token_hash); break; diff --git a/anonstream/static/style.css b/anonstream/static/style.css index c050f18..d408b9f 100644 --- a/anonstream/static/style.css +++ b/anonstream/static/style.css @@ -84,14 +84,14 @@ noscript { } #chat__header { text-align: center; - padding: 1ch 0; + padding: 0.5rem 0; border-bottom: var(--chat-border); } #chat-form_js { display: grid; grid-template: auto var(--button-height) / auto 5rem; grid-gap: 0.375rem; - margin: 0 1ch 1ch 1ch; + margin: 0 0.5rem 0.5rem 0.5rem; } #chat-form_js__submit { grid-column: 2 / span 1; @@ -125,7 +125,7 @@ noscript { #chat-messages_js { list-style: none; margin: 0; - padding: 0 1ch 1ch; + padding: 0 0.5rem 0.5rem; overflow-y: auto; width: 100%; box-sizing: border-box; diff --git a/anonstream/templates/home.html b/anonstream/templates/home.html index f51cbc7..eb5b3c6 100644 --- a/anonstream/templates/home.html +++ b/anonstream/templates/home.html @@ -12,7 +12,7 @@