Merge branch 'dev'
このコミットが含まれているのは:
コミット
a97f3254bd
|
@ -5,10 +5,12 @@ 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.config import SPEC as SPEC_CONFIG
|
||||
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.tripcode import SPEC as SPEC_TRIPCODE
|
||||
from anonstream.control.spec.methods.user import SPEC as SPEC_USER
|
||||
|
||||
SPEC = Str({
|
||||
|
@ -19,6 +21,8 @@ SPEC = Str({
|
|||
'user': SPEC_USER,
|
||||
'allowednesss': SPEC_ALLOWEDNESS,
|
||||
'emote': SPEC_EMOTE,
|
||||
'config': SPEC_CONFIG,
|
||||
'tripcode': SPEC_TRIPCODE,
|
||||
})
|
||||
|
||||
async def parse(request):
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import itertools
|
||||
import json
|
||||
|
||||
from quart import current_app
|
||||
|
||||
from anonstream.chat import delete_chat_messages
|
||||
from anonstream.control.exceptions import CommandFailed
|
||||
|
@ -10,6 +13,8 @@ from anonstream.control.spec.common import Str, End, Args, ArgsJsonString, ArgsU
|
|||
from anonstream.control.spec.utils import get_item, json_dumps_contiguous
|
||||
from anonstream.chat import add_chat_message, Rejected
|
||||
|
||||
MESSAGES = current_app.messages
|
||||
|
||||
class ArgsSeqs(Args):
|
||||
def consume(self, words, index):
|
||||
seqs = []
|
||||
|
@ -33,13 +38,12 @@ class ArgsSeqs(Args):
|
|||
async def cmd_chat_help():
|
||||
normal = ['chat', 'help']
|
||||
response = (
|
||||
'Usage: chat delete SEQS\n'
|
||||
'Usage: chat {show | delete SEQS | add USER NONCE COMMENT}\n'
|
||||
'Commands:\n'
|
||||
#' chat show [MESSAGES]...........show chat messages\n'
|
||||
' chat show......................show all chat messages\n'
|
||||
' chat delete SEQS...............delete chat messages\n'
|
||||
' chat add USER NONCE COMMENT....add chat message\n'
|
||||
'Definitions:\n'
|
||||
#' MESSAGES...................undefined\n'
|
||||
' SEQS.......................=SEQ [SEQ...]\n'
|
||||
' SEQ........................a chat message\'s seq, base-10 integer\n'
|
||||
' USER.......................={token TOKEN | hash HASH}\n'
|
||||
|
@ -50,6 +54,11 @@ async def cmd_chat_help():
|
|||
)
|
||||
return normal, response
|
||||
|
||||
async def cmd_chat_show():
|
||||
normal = ['chat', 'show']
|
||||
response = json.dumps(tuple(MESSAGES), separators=(',', ':')) + '\n'
|
||||
return normal, response
|
||||
|
||||
async def cmd_chat_delete(*seqs):
|
||||
delete_chat_messages(seqs)
|
||||
normal = ['chat', 'delete', *map(str, seqs)]
|
||||
|
@ -74,6 +83,7 @@ async def cmd_chat_add(user, nonce, comment):
|
|||
SPEC = Str({
|
||||
None: End(cmd_chat_help),
|
||||
'help': End(cmd_chat_help),
|
||||
'show': End(cmd_chat_show),
|
||||
'delete': ArgsSeqs(End(cmd_chat_delete)),
|
||||
'add': ArgsUser(ArgsJsonString(ArgsJsonString(End(cmd_chat_add)))),
|
||||
})
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import json
|
||||
|
||||
from quart import current_app
|
||||
|
||||
from anonstream.control.exceptions import CommandFailed
|
||||
from anonstream.control.spec.common import Str, End, ArgsString
|
||||
|
||||
CONFIG = current_app.config
|
||||
|
||||
async def cmd_config_help():
|
||||
normal = ['config', 'help']
|
||||
response = (
|
||||
'Usage: config show OPTION\n'
|
||||
'Commands:\n'
|
||||
' config show OPTION....show entry in app.config\n'
|
||||
'Definitions:\n'
|
||||
' OPTION................app.config key, re:[A-Z0-9_]+\n'
|
||||
)
|
||||
return normal, response
|
||||
|
||||
async def cmd_config_show(option):
|
||||
if option in {'SECRET_KEY', 'SECRET_KEY_STRING'}:
|
||||
raise CommandFailed('not going to show our secret key')
|
||||
try:
|
||||
value = CONFIG[option]
|
||||
except KeyError:
|
||||
raise CommandFailed(f'no config option with key {option!r}')
|
||||
try:
|
||||
value_json = json.dumps(value)
|
||||
except (TypeError, ValueError):
|
||||
raise CommandFailed(f'value is not json serializable')
|
||||
normal = ['config', 'show']
|
||||
response = value_json + '\n'
|
||||
return normal, response
|
||||
|
||||
SPEC = Str({
|
||||
None: End(cmd_config_help),
|
||||
'help': End(cmd_config_help),
|
||||
'show': ArgsString(End(cmd_config_show)),
|
||||
})
|
|
@ -16,7 +16,7 @@ EMOTES = current_app.emotes
|
|||
async def cmd_emote_help():
|
||||
normal = ['emote', 'help']
|
||||
response = (
|
||||
'Usage: emote [show | reload]\n'
|
||||
'Usage: emote {show | reload}\n'
|
||||
'Commands:\n'
|
||||
' emote show........show all current emotes\n'
|
||||
' emote reload......try to reload the emote schema (existing messages are not modified)\n'
|
||||
|
|
|
@ -19,15 +19,17 @@ async def cmd_help():
|
|||
' user eyes USER [show]..........show a list of active video responses\n'
|
||||
' user eyes USER delete EYES_ID..end a video response\n'
|
||||
' user add VERIFIED TOKEN........add new user\n'
|
||||
#' chat show MESSAGES.............show a list of messages\n'
|
||||
' chat delete SEQS...............delete a set of messages\n'
|
||||
' chat add USER NONCE COMMENT....add chat message\n'
|
||||
' chat show......................show a list of all chat messages\n'
|
||||
' chat delete SEQS...............delete a set of chat messages\n'
|
||||
' chat add USER NONCE COMMENT....add a chat message\n'
|
||||
' allowedness [show].............show the current allowedness\n'
|
||||
' 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 show.....................show all current emotes\n'
|
||||
' emote reload...................try reloading the emote schema\n'
|
||||
' config show OPTION.............show app config option\n'
|
||||
' tripcode generate PASSWORD.....show tripcode for given password\n'
|
||||
)
|
||||
return normal, response
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ from anonstream.stream import get_stream_title, set_stream_title
|
|||
async def cmd_title_help():
|
||||
normal = ['title', 'help']
|
||||
response = (
|
||||
'Usage: title [show | set TITLE]\n'
|
||||
'Usage: title {show | set TITLE}\n'
|
||||
'Commands:\n'
|
||||
' title [show].......show the stream title\n'
|
||||
' title set TITLE....set the stream title to TITLE\n'
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import json
|
||||
|
||||
from quart import current_app
|
||||
|
||||
from anonstream.control.exceptions import CommandFailed
|
||||
from anonstream.control.spec.common import Str, End, ArgsJsonString
|
||||
from anonstream.control.spec.utils import json_dumps_contiguous
|
||||
from anonstream.helpers.tripcode import generate_tripcode
|
||||
|
||||
CONFIG = current_app.config
|
||||
|
||||
async def cmd_tripcode_help():
|
||||
normal = ['tripcode', 'help']
|
||||
response = (
|
||||
'Usage: tripcode generate PASSWORD\n'
|
||||
'Commands:\n'
|
||||
' tripcode generate PASSWORD....show tripcode for given password\n'
|
||||
'Definitions:\n'
|
||||
' PASSWORD................json string, max length in config.toml (`chat.max_tripcode_password_length`)\n'
|
||||
)
|
||||
return normal, response
|
||||
|
||||
async def cmd_tripcode_generate(password):
|
||||
if len(password) > CONFIG['CHAT_TRIPCODE_PASSWORD_MAX_LENGTH']:
|
||||
raise CommandFailed(
|
||||
f'password exceeded maximum configured length of '
|
||||
f'{CONFIG["CHAT_TRIPCODE_PASSWORD_MAX_LENGTH"]} '
|
||||
f'characters'
|
||||
)
|
||||
tripcode = generate_tripcode(password)
|
||||
normal = ['tripcode', 'generate', json_dumps_contiguous(password)]
|
||||
response = json.dumps(tripcode) + '\n'
|
||||
return normal, response
|
||||
|
||||
SPEC = Str({
|
||||
None: End(cmd_tripcode_help),
|
||||
'help': End(cmd_tripcode_help),
|
||||
'generate': ArgsJsonString(End(cmd_tripcode_generate)),
|
||||
})
|
|
@ -58,10 +58,8 @@ async def cmd_user_get(user, attr):
|
|||
except (TypeError, ValueError) as e:
|
||||
raise CommandFailed('value is not representable in json') from e
|
||||
normal = [
|
||||
'user',
|
||||
'get',
|
||||
'token',
|
||||
json_dumps_contiguous(user['token']),
|
||||
'user', 'get',
|
||||
'token', json_dumps_contiguous(user['token']),
|
||||
attr,
|
||||
]
|
||||
response = value_json + '\n'
|
||||
|
@ -74,10 +72,8 @@ async def cmd_user_set(user, attr, value):
|
|||
if attr in USER_WEBSOCKET_ATTRS:
|
||||
USERS_UPDATE_BUFFER.add(user['token'])
|
||||
normal = [
|
||||
'user',
|
||||
'set',
|
||||
'token',
|
||||
json_dumps_contiguous(user['token']),
|
||||
'user', 'set',
|
||||
'token', json_dumps_contiguous(user['token']),
|
||||
attr,
|
||||
json_dumps_contiguous(value),
|
||||
]
|
||||
|
@ -86,11 +82,9 @@ async def cmd_user_set(user, attr, value):
|
|||
|
||||
async def cmd_user_eyes_show(user):
|
||||
normal = [
|
||||
'user',
|
||||
'eyes',
|
||||
'token',
|
||||
json_dumps_contiguous(user['token']),
|
||||
'show'
|
||||
'user', 'eyes',
|
||||
'token', json_dumps_contiguous(user['token']),
|
||||
'show',
|
||||
]
|
||||
response = json.dumps(user['eyes']['current']) + '\n'
|
||||
return normal, response
|
||||
|
@ -101,12 +95,9 @@ async def cmd_user_eyes_delete(user, eyes_id):
|
|||
except KeyError:
|
||||
pass
|
||||
normal = [
|
||||
'user',
|
||||
'eyes',
|
||||
'token',
|
||||
json_dumps_contiguous(user['token']),
|
||||
'delete',
|
||||
str(eyes_id),
|
||||
'user', 'eyes',
|
||||
'token', json_dumps_contiguous(user['token']),
|
||||
'delete', str(eyes_id),
|
||||
]
|
||||
response = ''
|
||||
return normal, response
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import math
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
|
||||
from quart import current_app, request, render_template, abort, make_response, redirect, url_for, send_from_directory
|
||||
from werkzeug.exceptions import Forbidden, NotFound, TooManyRequests
|
||||
|
@ -22,7 +23,12 @@ CAPTCHA_SIGNER = current_app.captcha_signer
|
|||
STATIC_DIRECTORY = current_app.root_path / 'static'
|
||||
|
||||
@current_app.route('/')
|
||||
@with_user_from(request, fallback_to_token=True, ignore_allowedness=True)
|
||||
@with_user_from(
|
||||
request,
|
||||
fallback_to_token=True,
|
||||
ignore_allowedness=True,
|
||||
redundant_token_redirect=True,
|
||||
)
|
||||
async def home(timestamp, user_or_token):
|
||||
match user_or_token:
|
||||
case str() | None as token:
|
||||
|
@ -128,12 +134,13 @@ async def access(timestamp, user_or_token):
|
|||
failure_id = None
|
||||
user = generate_and_add_user(timestamp, token, verified=True)
|
||||
if failure_id is not None:
|
||||
url = url_for('home', token=token, failure=failure_id)
|
||||
raise abort(redirect(url, 303))
|
||||
response = redirect(url_for('home', token=token, failure=failure_id), 303)
|
||||
else:
|
||||
response = redirect(url_for('home', token=user['token']), 303)
|
||||
response.headers['Set-Cookie'] = f'token={quote(user["token"])}; path=/'
|
||||
case dict() as user:
|
||||
pass
|
||||
url = url_for('home', token=user['token'])
|
||||
return redirect(url, 303)
|
||||
response = redirect(url_for('home', token=user['token']), 303)
|
||||
return response
|
||||
|
||||
@current_app.route('/static/<path:filename>')
|
||||
@with_user_from(request)
|
||||
|
|
|
@ -8,7 +8,7 @@ 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, redirect, url_for, Markup
|
||||
from werkzeug.exceptions import BadRequest, Unauthorized, Forbidden
|
||||
from werkzeug.security import check_password_hash
|
||||
|
||||
|
@ -87,7 +87,12 @@ def generate_and_add_user(
|
|||
USERS_UPDATE_BUFFER.add(token)
|
||||
return user
|
||||
|
||||
def with_user_from(context, fallback_to_token=False, ignore_allowedness=False):
|
||||
def with_user_from(
|
||||
context,
|
||||
fallback_to_token=False,
|
||||
ignore_allowedness=False,
|
||||
redundant_token_redirect=False,
|
||||
):
|
||||
def with_user_from_context(f):
|
||||
@wraps(f)
|
||||
async def wrapper(*args, **kwargs):
|
||||
|
@ -129,6 +134,18 @@ def with_user_from(context, fallback_to_token=False, ignore_allowedness=False):
|
|||
f"terminal when they started anonstream."
|
||||
))
|
||||
|
||||
# If token from the client's cookie is same as the token in the URL
|
||||
# query string, the client supports cookies. If we want, we can
|
||||
# redirect the client to this same URL path but with the token
|
||||
# parameter removed, since we'll pick up their token from their
|
||||
# cookie anyway.
|
||||
if (
|
||||
redundant_token_redirect
|
||||
and token_from_context is not None
|
||||
and token_from_args == token_from_cookie
|
||||
):
|
||||
return redirect(context.path, 303)
|
||||
|
||||
# Create response
|
||||
user = USERS_BY_TOKEN.get(token)
|
||||
if CONFIG['ACCESS_CAPTCHA'] and not broadcaster:
|
||||
|
|
|
@ -665,6 +665,11 @@ const on_websocket_message = async (event) => {
|
|||
info_button.dataset.visible = "";
|
||||
}
|
||||
|
||||
// form input maxlengths
|
||||
chat_form_comment.maxLength = receipt.maxlength.comment;
|
||||
chat_appearance_form_name.maxLength = receipt.maxlength.name;
|
||||
chat_appearance_form_password.maxLength = receipt.maxlength.password;
|
||||
|
||||
// chat form nonce
|
||||
chat_form_nonce.value = receipt.nonce;
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
font-family: sans-serif;
|
||||
font-size: 14pt;
|
||||
display: grid;
|
||||
grid-template-rows: calc(50% - 4rem) 1fr;
|
||||
grid-template-rows: calc(50% - 10vh + 2rem) 1fr;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
|
@ -37,20 +37,23 @@
|
|||
border-radius: 2px;
|
||||
color: #ddd;
|
||||
font-size: 14pt;
|
||||
padding: 4px 5px;
|
||||
padding: 5px 6px;
|
||||
width: 10ch;
|
||||
}
|
||||
input[name="answer"]:hover {
|
||||
background-color: #37373a;
|
||||
transition: 0.25s;
|
||||
}
|
||||
input[type="submit"] {
|
||||
}
|
||||
input[name="answer"]:focus {
|
||||
background-color: black;
|
||||
border-color: #3584e4;
|
||||
}
|
||||
input[type="submit"] {
|
||||
font-size: 14pt;
|
||||
}
|
||||
p {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
p {
|
||||
grid-column: 1 / span 2;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -8,6 +8,7 @@ from math import inf
|
|||
|
||||
from quart import current_app
|
||||
|
||||
from anonstream.events import notify_event_sockets
|
||||
from anonstream.wrappers import try_except_log, with_timestamp, get_timestamp
|
||||
from anonstream.helpers.user import get_default_name, get_presence, Presence
|
||||
from anonstream.helpers.captcha import check_captcha_digest, Answer
|
||||
|
@ -98,6 +99,21 @@ def try_change_appearance(user, name, color, password, want_tripcode):
|
|||
# Add to the users update buffer
|
||||
USERS_UPDATE_BUFFER.add(user['token'])
|
||||
|
||||
# Notify event sockets that a user's appearance was set
|
||||
# NOTE: Changing appearance is currently NOT ratelimited.
|
||||
# Applications using the event socket API should buffer these
|
||||
# events or do something else to a prevent a potential denial of
|
||||
# service.
|
||||
notify_event_sockets({
|
||||
'type': 'appearance',
|
||||
'event': {
|
||||
'token': user['token'],
|
||||
'name': user['name'],
|
||||
'color': user['color'],
|
||||
'tripcode': user['tripcode'],
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
|
||||
def change_name(user, name, dry_run=False):
|
||||
|
|
|
@ -36,6 +36,11 @@ async def websocket_outbound(queue, user):
|
|||
'scrollback': CONFIG['MAX_CHAT_SCROLLBACK'],
|
||||
'digest': get_random_captcha_digest_for(user),
|
||||
'pingpong': CONFIG['TASK_BROADCAST_PING'],
|
||||
'maxlength': {
|
||||
'comment': CONFIG['CHAT_COMMENT_MAX_LENGTH'],
|
||||
'name': CONFIG['CHAT_NAME_MAX_LENGTH'],
|
||||
'password': CONFIG['CHAT_TRIPCODE_PASSWORD_MAX_LENGTH'],
|
||||
},
|
||||
})
|
||||
while True:
|
||||
payload = await queue.get()
|
||||
|
|
読み込み中…
新しいイシューから参照