unwieldy commit 2

このコミットが含まれているのは:
n9k 2022-07-29 08:04:54 +00:00
コミット 6bc4903f3c
12個のファイルの変更118行の追加80行の削除

ファイルの表示

@ -2,17 +2,21 @@ from quart import current_app
LOCALES = current_app.locales
def get_lang_and_locale_from(context):
def get_lang_and_locale_from(context, burrow=(), validate=True):
lang = context.args.get('lang')
locale = LOCALES.get(lang)
if locale is None:
lang, locale = None, LOCALES[None]
if validate:
lang = None
locale = LOCALES[None]
for key in burrow:
locale = locale[key]
return lang, locale
def get_lang_from(context):
lang, locale = get_lang_and_locale_from(context)
def get_lang_from(context, validate=True):
lang, locale = get_lang_and_locale_from(context, validate=validate)
return lang
def get_locale_from(context):
def get_locale_from(context, burrow=()):
lang, locale = get_lang_and_locale_from(context)
return locale

ファイルの表示

@ -26,7 +26,7 @@ LANG = current_app.lang
@current_app.route('/')
@with_user_from(request, fallback_to_token=True, ignore_allowedness=True)
async def home(timestamp, user_or_token):
lang, locale = get_lang_and_locale_from(request)
lang, locale = get_lang_and_locale_from(request, burrow=('anonstream',))
match user_or_token:
case str() | None as token:
failure_id = request.args.get('failure', type=int)
@ -35,25 +35,27 @@ async def home(timestamp, user_or_token):
'captcha.html',
csp=generate_csp(),
token=token,
locale=locale['anonstream']['captcha'],
request_lang=get_lang_from(request, validate=False),
locale=locale['captcha'],
digest=get_random_captcha_digest(),
failure=locale['anonstream']['internal'].get(failure),
failure=locale['internal'].get(failure),
)
case dict() as user:
try:
ensure_allowedness(user, timestamp=timestamp)
except Blacklisted:
raise Forbidden(locale['anonstream']['error']['blacklisted'])
raise Forbidden(locale['error']['blacklisted'])
except SecretClub:
# TODO allow changing tripcode
raise Forbidden(locale['anonstream']['error']['not_whitelisted'])
raise Forbidden(locale['error']['not_whitelisted'])
else:
response = await render_template(
'home.html',
csp=generate_csp(),
user=user,
lang=lang or LANG,
locale=locale['anonstream']['home'],
lang=lang,
default_lang=LANG,
locale=locale['home'],
version=current_app.version,
)
return response
@ -99,7 +101,7 @@ async def stream(timestamp, user):
@current_app.route('/login')
@auth_required
async def login():
return redirect(url_for('home'), 303)
return redirect(url_for('home', lang=get_lang_from(request)), 303)
@current_app.route('/captcha.jpg')
@with_user_from(request, fallback_to_token=True)
@ -114,7 +116,6 @@ async def captcha(timestamp, user_or_token):
@current_app.post('/access')
@with_user_from(request, fallback_to_token=True, ignore_allowedness=True)
async def access(timestamp, user_or_token):
lang = get_lang_from(request)
match user_or_token:
case str() | None as token:
form = await request.form
@ -130,12 +131,11 @@ async def access(timestamp, user_or_token):
case Answer.OK:
failure_id = None
user = generate_and_add_user(timestamp, token, verified=True)
if failure_id is not None:
url = url_for('home', token=token, lang=lang, failure=failure_id)
raise abort(redirect(url, 303))
case dict() as user:
pass
url = url_for('home', token=user['token'])
token = user['token']
failure_id = None
lang = get_lang_from(request, validate=failure_id is None)
url = url_for('home', token=token, lang=lang, failure=failure_id)
return redirect(url, 303)
@current_app.route('/static/<path:filename>')

ファイルの表示

