Add js appearance form (not complete c.f. nojs)

このコミットが含まれているのは:
n9k 2022-03-07 05:39:06 +00:00
コミット 4cde4ea07a
8個のファイルの変更224行の追加26行の削除

ファイルの表示

@ -9,3 +9,7 @@ The canonical location of this repo is https://git.076.ne.jp/ninya9k/anonstream.
These mirrors also exist:
* https://gitlab.com/ninya9k/anonstream
* https://github.com/ninya9k/anonstream
## Credits
* [/anonstream/static/settings.svg](https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/anonstream/static/settings.svg): [setting](https://thenounproject.com/icon/setting-685325/) by [ulimicon](https://thenounproject.com/unlimicon/) is licensed under [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/).

ファイルの表示

@ -141,7 +141,7 @@ async def nojs_submit_appearance(user):
# Collect form data
name = form.get('name', '').strip()
if len(name) == 0 or name == get_default_name(user):
if len(name) == 0:
name = None
color = form.get('color', '')

ファイルの表示

@ -28,11 +28,15 @@ const jsmarkup_chat_form = `\
<textarea id="chat-form_js__comment" name="comment" maxlength="512" required placeholder="Send a message..." rows="1" autofocus></textarea>
<div id="chat-live">
<span id="chat-live__ball"></span>
<span id="chat-live__status"><span>Not connected<span data-verbose='true'> to chat</span></span></span>
<span id="chat-live__status">
<span data-verbose="true">Not connected to chat</span>
<span data-verbose="false">&times;</span>
</span>
</div>
<input id="chat-form_js__captcha-digest" type="hidden" name="captcha-digest" disabled>
<input id="chat-form_js__captcha-image" type="image" width="72" height="30">
<input id="chat-form_js__captcha-answer" name="captcha-answer" placeholder="Captcha" disabled>
<input id="chat-form_js__settings" type="image" src="/static/settings.svg" width="28" height="28" alt="Settings">
<input id="chat-form_js__submit" type="submit" value="Chat" accesskey="p" disabled>
<article id="chat-form_js__notice">
<button id="chat-form_js__notice__button" type="button">
@ -40,6 +44,17 @@ const jsmarkup_chat_form = `\
<small>Click to dismiss</small>
</button>
</article>
</form>
<form id="appearance-form_js" data-hidden="">
<span id="appearance-form_js__label-name">Name:</span>
<input id="appearance-form_js__name" name="name" maxlength="24">
<input id="appearance-form_js__color" type="color" name="color">
<span id="appearance-form_js__label-tripcode">Tripcode:</span>
<input id="appearance-form_js__password" type="password" name="password" placeholder="(tripcode password)" maxlength="1024">
<div id="appearance-form_js__row">
<article id="appearance-form_js__row__result"></article>
<input id="appearance-form_js__row__submit" type="submit" value="Update">
</div>
</form>`;
const insert_jsmarkup = () => {jsmarkup_info_float_viewership
@ -115,6 +130,30 @@ const show_notice = (text) => {
chat_form.dataset.notice = "";
}
/* override chat form settings input */
const chat_appearance_form = document.getElementById("appearance-form_js");
const chat_appearance_form_result = document.getElementById("appearance-form_js__row__result");
const chat_form_settings = document.getElementById("chat-form_js__settings");
chat_form_settings.addEventListener("click", (event) => {
event.preventDefault();
if (chat_appearance_form.dataset.hidden === undefined) {
chat_appearance_form.dataset.hidden = "";
chat_form_settings.style.backgroundColor = "";
chat_appearance_form_result.innerText = "";
if (!chat_appearance_form_submit.disabled) {
chat_appearance_form.reset();
}
} else {
chat_appearance_form.removeAttribute("data-hidden");
chat_form_settings.style.backgroundColor = "#4f4f53";
}
});
/* appearance form */
const chat_appearance_form_name = document.getElementById("appearance-form_js__name");
const chat_appearance_form_color = document.getElementById("appearance-form_js__color");
const chat_appearance_form_password = document.getElementById("appearance-form_js__password");
/* create websocket */
const info_title = document.getElementById("info_js__title");
const info_viewership = document.getElementById("info_js__float__viewership");
@ -523,6 +562,7 @@ const on_websocket_message = (event) => {
case "error":
console.log("ws error", receipt);
chat_form_submit.disabled = false;
chat_appearance_form_submit.disabled = false;
break;
case "init":
@ -576,6 +616,14 @@ const on_websocket_message = (event) => {
update_user_tripcodes();
update_users_list()
// appearance form default values
const user = users[TOKEN_HASH];
if (user.name !== null) {
chat_appearance_form_name.setAttribute("value", user.name);
}
chat_appearance_form_name.setAttribute("placeholder", default_name[user.broadcaster]);
chat_appearance_form_color.setAttribute("value", user.color);
// insert new messages
const last = chat_messages.children.length == 0 ? null : chat_messages.children[chat_messages.children.length - 1];
const last_seq = last === null ? null : parseInt(last.dataset.seq);
@ -666,6 +714,41 @@ const on_websocket_message = (event) => {
receipt.digest === null ? disable_captcha() : enable_captcha(receipt.digest);
break;
case "appearance":
console.log("ws appearance", receipt);
if (receipt.errors === undefined) {
if (receipt.name !== null) {
chat_appearance_form_name.setAttribute("value", receipt.name);
}
chat_appearance_form_color.setAttribute("value", receipt.color);
chat_appearance_form_result.innerHTML = receipt.result;
} else {
const ul = document.createElement("ul");
for (const error of receipt.errors) {
const li = document.createElement("li");
li.innerText = error[0];
for (const tuple of error.slice(1)) {
const mark = document.createElement("mark");
mark.innerText = tuple[0];
li.insertAdjacentText("beforeend", " ");
li.insertAdjacentElement("beforeend", mark);
li.insertAdjacentText("beforeend", tuple[1]);
}
ul.insertAdjacentElement("beforeend", li);
}
const result = document.createElement("div");
result.innerText = "Errors:";
result.insertAdjacentElement("beforeend", ul);
chat_appearance_form_result.innerHTML = result.innerHTML;
}
chat_appearance_form_submit.disabled = false;
chat_appearance_form.removeAttribute("data-hidden");
chat_form_settings.style.backgroundColor = "#4f4f53";
break;
default:
console.log("incomprehensible websocket message", receipt);
}
@ -680,13 +763,13 @@ const connect_websocket = () => {
return;
}
chat_live_ball.style.borderColor = "gold";
chat_live_status.innerHTML = "<span data-verbose='false'>Waiting...</span> <span data-verbose='true'>Connecting to chat...</span>";
chat_live_status.innerHTML = "<span data-verbose='true'>Connecting to chat...</span><span data-verbose='false'>&middot;&middot;&middot;</span>";
ws = new WebSocket(`ws://${document.domain}:${location.port}/live?token=${encodeURIComponent(TOKEN)}`);
ws.addEventListener("open", (event) => {
console.log("websocket open", event);
chat_form_submit.disabled = false;
chat_live_ball.style.borderColor = "green";
chat_live_status.innerHTML = "<span>Connected<span data-verbose='true'> to chat</span></span>";
chat_live_status.innerHTML = "<span><span data-verbose='true'>Connected to chat</span><span data-verbose='false'>&check;</span></span>";
// When the server is offline, a newly opened websocket can take a second
// to close. This timeout tries to ensure the backoff doesn't instantly
// (erroneously) reset to 2 seconds in that case.
@ -702,7 +785,7 @@ const connect_websocket = () => {
console.log("websocket close", event);
chat_form_submit.disabled = true;
chat_live_ball.style.borderColor = "maroon";
chat_live_status.innerHTML = "<span data-verbose='false'>Failed to connect</span> <span data-verbose='true'>Disconnected from chat</span>";
chat_live_status.innerHTML = "<span data-verbose='true'>Disconnected from chat</span><span data-verbose='false'>&times;</span>";
if (!ws.successor) {
ws.successor = true;
setTimeout(connect_websocket, websocket_backoff);
@ -745,6 +828,18 @@ chat_form.addEventListener("submit", (event) => {
ws.send(JSON.stringify(payload));
});
/* override js-only appearance form */
const chat_appearance_form_submit = document.getElementById("appearance-form_js__row__submit");
chat_appearance_form.addEventListener("submit", (event) => {
event.preventDefault();
const form = Object.fromEntries(new FormData(chat_appearance_form));
const payload = {type: "appearance", form: form};
chat_appearance_form_submit.disabled = true;
chat_appearance_form_password.value = "";
chat_appearance_form_result.innerText = "";
ws.send(JSON.stringify(payload));
});
/* when chat is being resized, peg its bottom in place (instead of its top) */
const track_scroll = (element) => {
chat_messages.dataset.scrollTop = chat_messages.scrollTop;

4
anonstream/static/settings.svg ノーマルファイル
ファイルの表示

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="243.55pt" height="243.55pt" version="1.1" viewBox="0 0 243.55 243.55" xmlns="http://www.w3.org/2000/svg">
<path d="m104.65 0-8.2461 29.598c-7.6914 2.1172-14.992 5.2305-21.777 9.0898l-27.062-15.012-23.891 23.891 15.012 27.062c-3.8594 6.7852-6.9727 14.086-9.0898 21.777l-29.598 8.2461v34.25l29.598 8.2461c2.1172 7.6914 5.2305 14.992 9.0898 21.777l-15.012 27.062 23.891 23.891 27.062-15.012c6.7852 3.8594 14.086 6.9727 21.777 9.0898l8.2461 29.598h34.25l8.2461-29.598c7.6914-2.1172 14.992-5.2305 21.777-9.0898l27.062 15.012 23.891-23.891-15.012-27.062c3.8594-6.7812 6.9727-14.086 9.0898-21.777l29.598-8.2461v-34.25l-29.598-8.2461c-2.1172-7.6914-5.2305-14.992-9.0898-21.777l15.012-27.062-23.891-23.891-27.062 15.012c-6.7852-3.8594-14.086-6.9727-21.777-9.0898l-8.2461-29.598zm17.125 74.418c26.156 0 47.359 21.203 47.359 47.359s-21.203 47.359-47.359 47.359-47.359-21.203-47.359-47.359 21.203-47.359 47.359-47.359z" fill="#bbbbbf"/>
</svg>

変更後

幅:  |  高さ:  |  サイズ: 984 B

ファイルの表示

@ -258,19 +258,19 @@ noscript {
#chat-users_nojs {
height: 100%;
}
#chat__form {
position: relative;
}
#chat-form_js {
display: grid;
grid-template-columns: 1fr min-content min-content 5rem;
grid-template-columns: 1fr min-content min-content min-content 5rem;
grid-template-rows: auto var(--button-height);
grid-gap: 0.375rem;
padding: 0 0.5rem 0.5rem 0.5rem;
position: relative;
}
#chat-form_js__submit {
grid-column: 2 / span 1;
}
#chat-form_js__comment {
grid-column: 1 / span 4;
grid-column: 1 / span 5;
background-color: #434347;
border-radius: 4px;
border: 2px solid transparent;
@ -299,9 +299,20 @@ noscript {
#chat-form_js__captcha-answer {
width: 8ch;
}
#chat-form_js__submit {
#chat-form_js__settings {
align-self: center;
padding: 5px;
box-sizing: border-box;
border-radius: 3px;
color: var(--text-color);
grid-column: 4;
}
#chat-form_js__settings:hover {
background-color: #434347;
}
#chat-form_js__submit {
grid-column: 5;
}
#chat-form_js:not([data-captcha]) > #chat-form_js__captcha-image,
#chat-form_js:not([data-captcha]) > #chat-form_js__captcha-answer {
display: none;
@ -312,9 +323,10 @@ noscript {
#chat-form_js__notice {
position: absolute;
width: 100%;
background: linear-gradient(#2323277f 25%, #232327);
background: linear-gradient(#23232700, #2323277f 8%, #232327);
height: 100%;
display: grid;
z-index: 1;
}
#chat-form_js__notice__button {
color: inherit;
@ -336,6 +348,51 @@ noscript {
#chat-form_nojs {
height: 13ch;
}
#appearance-form_js {
position: absolute;
bottom: 3rem;
padding: 0.5rem;
margin: 0 1rem;
width: calc(100% - 2rem);
box-sizing: border-box;
background: #343437df;
border: 2px outset #434347;
border-radius: 4px;
display: grid;
grid-template-columns: min-content 1fr min-content;
grid-template-rows: 1fr 1fr auto;
grid-gap: 0.375rem;
}
#appearance-form_js[data-hidden] {
display: none;
}
#appearance-form_js__label-name,
#appearance-form_js__label-tripcode {
align-self: center;
}
#appearance-form_js__name,
#appearance-form_js__password {
min-width: 12ch;
}
#appearance-form_js__row {
grid-column: 1 / span 3;
grid-row: 3;
display: grid;
grid-template-columns: auto 4rem;
align-items: end;
}
#appearance-form_js__row__result {
font-weight: bold;
font-size: 11pt;
}
#appearance-form_js__row__result > ul {
margin: 0;
padding-left: 1.125rem;
font-size: 10pt;
}
#appearance-form_js__row__submit {
min-height: 1.75rem;
}
#chat-live {
position: relative;
font-size: 9pt;

