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)
- ))