@ -6,12 +6,11 @@ from anonstream.locale import get_locale_from
for error in default_exceptions:
async def handle(error):
locale = get_locale_from(request)['http']
error.description = locale.get(error.description)
if localized_name := locale.get(str(error.code)):
error.name = localized_name
if error.description == error.__class__.description:
error.description = None
return (
await render_template(
'error.html',
error=error,
locale=locale,
), error.code
await render_template('error.html', error=error), error.code
)
current_app.register_error_handler(error, handle)

ファイルの表示

@ -5,7 +5,7 @@ from quart import current_app, request, render_template, redirect, url_for, esca
from anonstream.captcha import get_random_captcha_digest_for
from anonstream.chat import add_chat_message, Rejected
from anonstream.locale import get_locale_from
from anonstream.locale import get_lang_and_locale_from, get_lang_from, get_locale_from
from anonstream.stream import is_online, get_stream_title, get_stream_uptime_and_viewership
from anonstream.user import add_state, pop_state, try_change_appearance, update_presence, get_users_by_presence, Presence, verify, deverify, BadCaptcha, reading
from anonstream.routes.wrappers import with_user_from, render_template_with_etag
@ -64,7 +64,12 @@ async def nojs_chat_messages(timestamp, user):
@current_app.route('/chat/messages')
@with_user_from(request)
async def nojs_chat_messages_redirect(timestamp, user):
url = url_for('nojs_chat_messages', token=user['token'], _anchor='end')
url = url_for(
'nojs_chat_messages',
token=user['token'],
lang=get_lang_from(request),
_anchor='end',
)
return redirect(url, 303)
@current_app.route('/chat/users.html')
@ -86,17 +91,18 @@ async def nojs_chat_users(timestamp, user):
@current_app.route('/chat/form.html')
@with_user_from(request)
async def nojs_chat_form(timestamp, user):
lang, locale = get_lang_and_locale_from(request)
state_id = request.args.get('state', type=int)
state = pop_state(user, state_id)
prefer_chat_form = request.args.get('landing') != 'appearance'
print(state)
return await render_template(
'nojs_chat_form.html',
csp=generate_csp(),
user=user,
prefer_chat_form=prefer_chat_form,
state=state,
locale=get_locale_from(request)['anonstream'],
lang=lang,
locale=locale['anonstream'],
nonce=generate_nonce(),
digest=get_random_captcha_digest_for(user),
default_name=get_default_name(user),
@ -116,7 +122,12 @@ async def nojs_chat_form_redirect(timestamp, user):
)
else:
state_id = None
url = url_for('nojs_chat_form', token=user['token'], state=state_id)
url = url_for(
'nojs_chat_form',
token=user['token'],
lang=get_lang_from(request),
state=state_id,
)
return redirect(url, 303)
@current_app.post('/chat/message')
@ -162,6 +173,7 @@ async def nojs_submit_message(timestamp, user):
url = url_for(
'nojs_chat_form',
token=user['token'],
lang=get_lang_from(request),
landing='chat',
state=state_id,
)
@ -200,6 +212,7 @@ async def nojs_submit_appearance(timestamp, user):
url = url_for(
'nojs_chat_form',
token=user['token'],
lang=get_lang_from(request),
landing='appearance' if errors else 'chat',
state=state_id,
)

ファイルの表示

