From c2094f1d89ae249964698bee282166fa8c448cf4 Mon Sep 17 00:00:00 2001 From: n9k Date: Sun, 31 Jul 2022 22:56:34 +0000 Subject: [PATCH] Emotes: reorganize --- anonstream/__init__.py | 10 +++++----- anonstream/chat.py | 3 ++- anonstream/emote.py | 39 +++++++++++++++++++++++++++++++++++++ anonstream/helpers/chat.py | 26 +------------------------ anonstream/helpers/emote.py | 27 +++++++++++++++++++++++++ anonstream/utils/chat.py | 20 ------------------- 6 files changed, 74 insertions(+), 51 deletions(-) create mode 100644 anonstream/emote.py create mode 100644 anonstream/helpers/emote.py diff --git a/anonstream/__init__.py b/anonstream/__init__.py index abd8b8d..1f514e1 100644 --- a/anonstream/__init__.py +++ b/anonstream/__init__.py @@ -8,9 +8,9 @@ from collections import OrderedDict from quart_compress import Compress from anonstream.config import update_flask_from_toml +from anonstream.emote import load_emote_schema from anonstream.quart import Quart from anonstream.utils.captcha import create_captcha_factory, create_captcha_signer -from anonstream.utils.chat import precompute_emote_regex from anonstream.utils.user import generate_blank_allowedness __version__ = '1.6.4' @@ -49,10 +49,10 @@ def create_app(toml_config): app.allowedness = generate_blank_allowedness() # Read emote schema - with open(app.config['EMOTE_SCHEMA']) as fp: - emotes = json.load(fp) - precompute_emote_regex(emotes) - app.emotes = emotes + try: + app.emotes = load_emote_schema(app.config['EMOTE_SCHEMA']) + except (OSError, json.JSONDecodeError) as e: + raise AssertionError(f'couldn\'t load emote schema: {e!r}') from e # State for tasks app.users_update_buffer = set() diff --git a/anonstream/chat.py b/anonstream/chat.py index 68ceaba..7075411 100644 --- a/anonstream/chat.py +++ b/anonstream/chat.py @@ -8,7 +8,8 @@ from quart import current_app, escape from anonstream.broadcast import broadcast, broadcast_users_update from anonstream.events import notify_event_sockets -from anonstream.helpers.chat import generate_nonce_hash, get_scrollback, insert_emotes +from anonstream.helpers.chat import generate_nonce_hash, get_scrollback +from anonstream.helpers.emote import insert_emotes from anonstream.utils.chat import get_message_for_websocket, get_approx_linespan CONFIG = current_app.config diff --git a/anonstream/emote.py b/anonstream/emote.py new file mode 100644 index 0000000..f1c33c7 --- /dev/null +++ b/anonstream/emote.py @@ -0,0 +1,39 @@ +import json +import re + +from quart import escape + +class BadEmoteName(Exception): + pass + +def load_emote_schema(filepath): + with open(filepath) as fp: + emotes = json.load(fp) + precompute_emote_regex(emotes) + return emotes + +def precompute_emote_regex(schema): + for emote in schema: + if not emote['name']: + raise BadEmoteName(f'emote names cannot be empty: {emote}') + if re.search(r'\s', emote['name']): + raise BadEmoteName( + f'whitespace is not allowed in emote names: {emote["name"]!r}' + ) + for length in (emote['width'], emote['height']): + if length is not None and (not isinstance(length, int) or length < 0): + raise BadEmoteName( + f'emote dimensions must be null or non-negative integers: ' + f'{emote}' + ) + # 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', 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/helpers/chat.py b/anonstream/helpers/chat.py index 0ecc46c..3feac2a 100644 --- a/anonstream/helpers/chat.py +++ b/anonstream/helpers/chat.py @@ -2,13 +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, url_for, Markup +from quart import current_app CONFIG = current_app.config -EMOTES = current_app.emotes def generate_nonce_hash(nonce): parts = CONFIG['SECRET_KEY'] + b'nonce-hash\0' + nonce.encode() @@ -19,24 +16,3 @@ def get_scrollback(messages): if len(messages) < n: 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) - width = '' if emote_width is None else f'width="{escape(emote_width)}" ' - height = '' if emote_height is None else f'height="{escape(emote_height)}" ' - return Markup( - f'''''' - ) - -def insert_emotes(markup): - assert isinstance(markup, markupsafe.Markup) - for emote in EMOTES: - emote_markup = get_emote_markup( - emote['name'], emote['file'], emote['width'], emote['height'], - ) - markup = emote['regex'].sub(emote_markup, markup) - return Markup(markup) diff --git a/anonstream/helpers/emote.py b/anonstream/helpers/emote.py new file mode 100644 index 0000000..47dcaba --- /dev/null +++ b/anonstream/helpers/emote.py @@ -0,0 +1,27 @@ +import markupsafe +from functools import lru_cache + +from quart import current_app, escape, url_for, Markup + +EMOTES = current_app.emotes + +@lru_cache +def get_emote_markup(emote_name, emote_file, emote_width, emote_height): + emote_name_markup = escape(emote_name) + width = '' if emote_width is None else f'width="{escape(emote_width)}" ' + height = '' if emote_height is None else f'height="{escape(emote_height)}" ' + return Markup( + f'''''' + ) + +def insert_emotes(markup): + assert isinstance(markup, markupsafe.Markup) + for emote in EMOTES: + emote_markup = get_emote_markup( + emote['name'], emote['file'], emote['width'], emote['height'], + ) + markup = emote['regex'].sub(emote_markup, markup) + return Markup(markup) diff --git a/anonstream/utils/chat.py b/anonstream/utils/chat.py index 916a49c..39ff75f 100644 --- a/anonstream/utils/chat.py +++ b/anonstream/utils/chat.py @@ -30,23 +30,3 @@ def get_approx_linespan(text): linespan = sum(map(height, text.splitlines())) linespan = linespan if linespan > 0 else 1 return linespan - -def precompute_emote_regex(schema): - for emote in schema: - assert emote['name'], 'emote names cannot be empty' - assert not re.search(r'\s', emote['name']), \ - f'whitespace is not allowed in emote names: {emote["name"]!r}' - for length in (emote['width'], emote['height']): - assert length is None or isinstance(length, int) and length >= 0, \ - f'emote dimensions must be null or non-negative integers: {emote["name"]!r}' - # 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', 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) - ))