ファイルの表示

@ -4,7 +4,7 @@ from math import inf
from quart import current_app
from anonstream.wrappers import try_except_log, with_timestamp
from anonstream.helpers.user import get_presence, Presence
from anonstream.helpers.user import get_default_name, get_presence, Presence
from anonstream.helpers.captcha import check_captcha_digest, Answer
from anonstream.helpers.tripcode import generate_tripcode
from anonstream.utils.colour import color_to_colour, get_contrast, NotAColor
@ -69,6 +69,8 @@ def try_change_appearance(user, name, color, password, want_tripcode):
def change_name(user, name, dry_run=False):
if dry_run:
if name == get_default_name(user):
name = None
if name is not None:
if len(name) == 0:
raise BadAppearance('Name was empty')
@ -91,7 +93,7 @@ def change_color(user, color, dry_run=False):
if contrast < min_contrast:
raise BadAppearance(
'Colour had insufficient contrast:',
(f'{contrast:.2f}', f'/{min_contrast}'),
(f'{contrast:.2f}', f'/{min_contrast:.2f}'),
)
else:
user['color'] = color

ファイルの表示

@ -1,3 +1,7 @@
from enum import Enum
WS = Enum('WS', names=('MESSAGE, CAPTCHA, APPEARANCE'))
class Malformed(Exception):
pass
@ -19,13 +23,27 @@ def parse_websocket_data(receipt):
comment = get(str, form, 'comment')
digest = get(str, form, 'captcha-digest', '')
answer = get(str, form, 'captcha-answer', '')
return nonce, comment, digest, answer
return WS.MESSAGE, (nonce, comment, digest, answer)
case 'appearance':
raise NotImplemented
form = get(dict, receipt, 'form')
name = get(str, form, 'name').strip()
if len(name) == 0:
name = None
color = get(str, form, 'color')
password = get(str, form, 'password')
#match get(str | None, form, 'want-tripcode'):
# case '0':
# want_tripcode = False
# case '1':
# want_tripcode = True
# case _:
# want_tripcode = None
want_tripcode = bool(password)
return WS.APPEARANCE, (name, color, password, want_tripcode)
case 'captcha':
return None
return WS.CAPTCHA, ()
case _:
raise Malformed('malformed type')