@ -8,11 +8,12 @@ import string
from functools import wraps
from urllib.parse import quote, unquote
from quart import current_app, request, make_response, render_template, request, url_for, Markup
from quart import current_app, request, make_response, render_template, request, url_for, escape, Markup
from werkzeug.exceptions import BadRequest, Unauthorized, Forbidden
from werkzeug.security import check_password_hash
from anonstream.broadcast import broadcast
from anonstream.locale import get_lang_and_locale_from, get_locale_from
from anonstream.user import ensure_allowedness, Blacklisted, SecretClub
from anonstream.helpers.user import generate_user
from anonstream.utils.user import generate_token, Presence
@ -53,18 +54,11 @@ def auth_required(f):
async def wrapper(*args, **kwargs):
if check_auth(request):
return await f(*args, **kwargs)
hint = (
'The broadcaster should log in with the credentials printed in '
'their terminal.'
)
locale = get_locale_from(request)['anonstream']['error']
if request.authorization is None:
description = hint
description = locale['broadcaster_should_log_in']
else:
description = Markup(
f'Wrong username or password. Refresh the page to try again. '
f'<br>'
f'{hint}'
)
description = locale['wrong_username_or_password']
error = Unauthorized(description)
response = await current_app.handle_http_exception(error)
response = await make_response(response)
@ -107,11 +101,11 @@ def with_user_from(context, fallback_to_token=False, ignore_allowedness=False):
# Reject invalid tokens
if isinstance(token, str) and not RE_TOKEN.fullmatch(token):
raise BadRequest(Markup(
f'Your token contains disallowed characters or is too '
f'long. Tokens must match this regular expression: <br>'
f'<code>{RE_TOKEN.pattern}</code>'
))
locale = get_locale_from(context)
args = (
Markup(f'<br><code>{RE_TOKEN.pattern}</code>'),
)
raise BadRequest(escape(locale['invalid_token']) % args)
# Only logged in broadcaster may have the broadcaster's token
if (
@ -119,15 +113,16 @@ def with_user_from(context, fallback_to_token=False, ignore_allowedness=False):
and isinstance(token, str)
and hmac.compare_digest(token, CONFIG['AUTH_TOKEN'])
):
raise Unauthorized(Markup(
f"You are using the broadcaster's token but you are "
f"not logged in. The broadcaster should "
f"<a href=\"{url_for('login')}\" target=\"_top\">"
f"click here"
f"</a> "
f"and log in with the credentials printed in their "
f"terminal when they started anonstream."
))
lang, locale = get_lang_and_locale_from(
context, burrow=('anonstream', 'error'),
)
args = (
Markup(f'''<a href="{url_for('login', lang=lang)}" target="_top">'''),
Markup(f'''</a>'''),
)
raise Unauthorized(
escape(locale['impostor']) % args
)
# Create response
user = USERS_BY_TOKEN.get(token)
@ -136,19 +131,25 @@ def with_user_from(context, fallback_to_token=False, ignore_allowedness=False):
user['last']['seen'] = timestamp
user['headers'] = tuple(context.headers)
if not ignore_allowedness:
assert_allowedness(timestamp, user)
assert_allowedness(context, timestamp, user)
if user is not None and user['verified'] is not None:
response = await f(timestamp, user, *args, **kwargs)
elif fallback_to_token:
#assert not broadcaster
response = await f(timestamp, token, *args, **kwargs)
else:
raise Forbidden(Markup(
f"You have not solved the access captcha. "
f"<a href=\"{url_for('home', token=token)}\" target=\"_top\">"
f"Click here."
f"</a>"
))
lang, locale = get_lang_and_locale_from(
context, burrow=('anonstream', 'error'),
)
args = (
Markup(f'''<a href="{url_for('home', token=token, lang=lang)}" target="_top">'''),
Markup(f'''</a>'''),
)
if user is None:
string = locale['captcha']
else:
string = locale['captcha_again']
raise Forbidden(escape(string) % args)
else:
if user is not None:
user['last']['seen'] = timestamp
@ -161,7 +162,7 @@ def with_user_from(context, fallback_to_token=False, ignore_allowedness=False):
headers=tuple(context.headers),
)
if not ignore_allowedness:
assert_allowedness(timestamp, user)
assert_allowedness(context, timestamp, user)
response = await f(timestamp, user, *args, **kwargs)
# Set cookie
@ -229,10 +230,12 @@ def etag_conditional(f):
return wrapper
def assert_allowedness(timestamp, user):
def assert_allowedness(context, timestamp, user):
try:
ensure_allowedness(user, timestamp=timestamp)
except Blacklisted as e:
raise Forbidden('You have been blacklisted.')
locale = get_locale_from(context)['anonstream']['error']
raise Forbidden(locale['blacklisted'])
except SecretClub as e:
raise Forbidden('You have not been whitelisted.')
locale = get_locale_from(context)['anonstream']['error']
raise Forbidden(locale['whitelisted'])

