コミットを比較
3 コミット
a210445025
...
eb8d3d55a2
作成者 | SHA1 | 日付 |
---|---|---|
n9k | eb8d3d55a2 | |
n9k | cd2ba369ae | |
n9k | 4c2d42bed4 |
|
@ -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,7 @@ 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
|
||||
app.emotes = load_emote_schema(app.config['EMOTE_SCHEMA'])
|
||||
|
||||
# State for tasks
|
||||
app.users_update_buffer = set()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,18 +5,20 @@ from anonstream.control.spec import ParseException, Parsed
|
|||
from anonstream.control.spec.common import Str
|
||||
from anonstream.control.spec.methods.allowedness import SPEC as SPEC_ALLOWEDNESS
|
||||
from anonstream.control.spec.methods.chat import SPEC as SPEC_CHAT
|
||||
from anonstream.control.spec.methods.exit import SPEC as SPEC_EXIT
|
||||
from anonstream.control.spec.methods.emote import SPEC as SPEC_EMOTE
|
||||
from anonstream.control.spec.methods.help import SPEC as SPEC_HELP
|
||||
from anonstream.control.spec.methods.quit import SPEC as SPEC_QUIT
|
||||
from anonstream.control.spec.methods.title import SPEC as SPEC_TITLE
|
||||
from anonstream.control.spec.methods.user import SPEC as SPEC_USER
|
||||
|
||||
SPEC = Str({
|
||||
'help': SPEC_HELP,
|
||||
'exit': SPEC_EXIT,
|
||||
'quit': SPEC_QUIT,
|
||||
'title': SPEC_TITLE,
|
||||
'chat': SPEC_CHAT,
|
||||
'user': SPEC_USER,
|
||||
'allowednesss': SPEC_ALLOWEDNESS,
|
||||
'emote': SPEC_EMOTE,
|
||||
})
|
||||
|
||||
async def parse(request):
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
from quart import current_app
|
||||
|
||||
from anonstream.emote import load_emote_schema, BadEmoteName
|
||||
from anonstream.control.spec.common import Str, End
|
||||
from anonstream.control.exceptions import CommandFailed
|
||||
|
||||
CONFIG = current_app.config
|
||||
|
||||
async def cmd_emote_help():
|
||||
normal = ['emote', 'help']
|
||||
response = (
|
||||
'Usage: emote\n'
|
||||
'Commands:\n'
|
||||
' emote reload......try to reload the emote schema (existing messages are not modified)\n'
|
||||
)
|
||||
return normal, response
|
||||
|
||||
async def cmd_emote_reload():
|
||||
try:
|
||||
emotes = load_emote_schema(app.config['EMOTE_SCHEMA'])
|
||||
except BadEmoteName as e:
|
||||
error, *_ = e.args
|
||||
raise CommandFailed(error) from e
|
||||
normal = ['emote', 'reload']
|
||||
response = ''
|
||||
return normal, response
|
||||
|
||||
SPEC = Str({
|
||||
None: End(cmd_emote_help),
|
||||
'help': End(cmd_emote_help),
|
||||
'reload': End(cmd_emote_reload),
|
||||
})
|
||||
|
|
@ -24,6 +24,7 @@ async def cmd_help():
|
|||
' allowedness setdefault BOOLEAN.set the default allowedness\n'
|
||||
' allowedness add SET STRING.....add to the blacklist/whitelist\n'
|
||||
' allowedness remove SET STRING..remove from the blacklist/whitelist\n'
|
||||
' emote reload...................try reloading the emote schema\n'
|
||||
)
|
||||
return normal, response
|
||||
|
||||
|
|
|
@ -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('emote names cannot be empty')
|
||||
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["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)
|
||||
))
|
|
@ -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'''<img class="emote" '''
|
||||
f'''src="{escape(url_for('static', filename=emote_file))}" '''
|
||||
f'''{width}{height}'''
|
||||
f'''alt="{emote_name_markup}" title="{emote_name_markup}">'''
|
||||
)
|
||||
|
||||
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)
|
||||
|
|
|
@ -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'''<img class="emote" '''
|
||||
f'''src="{escape(url_for('static', filename=emote_file))}" '''
|
||||
f'''{width}{height}'''
|
||||
f'''alt="{emote_name_markup}" title="{emote_name_markup}">'''
|
||||
)
|
||||
|
||||
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)
|
|
@ -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)
|
||||
))
|
||||
|
|
読み込み中…
新しいイシューから参照