From 208ef9abc7e21acbf68b755afdb510a516b69425 Mon Sep 17 00:00:00 2001 From: n9k Date: Wed, 20 Jul 2022 06:04:55 +0000 Subject: [PATCH] Emotes: one emote, one file --- anonstream/__init__.py | 8 ++-- anonstream/config.py | 1 - anonstream/helpers/chat.py | 34 +++++++------- anonstream/routes/core.py | 2 +- anonstream/routes/nojs.py | 7 +-- anonstream/static/anonstream.js | 39 ---------------- anonstream/static/style.css | 3 -- anonstream/templates/nojs_chat_messages.html | 12 ----- anonstream/utils/chat.py | 49 ++++---------------- anonstream/websocket.py | 3 -- config.toml | 1 - 11 files changed, 31 insertions(+), 128 deletions(-) diff --git a/anonstream/__init__.py b/anonstream/__init__.py index f588a93..2e78d2b 100644 --- a/anonstream/__init__.py +++ b/anonstream/__init__.py @@ -9,7 +9,7 @@ from quart_compress import Compress from anonstream.config import update_flask_from_toml from anonstream.quart import Quart from anonstream.utils.captcha import create_captcha_factory, create_captcha_signer -from anonstream.utils.chat import schema_to_emotes +from anonstream.utils.chat import precompute_emote_regex from anonstream.utils.user import generate_blank_allowedness __version__ = '1.5.5' @@ -47,9 +47,11 @@ def create_app(toml_config): app.failures = OrderedDict() # access captcha failures app.allowedness = generate_blank_allowedness() + # Read emote schema with open(app.config['EMOTE_SCHEMA']) as fp: - schema = json.load(fp) - app.emotes = schema_to_emotes(schema) + emotes = json.load(fp) + precompute_emote_regex(emotes) + app.emotes = emotes # State for tasks app.users_update_buffer = set() diff --git a/anonstream/config.py b/anonstream/config.py index 023a241..e9f842f 100644 --- a/anonstream/config.py +++ b/anonstream/config.py @@ -169,6 +169,5 @@ def toml_to_flask_section_nojs(config): def toml_to_flask_section_emote(config): cfg = config['emote'] return { - 'EMOTE_SHEET': cfg['sheet'], 'EMOTE_SCHEMA': cfg['schema'], } diff --git a/anonstream/helpers/chat.py b/anonstream/helpers/chat.py index d538dec..81a517f 100644 --- a/anonstream/helpers/chat.py +++ b/anonstream/helpers/chat.py @@ -2,9 +2,10 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import hashlib +from functools import lru_cache import markupsafe -from quart import current_app, escape, Markup +from quart import current_app, escape, url_for, Markup CONFIG = current_app.config EMOTES = current_app.emotes @@ -19,24 +20,21 @@ def get_scrollback(messages): return messages return list(messages)[-n:] +@lru_cache +def get_emote_markup(emote_name, emote_file, emote_width, emote_height): + emote_name_markup = escape(emote_name) + return Markup( + f'''''' + ) + def insert_emotes(markup): assert isinstance(markup, markupsafe.Markup) - for name, regex, _position, _size in EMOTES: - emote_markup = ( - f'{escape(name)}' + for emote in EMOTES: + emote_markup = get_emote_markup( + emote['name'], emote['file'], emote['width'], emote['height'], ) - markup = regex.sub(emote_markup, markup) + markup = emote['regex'].sub(emote_markup, markup) return Markup(markup) - -def get_emotes_for_websocket(): - return { - name: { - 'x': position[0], - 'y': position[1], - 'width': size[0], - 'height': size[1], - } - for name, _regex, position, size in EMOTES - } - return tuple(EMOTES.values()) diff --git a/anonstream/routes/core.py b/anonstream/routes/core.py index ce2ef5c..d1ea26c 100644 --- a/anonstream/routes/core.py +++ b/anonstream/routes/core.py @@ -135,7 +135,7 @@ async def access(timestamp, user_or_token): url = url_for('home', token=user['token']) return redirect(url, 303) -@current_app.route('/static/') +@current_app.route('/static/') @with_user_from(request) @etag_conditional @clean_cache_headers diff --git a/anonstream/routes/nojs.py b/anonstream/routes/nojs.py index a487174..412ffbe 100644 --- a/anonstream/routes/nojs.py +++ b/anonstream/routes/nojs.py @@ -10,13 +10,12 @@ from anonstream.user import add_state, pop_state, try_change_appearance, update_ from anonstream.routes.wrappers import with_user_from, render_template_with_etag from anonstream.helpers.chat import get_scrollback from anonstream.helpers.user import get_default_name -from anonstream.utils.chat import generate_nonce, escape_css_string, get_emotehash +from anonstream.utils.chat import generate_nonce from anonstream.utils.security import generate_csp from anonstream.utils.user import concatenate_for_notice CONFIG = current_app.config USERS_BY_TOKEN = current_app.users_by_token -EMOTES = current_app.emotes @current_app.route('/stream.html') @with_user_from(request) @@ -54,12 +53,8 @@ async def nojs_chat_messages(timestamp, user): refresh=CONFIG['NOJS_REFRESH_MESSAGES'], user=user, users_by_token=USERS_BY_TOKEN, - emotes=EMOTES, - emotesheet=CONFIG['EMOTE_SHEET'], - emotehash=get_emotehash(tuple(EMOTES)), messages=get_scrollback(current_app.messages), timeout=CONFIG['NOJS_TIMEOUT_CHAT'], - escape_css_string=escape_css_string, get_default_name=get_default_name, ) diff --git a/anonstream/static/anonstream.js b/anonstream/static/anonstream.js index e8665ca..e2d3ae7 100644 --- a/anonstream/static/anonstream.js +++ b/anonstream/static/anonstream.js @@ -84,12 +84,6 @@ const insert_jsmarkup = () => { style_tripcode_colors.nonce = CSP; document.head.insertAdjacentElement("beforeend", style_tripcode_colors); } - if (document.getElementById("style-emote") === null) { - const style_emote = document.createElement("style"); - style_emote.id = "style-emote"; - style_emote.nonce = CSP; - document.head.insertAdjacentElement("beforeend", style_emote); - } if (document.getElementById("stream__video") === null) { const parent = document.getElementById("stream"); parent.insertAdjacentHTML("beforeend", jsmarkup_stream_video); @@ -140,7 +134,6 @@ insert_jsmarkup(); const stylesheet_color = document.styleSheets[1]; const stylesheet_tripcode_display = document.styleSheets[2]; const stylesheet_tripcode_colors = document.styleSheets[3]; -const stylesheet_emote = document.styleSheets[4]; /* override chat form notice button */ const chat_form = document.getElementById("chat-form_js"); @@ -305,39 +298,11 @@ const escape_css_string = (string) => { } return result.join(""); } -const update_emotes = async (emotes) => { - const rules = []; - for (const key of Object.keys(emotes)) { - const emote = emotes[key]; - rules.push( - `[data-emote="${escape_css_string(key)}"] { background-position: ${-emote.x}px ${-emote.y}px; width: ${emote.width}px; height: ${emote.height}px; }` - ); - } - rules.sort(); - const emotehash = await hexdigest(rules.toString(), 6); - const emotehash_rule = `.emote { background-image: url("/static/${escape_css_string(escape(emotesheet))}?coords=${escape_css_string(encodeURIComponent(emotehash))}"); }`; - - const rules_set = new Set([emotehash_rule, ...rules]); - const to_delete = []; - for (let index = 0; index < stylesheet_emote.cssRules.length; index++) { - const css_rule = stylesheet_emote.cssRules[index]; - if (!rules_set.delete(css_rule.cssText)) { - to_delete.push(index); - } - } - for (const rule of rules_set) { - stylesheet_emote.insertRule(rule); - } - for (const index of to_delete.reverse()) { - stylesheet_emote.deleteRule(index + rules_set.size); - } -} let users = {}; let stats = null; let stats_received = null; let default_name = {true: "Broadcaster", false: "Anonymous"}; -let emotesheet = "emotes.png"; let max_chat_scrollback = 256; let pingpong_period = 8.0; let ping = null; @@ -735,10 +700,6 @@ const on_websocket_message = async (event) => { chat_appearance_form_name.setAttribute("placeholder", default_name[user.broadcaster]); chat_appearance_form_color.setAttribute("value", user.color); - // emotes - emotesheet = receipt.emotesheet; - await update_emotes(receipt.emotes); - // insert new messages const last = chat_messages.children.length == 0 ? null : chat_messages.children[chat_messages.children.length - 1]; const last_seq = last === null ? null : parseInt(last.dataset.seq); diff --git a/anonstream/static/style.css b/anonstream/static/style.css index d48d840..836230a 100644 --- a/anonstream/static/style.css +++ b/anonstream/static/style.css @@ -264,10 +264,7 @@ noscript { line-height: 1.3125; } .emote { - display: inline-block; - font-size: 0; vertical-align: middle; - cursor: default; } .tripcode { padding: 0 5px; diff --git a/anonstream/templates/nojs_chat_messages.html b/anonstream/templates/nojs_chat_messages.html index 8b59731..30ba8bf 100644 --- a/anonstream/templates/nojs_chat_messages.html +++ b/anonstream/templates/nojs_chat_messages.html @@ -134,11 +134,7 @@ line-height: 1.3125; } .emote { - background-image: url("{{ escape_css_string(url_for('static', filename=emotesheet, coords=emotehash)) | safe }}"); - display: inline-block; - font-size: 0; vertical-align: middle; - cursor: default; } .tripcode { padding: 0 5px; @@ -161,14 +157,6 @@ {% endif %} {% endwith %} {% endfor %} - - {% for name, _regex, (x, y), (width, height) in emotes %} - [data-emote="{{ escape_css_string(name) | safe }}"] { - background-position: {{ -x }}px {{ -y }}px; - width: {{ width }}px; - height: {{ height }}px; - } - {% endfor %} diff --git a/anonstream/utils/chat.py b/anonstream/utils/chat.py index abc9e13..86d49ab 100644 --- a/anonstream/utils/chat.py +++ b/anonstream/utils/chat.py @@ -31,52 +31,19 @@ def get_approx_linespan(text): linespan = linespan if linespan > 0 else 1 return linespan -def schema_to_emotes(schema): - emotes = [] - for name, coords in schema.items(): +def precompute_emote_regex(schema): + for emote in schema: assert emote['name'], 'emote names cannot be empty' - assert not re.search(r'\s', name), \ + assert not re.search(r'\s', emote['name']), \ 'whitespace is not allowed in emote names' - name_markup = escape(name) # If the emote name begins with a word character [a-zA-Z0-9_], # match only if preceded by a non-word character or the empty # string. Similarly for the end of the emote name. # Examples: # * ":joy:" matches "abc :joy:~xyz" and "abc:joy:xyz" # * "JoySi" matches "abc JoySi~xyz" but NOT "abcJoySiabc" - onset = r'(?:^|(?<=\W))' if re.fullmatch(r'\w', name[0]) else r'' - finish = r'(?:$|(?=\W))' if re.fullmatch(r'\w', name[-1]) else r'' - regex = re.compile(''.join((onset, re.escape(name_markup), finish))) - position, size = tuple(coords['position']), tuple(coords['size']) - emotes.append((name, regex, position, size)) - return emotes - -def escape_css_string(string): - ''' - https://drafts.csswg.org/cssom/#common-serializing-idioms - ''' - result = [] - for char in string: - if char == '\0': - result.append('\ufffd') - elif char < '\u0020' or char == '\u007f': - result.append(f'\\{ord(char):x}') - elif char == '"' or char == '\\': - result.append(f'\\{char}') - else: - result.append(char) - return ''.join(result) - -@lru_cache(maxsize=1) -def get_emotehash(emotes): - rules = [] - for name, _regex, (x, y), (width, height) in sorted(emotes): - rule = ( - f'[data-emote="{escape_css_string(name)}"] ' - f'{{ background-position: {-x}px {-y}px; ' - f'width: {width}px; height: {height}px; }}' - ) - rules.append(rule.encode()) - plaintext = b','.join(rules) - digest = hashlib.sha256(plaintext).digest() - return digest[:6].hex() + onset = r'(?:^|(?<=\W))' if re.fullmatch(r'\w', emote['name'][0]) else r'' + finish = r'(?:$|(?=\W))' if re.fullmatch(r'\w', emote['name'][-1]) else r'' + emote['regex'] = re.compile(''.join( + (onset, re.escape(escape(emote['name'])), finish) + )) diff --git a/anonstream/websocket.py b/anonstream/websocket.py index 4e5aa5e..a20d393 100644 --- a/anonstream/websocket.py +++ b/anonstream/websocket.py @@ -11,7 +11,6 @@ from anonstream.captcha import get_random_captcha_digest_for from anonstream.chat import get_all_messages_for_websocket, add_chat_message, Rejected from anonstream.user import get_all_users_for_websocket, see, reading, verify, deverify, BadCaptcha, try_change_appearance, ensure_allowedness, AllowednessException from anonstream.wrappers import with_timestamp, get_timestamp -from anonstream.helpers.chat import get_emotes_for_websocket from anonstream.utils.chat import generate_nonce from anonstream.utils.user import identifying_string from anonstream.utils.websocket import parse_websocket_data, Malformed, WS @@ -37,8 +36,6 @@ async def websocket_outbound(queue, user): 'scrollback': CONFIG['MAX_CHAT_SCROLLBACK'], 'digest': get_random_captcha_digest_for(user), 'pingpong': CONFIG['TASK_BROADCAST_PING'], - 'emotes': get_emotes_for_websocket(), - 'emotesheet': CONFIG['EMOTE_SHEET'], }) while True: payload = await queue.get() diff --git a/config.toml b/config.toml index 076ddef..e727d6d 100644 --- a/config.toml +++ b/config.toml @@ -91,5 +91,4 @@ refresh_users = 6.0 timeout_chat = 30.0 [emote] -sheet = "emotes.png" schema = "emotes.json"