ファイルの表示

@ -55,7 +55,7 @@
</head>
<body>
<img src="{{ url_for('captcha', digest=digest) }}" width="72" height="30">
<form action="{{ url_for('access', token=token) }}" method="post">
<form action="{{ url_for('access', token=token, lang=request_lang) }}" method="post">
<input type="hidden" name="digest" value="{{ digest }}">
<input name="answer" placeholder="{{ locale.captcha }}" required autofocus>
<input type="submit" value="Submit">

ファイルの表示

@ -7,7 +7,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ error.code }} {{ locale[error.code | string] or error.name }}</title>
<title>{{ error.code }} {{ error.name }}</title>
<style>
body {
background-color: #232327;
@ -63,7 +63,7 @@
</head>
<body>
<main>
<h1>{{ locale[error.code | string] or error.name }}</h1>
<h1>{{ error.name }}</h1>
{% if error.description is not none %}
<p>{{ error.description }}</p>
{% endif %}

ファイルの表示

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
##}
<!doctype html>
<html id="nochat" lang="{{ lang }}">
<html id="nochat" lang="{{ lang or default_lang }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -15,7 +15,7 @@
<noscript><iframe id="stream_nojs" name="stream_nojs" src="{{ url_for('nojs_stream', token=user.token) }}"></iframe></noscript>
</article>
<article id="info">
<noscript><iframe id="info_nojs" src="{{ url_for('nojs_info', token=user.token) }}" data-js="false"></iframe></noscript>
<noscript><iframe id="info_nojs" src="{{ url_for('nojs_info', token=user.token, lang=lang) }}" data-js="false"></iframe></noscript>
</article>
<aside id="chat">
<input id="chat__toggle" type="checkbox">
@ -25,15 +25,15 @@
</header>
<article id="chat__body">
<section id="chat__body__messages">
<noscript><iframe id="chat-messages_nojs" src="{{ url_for('nojs_chat_messages', token=user.token, _anchor='end') }}" data-js="false"></iframe></noscript>
<noscript><iframe id="chat-messages_nojs" src="{{ url_for('nojs_chat_messages', token=user.token, lang=lang, _anchor='end') }}" data-js="false"></iframe></noscript>
</section>
<section id="chat__body__users">
<header id="chat-users-header"><h4>{{ locale.users_in_chat }}</h4></header>
<noscript><iframe id="chat-users_nojs" src="{{ url_for('nojs_chat_users', token=user.token) }}" data-js="false"></iframe></noscript>
<noscript><iframe id="chat-users_nojs" src="{{ url_for('nojs_chat_users', token=user.token, lang=lang) }}" data-js="false"></iframe></noscript>
</section>
</article>
<section id="chat__form">
<noscript><iframe id="chat-form_nojs" src="{{ url_for('nojs_chat_form', token=user.token) }}" data-js="false"></iframe></noscript>
<noscript><iframe id="chat-form_nojs" src="{{ url_for('nojs_chat_form', token=user.token, lang=lang) }}" data-js="false"></iframe></noscript>
</section>
</aside>
<nav id="nav">

ファイルの表示

@ -136,7 +136,7 @@
#appearance-form__buttons {
grid-column: 1 / span 3;
display: grid;
grid-template-columns: auto 5rem;
grid-template-columns: auto 6rem;
}
#password-column {
display: grid;
@ -224,7 +224,7 @@
<small>{{ locale.form.click_to_dismiss }}</small>
</label>
{% endif %}
<form id="chat-form" action="{{ url_for('nojs_submit_message', token=user.token) }}" method="post">
<form id="chat-form" action="{{ url_for('nojs_submit_message', token=user.token, lang=lang) }}" method="post">
<input type="hidden" name="nonce" value="{{ nonce }}">
<textarea id="chat-form__comment" name="comment" maxlength="{{ max_comment_length }}" {% if digest is none %}required {% endif %} placeholder="{{ locale.form.send_a_message }}" rows="1" tabindex="1" autofocus accesskey="m">{{ state.comment }}</textarea>
<input id="chat-form__submit" type="submit" value="{{ locale.form.chat }}" tabindex="4" accesskey="p">
@ -235,7 +235,7 @@
<input id="chat-form__captcha-answer" name="captcha-answer" required placeholder="Captcha" tabindex="3">
{% endif %}
</form>
<form id="appearance-form" action="{{ url_for('nojs_submit_appearance', token=user.token) }}" method="post">
<form id="appearance-form" action="{{ url_for('nojs_submit_appearance', token=user.token, lang=lang) }}" method="post">
<label id="appearance-form__label-name" for="appearance-form__name">{{ locale.form.name }}</label>
<input id="appearance-form__name" name="name" value="{{ user.name or '' }}" placeholder="{{ default_name }}" maxlength="{{ max_name_length }}">
<input type="color" name="color" value="{{ user.color }}">

