コミットを比較

...

5 コミット

作成者 SHA1 メッセージ 日付
n9k a93135e522 Nojs chat form: use `:checked` instead of `:target`
This works around a bug in mobile Firefox where under certain cirucmstances two
elements inside an iframe both become the iframe's target elment at the same
time, which breaks the CSS logic so instead of exactly one form being displayed,
nothing is displayed.
2022-03-06 00:09:38 +13:00
n9k e91a30a14d Keyboard accessible js captcha 2022-03-05 23:32:31 +13:00
n9k b6e2715077 Autofocus chat form textarea 2022-03-05 23:23:51 +13:00
n9k 3246fbe198 Show notice from websocket in js chat form 2022-03-05 23:21:02 +13:00
n9k c0ba5eaff9 Add config option for old tripcode algorithm 2022-03-05 22:36:57 +13:00
6個のファイルの変更111行の追加46行の削除

ファイルの表示

@ -25,6 +25,7 @@ def create_app(config_file):
'lstrip_blocks': True,
})
app.config.update({
'SECRET_KEY_STRING': config['secret_key'],
'SECRET_KEY': config['secret_key'].encode(),
'AUTH_USERNAME': config['auth']['username'],
'AUTH_PWHASH': auth_pwhash,
@ -56,6 +57,7 @@ def create_app(config_file):
'CHAT_NAME_MAX_LENGTH': config['chat']['max_name_length'],
'CHAT_NAME_MIN_CONTRAST': config['chat']['min_name_contrast'],
'CHAT_BACKGROUND_COLOUR': color_to_colour(config['chat']['background_color']),
'CHAT_LEGACY_TRIPCODE_ALGORITHM': config['chat']['legacy_tripcode_algorithm'],
'FLOOD_DURATION': config['flood']['duration'],
'FLOOD_THRESHOLD': config['flood']['threshold'],
'CAPTCHA_LIFETIME': config['captcha']['lifetime'],

ファイルの表示

@ -11,19 +11,27 @@ CONFIG = current_app.config
def _generate_tripcode_digest_legacy(password):
hexdigest, _ = werkzeug.security._hash_internal(
'pbkdf2:sha256:150000',
CONFIG['SECRET_KEY'],
CONFIG['SECRET_KEY_STRING'],
password,
)
digest = bytes.fromhex(hexdigest)
return base64.b64encode(digest)[:8].decode()
def generate_tripcode_digest(password):
def _generate_tripcode_digest(password):
parts = CONFIG['SECRET_KEY'] + b'tripcode\0' + password.encode()
digest = hashlib.sha256(parts).digest()
return base64.b64encode(digest)[:8].decode()
def generate_tripcode(password, generate_digest=generate_tripcode_digest):
digest = generate_digest(password)
def generate_tripcode_digest(password):
algorithm = (
_generate_tripcode_digest_legacy
if CONFIG['CHAT_LEGACY_TRIPCODE_ALGORITHM'] else
_generate_tripcode_digest
)
return algorithm(password)
def generate_tripcode(password):
digest = generate_tripcode_digest(password)
background_colour = generate_colour(
seed='tripcode-background\0' + digest,
bg=CONFIG['CHAT_BACKGROUND_COLOUR'],

ファイルの表示

@ -25,15 +25,21 @@ const jsmarkup_chat_users = `\
const jsmarkup_chat_form = `\
<form id="chat-form_js" data-js="true" action="/chat" method="post">
<input id="chat-form_js__nonce" type="hidden" name="nonce" value="">
<textarea id="chat-form_js__comment" name="comment" maxlength="512" required placeholder="Send a message..." rows="1"></textarea>
<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>
</div>
<input id="chat-form_js__captcha-digest" type="hidden" name="captcha-digest" disabled>
<img id="chat-form_js__captcha-image" width="72" height="30">
<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__submit" type="submit" value="Chat" accesskey="p" disabled>
<article id="chat-form_js__notice">
<button id="chat-form_js__notice__button" type="button">
<header id="chat-form_js__notice__button__header"></header>
<small>Click to dismiss</small>
</button>
</article>
</form>`;
const insert_jsmarkup = () => {jsmarkup_info_float_viewership
@ -96,6 +102,19 @@ const stylesheet_color = document.styleSheets[1];
const stylesheet_tripcode_display = document.styleSheets[2];
const stylesheet_tripcode_colors = document.styleSheets[3];
/* override chat form notice button */
const chat_form = document.getElementById("chat-form_js");
const chat_form_notice_button = document.getElementById("chat-form_js__notice__button");
const chat_form_notice_header = document.getElementById("chat-form_js__notice__button__header");
chat_form_notice_button.addEventListener("click", (event) => {
chat_form.removeAttribute("data-notice");
chat_form_notice_header.innerText = "";
});
const show_notice = (text) => {
chat_form_notice_header.innerText = text;
chat_form.dataset.notice = "";
}
/* create websocket */
const info_title = document.getElementById("info_js__title");
const info_viewership = document.getElementById("info_js__float__viewership");
@ -360,16 +379,16 @@ chat_form_captcha_image.addEventListener("error", (event) => {
chat_form_captcha_image.title = "Click for a new captcha";
});
chat_form_captcha_image.addEventListener("click", (event) => {
if (chat_form_captcha_image.dataset.reloadable === undefined) {
return;
event.preventDefault();
if (chat_form_captcha_image.dataset.reloadable !== undefined) {
chat_form_submit.disabled = true;
chat_form_captcha_image.alt = "Waiting...";
chat_form_captcha_image.removeAttribute("title");
chat_form_captcha_image.removeAttribute("data-reloadable");
chat_form_captcha_image.removeAttribute("src");
const payload = {type: "captcha"};
ws.send(JSON.stringify(payload));
}
chat_form_submit.disabled = true;
chat_form_captcha_image.alt = "Waiting...";
chat_form_captcha_image.removeAttribute("title");
chat_form_captcha_image.removeAttribute("data-reloadable");
chat_form_captcha_image.removeAttribute("src");
const payload = {type: "captcha"};
ws.send(JSON.stringify(payload));
});
const enable_captcha = (digest) => {
chat_form_captcha_digest.value = digest;
@ -594,6 +613,11 @@ const on_websocket_message = (event) => {
case "ack":
console.log("ws ack", receipt);
if (receipt.notice !== null) {
show_notice(receipt.notice);
}
const existing_nonce = chat_form_nonce.value;
if (receipt.clear && receipt.nonce === existing_nonce) {
chat_form_comment.value = "";
@ -603,6 +627,7 @@ const on_websocket_message = (event) => {
chat_form_nonce.value = receipt.next;
receipt.digest === null ? disable_captcha() : enable_captcha(receipt.digest);
chat_form_submit.disabled = false;
break;
case "message":
@ -709,7 +734,6 @@ stream.addEventListener("error", (event) => {
});
/* override js-only chat form */
const chat_form = document.getElementById("chat-form_js");
const chat_form_nonce = document.getElementById("chat-form_js__nonce");
const chat_form_comment = document.getElementById("chat-form_js__comment");
const chat_form_submit = document.getElementById("chat-form_js__submit");

ファイルの表示

@ -107,9 +107,7 @@ noscript {
#chat__toggle {
opacity: 0;
position: absolute;
top: calc(0.5rem + 1px);
left: calc(0.5rem + 4px);
margin: 0;
pointer-events: none;
}
#chat__toggle:checked ~ #chat__body > #chat__body__messages,
#chat__toggle:not(:checked) ~ #chat__body > #chat__body__users {
@ -263,7 +261,8 @@ noscript {
grid-template-columns: 1fr min-content min-content 5rem;
grid-template-rows: auto var(--button-height);
grid-gap: 0.375rem;
margin: 0 0.5rem 0.5rem 0.5rem;
padding: 0 0.5rem 0.5rem 0.5rem;
position: relative;
}
#chat-form_js__submit {
grid-column: 2 / span 1;
@ -305,6 +304,33 @@ noscript {
#chat-form_js:not([data-captcha]) > #chat-form_js__captcha-answer {
display: none;
}
#chat-form_js:not([data-notice]) > #chat-form_js__notice {
display: none;
}
#chat-form_js__notice {
position: absolute;
width: 100%;
background: linear-gradient(#2323277f 25%, #232327);
height: 100%;
display: grid;
}
#chat-form_js__notice__button {
color: inherit;
border-color: black;
background-color: #232327;
border-radius: 4px;
text-align: center;
margin: auto;
display: grid;
grid-gap: 0.375rem;
padding: 0.625rem 1.25rem;
box-shadow: 0 0 12px black;
cursor: pointer;
}
#chat-form_js__notice__button__header {
font-size: 14pt;
line-height: 1.5;
}
#chat-form_nojs {
height: 13ch;
}

ファイルの表示

@ -56,6 +56,7 @@
height: 100%;
box-sizing: border-box;
align-items: center;
cursor: pointer;
}
#notice h2 {
margin: 0;
@ -73,7 +74,7 @@
grid-gap: 0.375rem;
}
#chat-form__exit,
#appearance-form__exit,
#appearance-form__buttons__exit,
#appearance-form__label-name,
#appearance-form__label-password {
font-size: 11pt;
@ -120,6 +121,7 @@
}
#appearance-form {
display: grid;
grid-auto-rows: 1fr 1fr 2rem;
grid-auto-columns: min-content 1fr min-content;
}
@ -172,46 +174,48 @@
display: none;
}
#appearance-form {
#toggle {
opacity: 0;
position: absolute;
pointer-events: none;
}
#chat-form__exit > label,
#appearance-form__buttons__exit > label {
padding: 1px;
}
#toggle:focus-visible ~ #chat-form > #chat-form__exit > label,
#toggle:focus-visible ~ #appearance-form #appearance-form__buttons__exit > label {
padding: 0;
border: 1px dotted;
}
#notice-radio {
display: none;
}
#appearance:target ~ #appearance-form {
display: grid;
}
#appearance:target ~ #chat-form {
#toggle:checked ~ #chat-form,
#toggle:not(:checked) ~ #appearance-form {
display: none;
}
#chat:target ~ #appearance-form {
#notice-radio:checked + #notice,
#notice-radio:not(:checked) ~ #chat-form,
#notice-radio:not(:checked) ~ #appearance-form {
display: none;
}
{% if state.notice %}
#chat-form {
display: none;
}
#chat:target ~ #chat-form {
display: grid;
}
#chat:target ~ #notice,
#appearance:target ~ #notice {
display: none;
}
{% endif %}
</style>
</head>
<body>
<div id="chat"></div>
<div id="appearance"></div>
<input id="toggle" type="checkbox"{% if not prefer_chat_form %} checked{% endif %}>
{% if state.notice %}
<a id="notice" {% if state.verbose %}class="verbose" {% endif %}{% if prefer_chat_form %}href="#chat"{% else %}href="#appearance"{% endif %}>
<input id="notice-radio" type="radio">
<label id="notice" for="notice-radio" {% if state.verbose %}class="verbose" {% endif %}>
<header><h2>{{ state.notice }}</h2></header>
<small>Click to dismiss</small>
</a>
</label>
{% endif %}
<form id="chat-form" action="{{ url_for('nojs_submit_message', token=user.token) }}" method="post">
<input type="hidden" name="nonce" value="{{ nonce }}">
<textarea id="chat-form__comment" name="comment" maxlength="512" {% if digest is none %}required {% endif %} placeholder="Send a message..." rows="1" tabindex="1">{{ state.comment }}</textarea>
<textarea id="chat-form__comment" name="comment" maxlength="512" {% if digest is none %}required {% endif %} placeholder="Send a message..." rows="1" tabindex="1" autofocus>{{ state.comment }}</textarea>
<input id="chat-form__submit" type="submit" value="Chat" tabindex="4" accesskey="p">
<div id="chat-form__exit"><a href="#appearance">Settings</a></div>
<div id="chat-form__exit"><label for="toggle" class="pseudolink">Settings</label></div>
{% if digest %}
<input type="hidden" name="captcha-digest" value="{{ digest }}">
<input id="chat-form__captcha-image" type="image" formaction="{{ url_for('nojs_chat_form_redirect', token=user.token) }}" formnovalidate src="{{ url_for('captcha', token=user.token, digest=digest) }}" width="72" height="30" alt="Captcha failed to load" title="Click for a new captcha" tabindex="2">
@ -239,7 +243,7 @@
<input id="appearance-form__password" name="password" type="password" placeholder="(tripcode password)" maxlength="1024">
<div id="hide-password"><label for="password-toggle" class="pseudolink x">&cross;</label></div>
<div id="appearance-form__buttons">
<div id="appearance-form__exit"><a href="#chat">Return to chat</a></div>
<div id="appearance-form__buttons__exit"><label for="toggle" class="pseudolink">Return to chat</label></div>
<input type="submit" value="Update">
</div>
</form>

ファイルの表示

@ -45,6 +45,7 @@ max_comment_length = 512
max_name_length = 24
min_name_contrast = 3.0
background_color = "#232327"
legacy_tripcode_algorithm = false
[flood]
duration = 20.0