コミットを比較
10 コミット
1858848f85
...
2b07354e5c
作成者 | SHA1 | 日付 |
---|---|---|
n9k | 2b07354e5c | |
n9k | 9b13526f7c | |
n9k | 60021851c6 | |
n9k | 50578e2eb3 | |
n9k | 984ae8ca5f | |
n9k | c904c51d03 | |
n9k | 30d95a0c7d | |
n9k | 48afc6d6fa | |
n9k | a0fd481502 | |
n9k | bb2bc44629 |
|
@ -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/).
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -5,11 +5,16 @@ from anonstream.segments import segments
|
|||
from anonstream.stream import is_online, get_stream_uptime
|
||||
from anonstream.user import watched
|
||||
from anonstream.routes.wrappers import with_user_from, auth_required
|
||||
from anonstream.utils.security import generate_csp
|
||||
|
||||
@current_app.route('/')
|
||||
@with_user_from(request)
|
||||
async def home(user):
|
||||
return await render_template('home.html', user=user)
|
||||
return await render_template(
|
||||
'home.html',
|
||||
csp=generate_csp(),
|
||||
user=user,
|
||||
)
|
||||
|
||||
@current_app.route('/stream.mp4')
|
||||
@with_user_from(request)
|
||||
|
|
|
@ -8,6 +8,7 @@ from anonstream.routes.wrappers import with_user_from, render_template_with_etag
|
|||
from anonstream.helpers.chat import get_scrollback
|
||||
from anonstream.helpers.user import get_default_name
|
||||
from anonstream.utils.chat import generate_nonce
|
||||
from anonstream.utils.security import generate_csp
|
||||
from anonstream.utils.user import concatenate_for_notice
|
||||
|
||||
CONFIG = current_app.config
|
||||
|
@ -18,6 +19,7 @@ USERS_BY_TOKEN = current_app.users_by_token
|
|||
async def nojs_stream(user):
|
||||
return await render_template(
|
||||
'nojs_stream.html',
|
||||
csp=generate_csp(),
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
@ -28,6 +30,7 @@ async def nojs_info(user):
|
|||
uptime, viewership = get_stream_uptime_and_viewership()
|
||||
return await render_template(
|
||||
'nojs_info.html',
|
||||
csp=generate_csp(),
|
||||
user=user,
|
||||
viewership=viewership,
|
||||
uptime=uptime,
|
||||
|
@ -40,6 +43,7 @@ async def nojs_info(user):
|
|||
async def nojs_chat_messages(user):
|
||||
return await render_template_with_etag(
|
||||
'nojs_chat_messages.html',
|
||||
{'csp': generate_csp()},
|
||||
user=user,
|
||||
users_by_token=USERS_BY_TOKEN,
|
||||
messages=get_scrollback(current_app.messages),
|
||||
|
@ -58,6 +62,7 @@ async def nojs_chat_users(user):
|
|||
users_by_presence = get_users_by_presence()
|
||||
return await render_template_with_etag(
|
||||
'nojs_chat_users.html',
|
||||
{'csp': generate_csp()},
|
||||
user=user,
|
||||
get_default_name=get_default_name,
|
||||
users_watching=users_by_presence[Presence.WATCHING],
|
||||
|
@ -73,6 +78,7 @@ async def nojs_chat_form(user):
|
|||
prefer_chat_form = request.args.get('landing') != 'appearance'
|
||||
return await render_template(
|
||||
'nojs_chat_form.html',
|
||||
csp=generate_csp(),
|
||||
user=user,
|
||||
state=state,
|
||||
prefer_chat_form=prefer_chat_form,
|
||||
|
@ -141,7 +147,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', '')
|
||||
|
|
|
@ -86,11 +86,16 @@ def with_user_from(context):
|
|||
|
||||
return with_user_from_context
|
||||
|
||||
async def render_template_with_etag(*args, **kwargs):
|
||||
rendered_template = await render_template(*args, **kwargs)
|
||||
tag = hashlib.sha256(rendered_template.encode()).hexdigest()
|
||||
async def render_template_with_etag(template, deferred_kwargs, **kwargs):
|
||||
render = await render_template(template, **kwargs)
|
||||
tag = hashlib.sha256(render.encode()).hexdigest()
|
||||
etag = f'W/"{tag}"'
|
||||
if request.if_none_match.contains_weak(tag):
|
||||
return '', 304, {'ETag': etag}
|
||||
else:
|
||||
rendered_template = await render_template(
|
||||
template,
|
||||
**deferred_kwargs,
|
||||
**kwargs,
|
||||
)
|
||||
return rendered_template, {'ETag': etag}
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
const TOKEN = document.body.dataset.token;
|
||||
const TOKEN_HASH = document.body.dataset.tokenHash;
|
||||
|
||||
/* Content Security Policy nonce */
|
||||
const CSP = document.body.dataset.csp;
|
||||
|
||||
/* insert js-only markup */
|
||||
const jsmarkup_style_color = '<style id="style-color"></style>'
|
||||
const jsmarkup_style_tripcode_display = '<style id="style-tripcode-display"></style>'
|
||||
const jsmarkup_style_tripcode_colors = '<style id="style-tripcode-colors"></style>'
|
||||
const jsmarkup_stream = `<video id="stream_js" src="/stream.mp4?token=${encodeURIComponent(TOKEN)}" autoplay controls></video>`
|
||||
const jsmarkup_info = '<div id="info_js" data-js="true"></div>';
|
||||
const jsmarkup_info_float = '<aside id="info_js__float"></aside>';
|
||||
|
@ -25,29 +25,56 @@ 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>
|
||||
<span id="chat-live__status">
|
||||
<span data-verbose="true">Not connected to chat</span>
|
||||
<span data-verbose="false">×</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__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">
|
||||
<header id="chat-form_js__notice__button__header"></header>
|
||||
<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
|
||||
const insert_jsmarkup = () => {
|
||||
if (document.getElementById("style-color") === null) {
|
||||
const parent = document.head;
|
||||
parent.insertAdjacentHTML("beforeend", jsmarkup_style_color);
|
||||
const style_color = document.createElement("style");
|
||||
style_color.id = "style-color";
|
||||
style_color.nonce = CSP;
|
||||
document.head.insertAdjacentElement("beforeend", style_color);
|
||||
}
|
||||
if (document.getElementById("style-tripcode-display") === null) {
|
||||
const parent = document.head;
|
||||
parent.insertAdjacentHTML("beforeend", jsmarkup_style_tripcode_display);
|
||||
const style_tripcode_display = document.createElement("style");
|
||||
style_tripcode_display.id = "style-tripcode-display";
|
||||
style_tripcode_display.nonce = CSP;
|
||||
document.head.insertAdjacentElement("beforeend", style_tripcode_display);
|
||||
}
|
||||
if (document.getElementById("style-tripcode-colors") === null) {
|
||||
const parent = document.head;
|
||||
parent.insertAdjacentHTML("beforeend", jsmarkup_style_tripcode_colors);
|
||||
const style_tripcode_colors = document.createElement("style");
|
||||
style_tripcode_colors.id = "style-tripcode-colors";
|
||||
style_tripcode_colors.nonce = CSP;
|
||||
document.head.insertAdjacentElement("beforeend", style_tripcode_colors);
|
||||
}
|
||||
if (document.getElementById("stream_js") === null) {
|
||||
const parent = document.getElementById("stream");
|
||||
|
@ -96,6 +123,43 @@ 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 = "";
|
||||
}
|
||||
|
||||
/* 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");
|
||||
|
@ -120,11 +184,7 @@ const create_chat_message = (object) => {
|
|||
chat_message_time.title = `${object.date} ${object.time_seconds}`;
|
||||
chat_message_time.innerText = object.time_minutes;
|
||||
|
||||
const [
|
||||
chat_message_name,
|
||||
chat_message_tripcode_nbsp,
|
||||
chat_message_tripcode,
|
||||
] = create_chat_user_components(user);
|
||||
const chat_message_user_components = create_chat_user_components(user);
|
||||
|
||||
const chat_message_markup = document.createElement("span");
|
||||
chat_message_markup.classList.add("chat-message__markup");
|
||||
|
@ -132,9 +192,9 @@ const create_chat_message = (object) => {
|
|||
|
||||
chat_message.insertAdjacentElement("beforeend", chat_message_time);
|
||||
chat_message.insertAdjacentHTML("beforeend", " ");
|
||||
chat_message.insertAdjacentElement("beforeend", chat_message_name);
|
||||
chat_message.insertAdjacentElement("beforeend", chat_message_tripcode_nbsp);
|
||||
chat_message.insertAdjacentElement("beforeend", chat_message_tripcode);
|
||||
for (const chat_message_user_component of chat_message_user_components) {
|
||||
chat_message.insertAdjacentElement("beforeend", chat_message_user_component);
|
||||
}
|
||||
chat_message.insertAdjacentHTML("beforeend", ": ");
|
||||
chat_message.insertAdjacentElement("beforeend", chat_message_markup);
|
||||
|
||||
|
@ -146,9 +206,11 @@ const create_chat_user_name = (user) => {
|
|||
chat_user_name.innerText = get_user_name({user});
|
||||
//chat_user_name.dataset.color = user.color; // not working in any browser
|
||||
if (!user.broadcaster && user.name === null) {
|
||||
const b = document.createElement("b");
|
||||
b.innerText = user.tag;
|
||||
const chat_user_name_tag = document.createElement("sup");
|
||||
chat_user_name_tag.classList.add("chat-name__tag");
|
||||
chat_user_name_tag.innerText = user.tag;
|
||||
chat_user_name_tag.innerHTML = b.outerHTML;
|
||||
chat_user_name.insertAdjacentElement("beforeend", chat_user_name_tag);
|
||||
}
|
||||
return chat_user_name;
|
||||
|
@ -167,7 +229,20 @@ const create_chat_user_components = (user) => {
|
|||
chat_user_tripcode.innerHTML = user.tripcode.digest;
|
||||
}
|
||||
|
||||
return [chat_user_name, chat_user_tripcode_nbsp, chat_user_tripcode];
|
||||
let result;
|
||||
if (!user.broadcaster) {
|
||||
result = [];
|
||||
} else {
|
||||
const chat_user_insignia = document.createElement("b");
|
||||
chat_user_insignia.classList.add("chat-insignia")
|
||||
chat_user_insignia.title = "Broadcaster";
|
||||
chat_user_insignia.innerText = "##";
|
||||
const chat_user_insignia_nbsp = document.createElement("span");
|
||||
chat_user_insignia_nbsp.innerHTML = " "
|
||||
result = [chat_user_insignia, chat_user_insignia_nbsp];
|
||||
}
|
||||
result.push(...[chat_user_name, chat_user_tripcode_nbsp, chat_user_tripcode]);
|
||||
return result;
|
||||
}
|
||||
const create_and_add_chat_message = (object) => {
|
||||
const chat_message = create_chat_message(object);
|
||||
|
@ -283,26 +358,26 @@ const update_user_tripcodes = (token_hash=null) => {
|
|||
for (const this_token_hash of token_hashes) {
|
||||
const tripcode = users[this_token_hash].tripcode;
|
||||
if (tripcode === null) {
|
||||
if (!to_ignore_display.has(token_hash)) {
|
||||
if (!to_ignore_display.has(this_token_hash)) {
|
||||
stylesheet_tripcode_display.insertRule(
|
||||
`[data-token-hash="${this_token_hash}"] > .for-tripcode { display: none; }`,
|
||||
stylesheet_tripcode_display.cssRules.length,
|
||||
);
|
||||
}
|
||||
if (!to_ignore_colors.has(token_hash)) {
|
||||
if (!to_ignore_colors.has(this_token_hash)) {
|
||||
stylesheet_tripcode_colors.insertRule(
|
||||
`[data-token-hash="${this_token_hash}"] > .tripcode { background-color: initial; color: initial; }`,
|
||||
stylesheet_tripcode_colors.cssRules.length,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!to_ignore_display.has(token_hash)) {
|
||||
if (!to_ignore_display.has(this_token_hash)) {
|
||||
stylesheet_tripcode_display.insertRule(
|
||||
`[data-token-hash="${this_token_hash}"] > .for-tripcode { display: inline; }`,
|
||||
stylesheet_tripcode_display.cssRules.length,
|
||||
);
|
||||
}
|
||||
if (!to_ignore_colors.has(token_hash)) {
|
||||
if (!to_ignore_colors.has(this_token_hash)) {
|
||||
stylesheet_tripcode_colors.insertRule(
|
||||
`[data-token-hash="${this_token_hash}"] > .tripcode { background-color: ${tripcode.background_color}; color: ${tripcode.foreground_color}; }`,
|
||||
stylesheet_tripcode_colors.cssRules.length,
|
||||
|
@ -349,16 +424,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;
|
||||
|
@ -386,9 +461,9 @@ const disable_captcha = () => {
|
|||
}
|
||||
|
||||
const set_title = (title) => {
|
||||
const element = document.createElement("h1");
|
||||
element.innerText = title.replaceAll(/\r?\n/g, " ");
|
||||
info_title.innerHTML = element.outerHTML;
|
||||
const h1 = document.createElement("h1");
|
||||
h1.innerText = title.replaceAll(/\r?\n/g, " ");
|
||||
info_title.innerHTML = h1.outerHTML;
|
||||
}
|
||||
|
||||
const update_uptime = () => {
|
||||
|
@ -493,6 +568,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":
|
||||
|
@ -546,6 +622,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);
|
||||
|
@ -583,6 +667,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 = "";
|
||||
|
@ -592,6 +681,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":
|
||||
|
@ -630,6 +720,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);
|
||||
}
|
||||
|
@ -644,13 +769,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'>···</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'>✓</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.
|
||||
|
@ -666,7 +791,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'>×</span>";
|
||||
if (!ws.successor) {
|
||||
ws.successor = true;
|
||||
setTimeout(connect_websocket, websocket_backoff);
|
||||
|
@ -698,7 +823,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");
|
||||
|
@ -710,6 +834,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;
|
||||
|
|
|
@ -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 |
|
@ -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 {
|
||||
|
@ -192,6 +190,9 @@ noscript {
|
|||
.chat-message__time {
|
||||
color: #b2b2b3;
|
||||
font-size: 10pt;
|
||||
cursor: default;
|
||||
}
|
||||
.chat-insignia {
|
||||
cursor: help;
|
||||
}
|
||||
.chat-name {
|
||||
|
@ -256,18 +257,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;
|
||||
margin: 0 0.5rem 0.5rem 0.5rem;
|
||||
}
|
||||
#chat-form_js__submit {
|
||||
grid-column: 2 / span 1;
|
||||
padding: 0 0.5rem 0.5rem 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
#chat-form_js__comment {
|
||||
grid-column: 1 / span 4;
|
||||
grid-column: 1 / span 5;
|
||||
background-color: #434347;
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
|
@ -296,16 +298,100 @@ 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;
|
||||
}
|
||||
#chat-form_js:not([data-notice]) > #chat-form_js__notice {
|
||||
display: none;
|
||||
}
|
||||
#chat-form_js__notice {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background: linear-gradient(#23232700, #2323277f 8%, #232327);
|
||||
height: 100%;
|
||||
display: grid;
|
||||
z-index: 1;
|
||||
}
|
||||
#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;
|
||||
}
|
||||
#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;
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="content-security-policy" content="default-src 'none'; connect-src 'self'; img-src 'self'; frame-src 'self'; media-src 'self'; script-src 'self'; style-src 'self' 'nonce-{{ csp }}';">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
|
||||
</head>
|
||||
<body id="both" data-token="{{ user.token }}" data-token-hash="{{ user.token_hash }}">
|
||||
<body id="both" data-token="{{ user.token }}" data-token-hash="{{ user.token_hash }}" data-csp="{{ csp }}">
|
||||
<article id="stream">
|
||||
<noscript><iframe id="stream_nojs" name="stream_nojs" src="{{ url_for('nojs_stream', token=user.token) }}"></iframe></noscript>
|
||||
</article>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{%
|
||||
macro appearance(
|
||||
user,
|
||||
insignia_class,
|
||||
name_class,
|
||||
tag_class,
|
||||
tripcode_nbsp_class='for-tripcode',
|
||||
|
@ -8,15 +9,19 @@
|
|||
)
|
||||
%}
|
||||
{{- '' -}}
|
||||
<span class="{{ name_class }}" style="color:{{ user.color }};">
|
||||
{%- if user.broadcaster -%}
|
||||
<b class="{{ insignia_class }}" title="Broadcaster">##</b>
|
||||
{{- ' ' | safe -}}
|
||||
{%- endif -%}
|
||||
<span class="{{ name_class }}">
|
||||
{{- user.name or get_default_name(user) -}}
|
||||
{%- if not user.broadcaster and user.name is none -%}
|
||||
<sup class="{{ tag_class }}">{{ user.tag }}</sup>
|
||||
<sup class="{{ tag_class }}"><b>{{ user.tag }}</b></sup>
|
||||
{%- endif -%}
|
||||
</span>
|
||||
{%- if user.tripcode -%}
|
||||
<span class="{{ tripcode_nbsp_class }}"> </span>
|
||||
{{- '' -}}
|
||||
<span class="{{ tripcode_class }}" style="background-color:{{ user.tripcode.background_color }};color:{{ user.tripcode.foreground_color }};">{{ user.tripcode.digest }}</span>
|
||||
<span class="{{ tripcode_class }}">{{ user.tripcode.digest }}</span>
|
||||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
<meta http-equiv="content-security-policy" content="default-src 'none'; img-src 'self'; style-src 'nonce-{{ csp }}';">
|
||||
<style nonce="{{ csp }}">
|
||||
:root {
|
||||
--link-color: #42a5d7;
|
||||
--padding-size: 0.5rem;
|
||||
|
@ -41,6 +42,8 @@
|
|||
padding: 0;
|
||||
}
|
||||
#tripcode {
|
||||
background-color: {{ user.tripcode.background_color }};
|
||||
color: {{ user.tripcode.foreground_color }};
|
||||
cursor: pointer;
|
||||
}
|
||||
.x {
|
||||
|
@ -56,6 +59,7 @@
|
|||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
#notice h2 {
|
||||
margin: 0;
|
||||
|
@ -73,7 +77,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 +124,7 @@
|
|||
}
|
||||
|
||||
#appearance-form {
|
||||
display: grid;
|
||||
grid-auto-rows: 1fr 1fr 2rem;
|
||||
grid-auto-columns: min-content 1fr min-content;
|
||||
}
|
||||
|
@ -172,46 +177,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 %}accesskey="x">
|
||||
{% 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" accesskey="z">
|
||||
<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 accesskey="m">{{ 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">
|
||||
|
@ -226,11 +233,11 @@
|
|||
<input id="password-toggle" name="set-tripcode" type="checkbox" accesskey="s">
|
||||
<input id="cleared-toggle" name="clear-tripcode" type="checkbox"{% if user.tripcode != none %} accesskey="c"{% endif %}>
|
||||
<div id="password-column">
|
||||
{% if user.tripcode == none %}
|
||||
{% if user.tripcode is none %}
|
||||
<span class="tripcode">(no tripcode)</span>
|
||||
<label for="password-toggle" class="show-password pseudolink">set</label>
|
||||
{% else %}
|
||||
<label id="tripcode" for="password-toggle" class="show-password tripcode" style="background-color:{{ user.tripcode.background_color }};color:{{ user.tripcode.foreground_color }};">{{ user.tripcode.digest }}</label>
|
||||
<label id="tripcode" for="password-toggle" class="show-password tripcode">{{ user.tripcode.digest }}</label>
|
||||
<label id="show-cleared" for="cleared-toggle" class="pseudolink x">✗</label>
|
||||
<div id="cleared" class="tripcode">(cleared)</div>
|
||||
<label id="hide-cleared" for="cleared-toggle" class="pseudolink">undo</label>
|
||||
|
@ -239,7 +246,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">✗</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>
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="content-security-policy" content="default-src 'none'; style-src 'nonce-{{ csp }}';">
|
||||
<meta http-equiv="refresh" content="4">
|
||||
<meta http-equiv="refresh" content="5; url={{ url_for('nojs_chat_messages_redirect', token=user.token) }}">
|
||||
<style>
|
||||
<style nonce="{{ csp }}">
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -100,6 +101,9 @@
|
|||
.chat-message__time {
|
||||
color: #b2b2b3;
|
||||
font-size: 10pt;
|
||||
cursor: default;
|
||||
}
|
||||
.chat-message__insignia {
|
||||
cursor: help;
|
||||
}
|
||||
.chat-message__name {
|
||||
|
@ -124,6 +128,20 @@
|
|||
font-size: 9pt;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
{% for token in messages | map(attribute='token') | list | unique %}
|
||||
{% with user = users_by_token[token] %}
|
||||
[data-token-hash="{{ user.token_hash }}"] > .chat-message__name {
|
||||
color: {{ user.color }};
|
||||
}
|
||||
{% if user.tripcode %}
|
||||
[data-token-hash="{{ user.token_hash }}"] > .tripcode {
|
||||
background-color: {{ user.tripcode.background_color }};
|
||||
color: {{ user.tripcode.foreground_color }};
|
||||
}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -141,7 +159,7 @@
|
|||
{% with user = users_by_token[message.token] %}
|
||||
<time class="chat-message__time" datetime="{{ message.date }}T{{ message.time_seconds }}Z" title="{{ message.date }} {{ message.time_seconds }}">{{ message.time_minutes }}</time>
|
||||
{{- ' ' | safe -}}
|
||||
{{ appearance(user, name_class='chat-message__name', tag_class='chat-message__name__tag') }}
|
||||
{{ appearance(user, insignia_class='chat-message__insignia', name_class='chat-message__name', tag_class='chat-message__name__tag') }}
|
||||
{{- ': ' -}}
|
||||
<span class="chat-message__markup">{{ message.markup }}</span>
|
||||
{% endwith %}
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="content-security-policy" content="default-src 'none'; style-src 'nonce-{{ csp }}';">
|
||||
<meta http-equiv="refresh" content="6">
|
||||
<style>
|
||||
<style nonce="{{ csp }}">
|
||||
html {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
@ -61,6 +62,9 @@
|
|||
.user {
|
||||
line-height: 1.4375;
|
||||
}
|
||||
.user__insignia {
|
||||
cursor: help;
|
||||
}
|
||||
.user__name {
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
|
@ -77,6 +81,18 @@
|
|||
font-size: 9pt;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
{% for user in users_watching + users_notwatching %}
|
||||
[data-token-hash="{{ user.token_hash }}"] > .user__name {
|
||||
color: {{ user.color }};
|
||||
}
|
||||
{% if user.tripcode %}
|
||||
[data-token-hash="{{ user.token_hash }}"] > .tripcode {
|
||||
background-color: {{ user.tripcode.background_color }};
|
||||
color: {{ user.tripcode.foreground_color }};
|
||||
}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -90,8 +106,8 @@
|
|||
<h5>Watching ({{ users_watching | length }})</h5>
|
||||
<ul>
|
||||
{% for user_listed in users_watching %}
|
||||
<li class="user">
|
||||
{{- appearance(user_listed, name_class='user__name', tag_class='user__name__tag') -}}
|
||||
<li class="user" data-token-hash="{{ user.token_hash }}">
|
||||
{{- appearance(user_listed, insignia_class='user__insignia', name_class='user__name', tag_class='user__name__tag') -}}
|
||||
{%- if user.token == user_listed.token %} (You){% endif -%}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
@ -100,8 +116,8 @@
|
|||
<h5>Not watching ({{ users_notwatching | length }})</h5>
|
||||
<ul>
|
||||
{% for user_listed in users_notwatching %}
|
||||
<li class="user">
|
||||
{{- appearance(user_listed, name_class='user__name', tag_class='user__name__tag') -}}
|
||||
<li class="user" data-token-hash="{{ user.token_hash }}">
|
||||
{{- appearance(user_listed, insignia_class='user__insignia', name_class='user__name', tag_class='user__name__tag') -}}
|
||||
{%- if user.token == user_listed.token %} (You){% endif -%}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="content-security-policy" content="default-src 'none'; style-src 'nonce-{{ csp }}';">
|
||||
<meta http-equiv="refresh" content="6">
|
||||
<style>
|
||||
<style nonce="{{ csp }}">
|
||||
body {
|
||||
overflow-y: auto;
|
||||
margin: 0.75ch 1.25ch;
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
<meta http-equiv="content-security-policy" content="default-src 'none'; media-src 'self'; style-src 'nonce-{{ csp }}';">
|
||||
<style nonce="{{ csp }}">
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import secrets
|
||||
|
||||
def generate_csp():
|
||||
'''
|
||||
Generate a random Content Secuity Policy nonce.
|
||||
'''
|
||||
return secrets.token_urlsafe(16)
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
読み込み中…
新しいイシューから参照