ファイルの表示

@ -3,6 +3,12 @@ import types
SPEC = {
'anonstream': {
'error': {
'invalid_token': str,
'captcha': str,
'captcha_again': str,
'impostor': str,
'broadcaster_should_log_in': str,
'wrong_username_or_password': str,
'blacklisted': str,
'not_whitelisted': str,
'offline': str,
@ -32,6 +38,9 @@ SPEC = {
'click_for_a_new_captcha': str,
},
'home': {
'info': str,
'chat': str,
'both': str,
'source': str,
'users': str,
'users_in_chat': str,

ファイルの表示

@ -2,7 +2,7 @@ secret_key = "place secret key here"
[locale]
default = "en"
offered = ["en"]
offered = ["en", "de"]
directory = "l10n/"
[socket.control]
@ -31,6 +31,7 @@ file_cache_lifetime = 0.5
[access]
captcha = true
hide_offered_locales = 0 #"don't" "from-new" "from-everyone"
[captcha]
lifetime = 1800

ファイルの表示

@ -1,11 +1,17 @@
{
"anonstream": {
"error": {
"invalid_token": "Your token contains disallowed characters or is too long. Tokens must match this regular expression: %s",
"captcha": "You have not solved the access captcha. %sClick here.%s",
"captcha_again": "You must solve the access captcha again because you have been away. %sClick here.%s",
"impostor": "You are using the broadcaster's token but you are not logged in. The broadcaster should %sclick here%s and log in with the credentials printed in their terminal when they started anonstream.",
"broadcaster_should_log_in": "The broadcaster should log in with the credentials printed in their terminal.",
"wrong_username_or_password": "Wrong username or password. Refresh the page to try again. %sThe broadcaster should log in with the credentials printed in their terminal.",
"blacklisted": "You have been blacklisted.",
"not_whitelisted": "You have not been whitelisted.",
"offline": "The stream is offline.",
"ratelimit": "You have requested the stream recently. Try again in %.1f seconds.",
"limit": "You have made %d concurrent requests or the stream. End one of those before making a new request."
"ratelimit": "You have requested the stream recently. Try again in %.1f seconds.",
"limit": "You have made %d concurrent requests or the stream. End one of those before making a new request."
},
"internal": {
"captcha_required": "Captcha required",
@ -30,6 +36,9 @@
"click_for_a_new_captcha": "Click for a new captcha"
},
"home": {
"info": "info",
"chat": "chat",
"both": "both",
"users": "Users",
"stream_chat": "Stream chat",
"users_in_chat": "Users in chat",