ファイルの表示

@ -6,9 +6,9 @@ from quart import current_app, websocket
from anonstream.stream import get_stream_title, get_stream_uptime_and_viewership
from anonstream.captcha import get_random_captcha_digest_for
from anonstream.chat import get_all_messages_for_websocket, add_chat_message, Rejected
from anonstream.user import get_all_users_for_websocket, see, verify, deverify, BadCaptcha
from anonstream.user import get_all_users_for_websocket, see, verify, deverify, BadCaptcha, try_change_appearance
from anonstream.utils.chat import generate_nonce
from anonstream.utils.websocket import parse_websocket_data, Malformed
from anonstream.utils.websocket import parse_websocket_data, Malformed, WS
CONFIG = current_app.config
@ -41,7 +41,7 @@ async def websocket_inbound(queue, user):
finally:
see(user)
try:
parsed = parse_websocket_data(receipt)
receipt_type, parsed = parse_websocket_data(receipt)
except Malformed as e:
error , *_ = e.args
payload = {
@ -49,12 +49,14 @@ async def websocket_inbound(queue, user):
'because': error,
}
else:
match parsed:
case [nonce, comment, digest, answer]:
payload = handle_inbound_message(user, *parsed)
case None:
payload = handle_inbound_captcha(user)
match receipt_type:
case WS.MESSAGE:
handle = handle_inbound_message
case WS.APPEARANCE:
handle = handle_inbound_appearance
case WS.CAPTCHA:
handle = handle_inbound_captcha
payload = handle(user, *parsed)
queue.put_nowait(payload)
@ -64,6 +66,22 @@ def handle_inbound_captcha(user):
'digest': get_random_captcha_digest_for(user),
}
def handle_inbound_appearance(user, name, color, password, want_tripcode):
errors = try_change_appearance(user, name, color, password, want_tripcode)
if errors:
return {
'type': 'appearance',
'errors': [error.args for error in errors],
}
else:
return {
'type': 'appearance',
'result': 'Changed appearance',
'name': user['name'],
'color': user['color'],
#'tripcode': user['tripcode'],
}
def handle_inbound_message(user, nonce, comment, digest, answer):
try:
verification_happened = verify(user, digest, answer)