Nojs chat: ETag, limit scrollback, timeout notice
Limiting scrollback is happening for the js chat too. Also reject long comments.
このコミットが含まれているのは:
コミット
6109de37ec
|
@ -31,16 +31,19 @@ async def create_app():
|
||||||
'MAX_CHAT_SCROLLBACK': config['memory']['chat_scrollback'],
|
'MAX_CHAT_SCROLLBACK': config['memory']['chat_scrollback'],
|
||||||
'CHECKUP_PERIOD_USER': config['ratelimits']['user_absence'],
|
'CHECKUP_PERIOD_USER': config['ratelimits']['user_absence'],
|
||||||
'CHECKUP_PERIOD_CAPTCHA': config['ratelimits']['captcha_expiry'],
|
'CHECKUP_PERIOD_CAPTCHA': config['ratelimits']['captcha_expiry'],
|
||||||
'THRESHOLD_IDLE': config['thresholds']['idle'],
|
'THRESHOLD_USER_IDLE': config['thresholds']['user_idle'],
|
||||||
'THRESHOLD_ABSENT': config['thresholds']['absent'],
|
'THRESHOLD_USER_ABSENT': config['thresholds']['user_absent'],
|
||||||
|
'THRESHOLD_NOJS_CHAT_TIMEOUT': config['thresholds']['nojs_chat_timeout'],
|
||||||
'CHAT_COMMENT_MAX_LENGTH': config['chat']['max_name_length'],
|
'CHAT_COMMENT_MAX_LENGTH': config['chat']['max_name_length'],
|
||||||
'CHAT_NAME_MAX_LENGTH': config['chat']['max_name_length'],
|
'CHAT_NAME_MAX_LENGTH': config['chat']['max_name_length'],
|
||||||
'CHAT_NAME_MIN_CONTRAST': config['chat']['min_name_contrast'],
|
'CHAT_NAME_MIN_CONTRAST': config['chat']['min_name_contrast'],
|
||||||
'CHAT_BACKGROUND_COLOUR': color_to_colour(config['chat']['background_color']),
|
'CHAT_BACKGROUND_COLOUR': color_to_colour(config['chat']['background_color']),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
assert app.config['MAX_NOTICES'] >= 0
|
||||||
|
assert app.config['MAX_CHAT_SCROLLBACK'] >= 0
|
||||||
assert app.config['MAX_CHAT_MESSAGES'] >= app.config['MAX_CHAT_SCROLLBACK']
|
assert app.config['MAX_CHAT_MESSAGES'] >= app.config['MAX_CHAT_SCROLLBACK']
|
||||||
assert app.config['THRESHOLD_ABSENT'] >= app.config['THRESHOLD_IDLE']
|
assert app.config['THRESHOLD_USER_ABSENT'] >= app.config['THRESHOLD_USER_IDLE']
|
||||||
|
|
||||||
app.messages_by_id = OrderedDict()
|
app.messages_by_id = OrderedDict()
|
||||||
app.users_by_token = {}
|
app.users_by_token = {}
|
||||||
|
|
|
@ -3,9 +3,10 @@ from datetime import datetime
|
||||||
|
|
||||||
from quart import current_app, escape
|
from quart import current_app, escape
|
||||||
|
|
||||||
from anonstream.helpers.chat import generate_nonce_hash
|
from anonstream.helpers.chat import generate_nonce_hash, get_scrollback
|
||||||
from anonstream.utils.chat import message_for_websocket
|
from anonstream.utils.chat import message_for_websocket
|
||||||
|
|
||||||
|
CONFIG = current_app.config
|
||||||
MESSAGES_BY_ID = current_app.messages_by_id
|
MESSAGES_BY_ID = current_app.messages_by_id
|
||||||
MESSAGES = current_app.messages
|
MESSAGES = current_app.messages
|
||||||
USERS_BY_TOKEN = current_app.users_by_token
|
USERS_BY_TOKEN = current_app.users_by_token
|
||||||
|
@ -25,7 +26,7 @@ def messages_for_websocket():
|
||||||
user=USERS_BY_TOKEN[message['token']],
|
user=USERS_BY_TOKEN[message['token']],
|
||||||
message=message,
|
message=message,
|
||||||
),
|
),
|
||||||
MESSAGES,
|
get_scrollback(MESSAGES),
|
||||||
))
|
))
|
||||||
|
|
||||||
async def add_chat_message(user, nonce, comment):
|
async def add_chat_message(user, nonce, comment):
|
||||||
|
@ -35,6 +36,8 @@ async def add_chat_message(user, nonce, comment):
|
||||||
raise Rejected('Discarded suspected duplicate message')
|
raise Rejected('Discarded suspected duplicate message')
|
||||||
if len(comment) == 0:
|
if len(comment) == 0:
|
||||||
raise Rejected('Message was empty')
|
raise Rejected('Message was empty')
|
||||||
|
if len(comment) > 512:
|
||||||
|
raise Rejected('Message exceeded 512 chars')
|
||||||
|
|
||||||
# add message
|
# add message
|
||||||
timestamp_ms = time.time_ns() // 1_000_000
|
timestamp_ms = time.time_ns() // 1_000_000
|
||||||
|
@ -62,6 +65,9 @@ async def add_chat_message(user, nonce, comment):
|
||||||
'markup': markup,
|
'markup': markup,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
while len(MESSAGES_BY_ID) > CONFIG['MAX_CHAT_MESSAGES']:
|
||||||
|
MESSAGES_BY_ID.pop(last=False)
|
||||||
|
|
||||||
# broadcast message to websockets
|
# broadcast message to websockets
|
||||||
await broadcast(
|
await broadcast(
|
||||||
USERS,
|
USERS,
|
||||||
|
|
|
@ -7,3 +7,9 @@ CONFIG = current_app.config
|
||||||
def generate_nonce_hash(nonce):
|
def generate_nonce_hash(nonce):
|
||||||
parts = CONFIG['SECRET_KEY'] + b'nonce-hash\0' + nonce.encode()
|
parts = CONFIG['SECRET_KEY'] + b'nonce-hash\0' + nonce.encode()
|
||||||
return hashlib.sha256(parts).digest()
|
return hashlib.sha256(parts).digest()
|
||||||
|
|
||||||
|
def get_scrollback(messages):
|
||||||
|
n = CONFIG['MAX_CHAT_SCROLLBACK']
|
||||||
|
if len(messages) < n:
|
||||||
|
return messages
|
||||||
|
return list(messages)[-n:]
|
||||||
|
|
|
@ -44,14 +44,14 @@ def get_default_name(user):
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_watching(timestamp, user):
|
def is_watching(timestamp, user):
|
||||||
return user['watching_last'] >= timestamp - CONFIG['THRESHOLD_IDLE']
|
return user['watching_last'] >= timestamp - CONFIG['THRESHOLD_USER_IDLE']
|
||||||
|
|
||||||
def is_idle(timestamp, user):
|
def is_idle(timestamp, user):
|
||||||
return is_present(timestamp, user) and not is_watching(timestamp, user)
|
return is_present(timestamp, user) and not is_watching(timestamp, user)
|
||||||
|
|
||||||
def is_present(timestamp, user):
|
def is_present(timestamp, user):
|
||||||
return (
|
return (
|
||||||
user['seen']['last'] >= timestamp - CONFIG['THRESHOLD_ABSENT']
|
user['seen']['last'] >= timestamp - CONFIG['THRESHOLD_USER_ABSENT']
|
||||||
or len(user['websockets']) > 0
|
or len(user['websockets']) > 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,9 @@ from quart import current_app, request, render_template, redirect, url_for, esca
|
||||||
from anonstream.stream import get_stream_title
|
from anonstream.stream import get_stream_title
|
||||||
from anonstream.user import add_notice, pop_notice, try_change_appearance
|
from anonstream.user import add_notice, pop_notice, try_change_appearance
|
||||||
from anonstream.chat import add_chat_message, Rejected
|
from anonstream.chat import add_chat_message, Rejected
|
||||||
from anonstream.routes.wrappers import with_user_from
|
from anonstream.routes.wrappers import with_user_from, render_template_with_etag
|
||||||
from anonstream.helpers.user import get_default_name
|
from anonstream.helpers.user import get_default_name
|
||||||
|
from anonstream.helpers.chat import get_scrollback
|
||||||
from anonstream.utils.chat import generate_nonce
|
from anonstream.utils.chat import generate_nonce
|
||||||
from anonstream.utils.user import concatenate_for_notice
|
from anonstream.utils.user import concatenate_for_notice
|
||||||
|
|
||||||
|
@ -20,14 +21,20 @@ async def nojs_info(user):
|
||||||
@current_app.route('/chat/messages.html')
|
@current_app.route('/chat/messages.html')
|
||||||
@with_user_from(request)
|
@with_user_from(request)
|
||||||
async def nojs_chat(user):
|
async def nojs_chat(user):
|
||||||
return await render_template(
|
return await render_template_with_etag(
|
||||||
'nojs_chat.html',
|
'nojs_chat.html',
|
||||||
user=user,
|
user=user,
|
||||||
users_by_token=current_app.users_by_token,
|
users_by_token=current_app.users_by_token,
|
||||||
messages=current_app.messages,
|
messages=get_scrollback(current_app.messages),
|
||||||
|
timeout=current_app.config['THRESHOLD_NOJS_CHAT_TIMEOUT'],
|
||||||
get_default_name=get_default_name,
|
get_default_name=get_default_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@current_app.route('/chat/redirect')
|
||||||
|
@with_user_from(request)
|
||||||
|
async def nojs_chat_redirect(user):
|
||||||
|
return redirect(url_for('nojs_chat', _anchor='end'))
|
||||||
|
|
||||||
@current_app.route('/chat/form.html')
|
@current_app.route('/chat/form.html')
|
||||||
@with_user_from(request)
|
@with_user_from(request)
|
||||||
async def nojs_form(user):
|
async def nojs_form(user):
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
import hashlib
|
||||||
import time
|
import time
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from quart import current_app, request, abort, make_response
|
from quart import current_app, request, abort, make_response, render_template, request
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
from anonstream.user import sunset, user_for_websocket
|
from anonstream.user import sunset, user_for_websocket
|
||||||
|
@ -97,3 +98,12 @@ def with_user_from(context):
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return 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()
|
||||||
|
etag = f'W/"{tag}"'
|
||||||
|
if request.if_none_match.contains_weak(tag):
|
||||||
|
return '', 304, {'ETag': etag}
|
||||||
|
else:
|
||||||
|
return rendered_template, {'ETag': etag}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* token */
|
/* token */
|
||||||
const token = document.querySelector("body").dataset.token;
|
const token = document.body.dataset.token;
|
||||||
|
|
||||||
/* insert js-only markup */
|
/* insert js-only markup */
|
||||||
const jsmarkup_style_color = '<style id="style-color"></style>'
|
const jsmarkup_style_color = '<style id="style-color"></style>'
|
||||||
|
@ -7,7 +7,7 @@ const jsmarkup_style_tripcode_display = '<style id="style-tripcode-display"></st
|
||||||
const jsmarkup_style_tripcode_colors = '<style id="style-tripcode-colors"></style>'
|
const jsmarkup_style_tripcode_colors = '<style id="style-tripcode-colors"></style>'
|
||||||
const jsmarkup_info = '<div id="info_js"></div>';
|
const jsmarkup_info = '<div id="info_js"></div>';
|
||||||
const jsmarkup_info_title = '<header id="info_js__title" data-js="true"></header>';
|
const jsmarkup_info_title = '<header id="info_js__title" data-js="true"></header>';
|
||||||
const jsmarkup_chat_messages = '<ul id="chat-messages_js" data-js="true"></ul>';
|
const jsmarkup_chat_messages = '<ol id="chat-messages_js" data-js="true"></ol>';
|
||||||
const jsmarkup_chat_form = `\
|
const jsmarkup_chat_form = `\
|
||||||
<form id="chat-form_js" data-js="true" action="/chat" method="post">
|
<form id="chat-form_js" data-js="true" action="/chat" method="post">
|
||||||
<input id="chat-form_js__nonce" type="hidden" name="nonce" value="">
|
<input id="chat-form_js__nonce" type="hidden" name="nonce" value="">
|
||||||
|
@ -69,7 +69,7 @@ const create_chat_message = (object) => {
|
||||||
|
|
||||||
const chat_message_name = document.createElement("span");
|
const chat_message_name = document.createElement("span");
|
||||||
chat_message_name.classList.add("chat-message__name");
|
chat_message_name.classList.add("chat-message__name");
|
||||||
chat_message_name.innerText = user.name || default_name[user.broadcaster];
|
chat_message_name.innerText = get_user_name({user});
|
||||||
//chat_message_name.dataset.color = user.color; // not working in any browser
|
//chat_message_name.dataset.color = user.color; // not working in any browser
|
||||||
|
|
||||||
const chat_message_tripcode_nbsp = document.createElement("span");
|
const chat_message_tripcode_nbsp = document.createElement("span");
|
||||||
|
@ -95,9 +95,17 @@ const create_chat_message = (object) => {
|
||||||
|
|
||||||
return chat_message
|
return chat_message
|
||||||
}
|
}
|
||||||
|
const create_and_add_chat_message = (object) => {
|
||||||
|
const chat_message = create_chat_message(object);
|
||||||
|
chat_messages.insertAdjacentElement("beforeend", chat_message);
|
||||||
|
while (chat_messages.children.length > max_chat_scrollback) {
|
||||||
|
chat_messages.children[0].remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let users = {};
|
let users = {};
|
||||||
let default_name = {true: "Broadcaster", false: "Anonymous"};
|
let default_name = {true: "Broadcaster", false: "Anonymous"};
|
||||||
|
let max_chat_scrollback = 256;
|
||||||
const tidy_stylesheet = ({stylesheet, selector_regex, ignore_condition}) => {
|
const tidy_stylesheet = ({stylesheet, selector_regex, ignore_condition}) => {
|
||||||
const to_delete = [];
|
const to_delete = [];
|
||||||
const to_ignore = new Set();
|
const to_ignore = new Set();
|
||||||
|
@ -148,11 +156,17 @@ const update_user_colors = (token_hash=null) => {
|
||||||
stylesheet_color.deleteRule(index);
|
stylesheet_color.deleteRule(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const update_user_name = (token_hash) => {
|
const get_user_name({user=null, token_hash}) {
|
||||||
const name = users[token_hash].name;
|
const user = user || users[token_hash]
|
||||||
|
return user.name || default_name[user.broadcaster];
|
||||||
|
}
|
||||||
|
const update_user_names = (token_hash=null) => {
|
||||||
|
const token_hashes = token_hash === null ? Object.keys(users) : [token_hash];
|
||||||
for (const chat_message of chat_messages.children) {
|
for (const chat_message of chat_messages.children) {
|
||||||
if (token_hash === chat_message.dataset.tokenHash) {
|
const this_token_hash = chat_message.dataset.tokenHash;
|
||||||
chat_message.querySelector(".chat-message__name").innerText = name;
|
if (token_hashes.includes(this_token_hash) {
|
||||||
|
const chat_message_name = chat_message.querySelector(".chat-message__name");
|
||||||
|
chat_message_name.innerText = get_user_name({token_hash: this_token_hash});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,7 +247,8 @@ const update_user_tripcodes = (token_hash=null) => {
|
||||||
const this_token_hash = chat_message.dataset.tokenHash;
|
const this_token_hash = chat_message.dataset.tokenHash;
|
||||||
const tripcode = users[this_token_hash].tripcode;
|
const tripcode = users[this_token_hash].tripcode;
|
||||||
if (token_hashes.includes(this_token_hash)) {
|
if (token_hashes.includes(this_token_hash)) {
|
||||||
chat_message.querySelector(".tripcode").innerText = tripcode === null ? "" : tripcode.digest;
|
const chat_message_tripcode = chat_message.querySelector(".tripcode");
|
||||||
|
chat_message_tripcode.innerText = tripcode === null ? "" : tripcode.digest;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -253,7 +268,9 @@ const on_websocket_message = (event) => {
|
||||||
info_title.innerText = receipt.title;
|
info_title.innerText = receipt.title;
|
||||||
|
|
||||||
default_name = receipt.default;
|
default_name = receipt.default;
|
||||||
|
max_chat_scrollback = receipt.scrollback;
|
||||||
users = receipt.users;
|
users = receipt.users;
|
||||||
|
update_user_names();
|
||||||
update_user_colors();
|
update_user_colors();
|
||||||
update_user_tripcodes();
|
update_user_tripcodes();
|
||||||
|
|
||||||
|
@ -273,8 +290,7 @@ const on_websocket_message = (event) => {
|
||||||
const last_seq = last === null ? null : parseInt(last.dataset.seq);
|
const last_seq = last === null ? null : parseInt(last.dataset.seq);
|
||||||
for (const message of receipt.messages) {
|
for (const message of receipt.messages) {
|
||||||
if (message.seq > last_seq) {
|
if (message.seq > last_seq) {
|
||||||
const chat_message = create_chat_message(message);
|
create_and_add_chat_message(message);
|
||||||
chat_messages.insertAdjacentElement("beforeend", chat_message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,8 +321,7 @@ const on_websocket_message = (event) => {
|
||||||
|
|
||||||
case "chat":
|
case "chat":
|
||||||
console.log("ws chat", receipt);
|
console.log("ws chat", receipt);
|
||||||
const chat_message = create_chat_message(receipt);
|
create_and_add_chat_message(receipt);
|
||||||
chat_messages.insertAdjacentElement("beforeend", chat_message);
|
|
||||||
chat_messages.scrollTo({
|
chat_messages.scrollTo({
|
||||||
left: 0,
|
left: 0,
|
||||||
top: chat_messages.scrollTopMax,
|
top: chat_messages.scrollTopMax,
|
||||||
|
@ -327,7 +342,7 @@ const on_websocket_message = (event) => {
|
||||||
user.name = receipt.name;
|
user.name = receipt.name;
|
||||||
user.color = receipt.color;
|
user.color = receipt.color;
|
||||||
user.tripcode = receipt.tripcode;
|
user.tripcode = receipt.tripcode;
|
||||||
update_user_name(receipt.token_hash);
|
update_user_names(receipt.token_hash);
|
||||||
update_user_colors(receipt.token_hash);
|
update_user_colors(receipt.token_hash);
|
||||||
update_user_tripcodes(receipt.token_hash);
|
update_user_tripcodes(receipt.token_hash);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -84,14 +84,14 @@ noscript {
|
||||||
}
|
}
|
||||||
#chat__header {
|
#chat__header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1ch 0;
|
padding: 0.5rem 0;
|
||||||
border-bottom: var(--chat-border);
|
border-bottom: var(--chat-border);
|
||||||
}
|
}
|
||||||
#chat-form_js {
|
#chat-form_js {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template: auto var(--button-height) / auto 5rem;
|
grid-template: auto var(--button-height) / auto 5rem;
|
||||||
grid-gap: 0.375rem;
|
grid-gap: 0.375rem;
|
||||||
margin: 0 1ch 1ch 1ch;
|
margin: 0 0.5rem 0.5rem 0.5rem;
|
||||||
}
|
}
|
||||||
#chat-form_js__submit {
|
#chat-form_js__submit {
|
||||||
grid-column: 2 / span 1;
|
grid-column: 2 / span 1;
|
||||||
|
@ -125,7 +125,7 @@ noscript {
|
||||||
#chat-messages_js {
|
#chat-messages_js {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 1ch 1ch;
|
padding: 0 0.5rem 0.5rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<aside id="chat">
|
<aside id="chat">
|
||||||
<header id="chat__header">Stream chat</header>
|
<header id="chat__header">Stream chat</header>
|
||||||
<article id="chat__messages">
|
<article id="chat__messages">
|
||||||
<noscript><iframe id="chat-messages_nojs" src="{{ url_for('nojs_chat', token=user.token, _anchor='bottom') }}" data-js="false"></iframe></noscript>
|
<noscript><iframe id="chat-messages_nojs" src="{{ url_for('nojs_chat', token=user.token, _anchor='end') }}" data-js="false"></iframe></noscript>
|
||||||
</article>
|
</article>
|
||||||
<section id="chat__form">
|
<section id="chat__form">
|
||||||
<noscript><iframe id="chat-form_nojs" src="{{ url_for('nojs_form', token=user.token) }}" data-js="false"></iframe></noscript>
|
<noscript><iframe id="chat-form_nojs" src="{{ url_for('nojs_form', token=user.token) }}" data-js="false"></iframe></noscript>
|
||||||
|
|
|
@ -9,18 +9,84 @@
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 100%;
|
padding: 0.5rem 0.5rem 0 0.5rem;
|
||||||
|
min-height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #3674bf;
|
||||||
|
border: 4px outset #3584e4;
|
||||||
|
box-shadow: 0 0 5px #3584e4;
|
||||||
|
padding: 1.25ch 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 12pt;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
}
|
||||||
|
#chat-timeout {
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
width: calc(100% - 1rem);
|
||||||
|
visibility: hidden;
|
||||||
|
animation: appear 0s {{ timeout }}s forwards;
|
||||||
|
}
|
||||||
|
#chat-timeout header {
|
||||||
|
font-size: 20pt;
|
||||||
|
}
|
||||||
|
#chat-timeout-dismiss {
|
||||||
|
animation: padding 0s {{ timeout }}s forwards;
|
||||||
|
}
|
||||||
|
#chat-timeout-dismiss > .button {
|
||||||
|
visibility: hidden;
|
||||||
|
height: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
animation:
|
||||||
|
appear 0s {{ timeout }}s forwards,
|
||||||
|
unskinny 0s {{ timeout }}s forwards;
|
||||||
|
}
|
||||||
|
#chat-timeout-alt {
|
||||||
|
padding: 4px 0 2px 0;
|
||||||
|
}
|
||||||
|
#notimeout:target + #chat-timeout,
|
||||||
|
#notimeout:target ~ #chat-timeout-dismiss,
|
||||||
|
#notimeout:not(:target) ~ #chat-timeout-alt {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@keyframes appear {
|
||||||
|
to {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes unskinny {
|
||||||
|
to {
|
||||||
|
height: auto;
|
||||||
|
padding: 1.25ch 0;
|
||||||
|
border: 4px outset #3584e4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes padding {
|
||||||
|
to {
|
||||||
|
padding: 4px 0 2px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#chat-messages {
|
#chat-messages {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 1ch 1ch 0 1ch;
|
padding: 0;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
box-sizing: border-box;
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
}
|
||||||
.chat-message {
|
.chat-message {
|
||||||
padding: 0.5ch 0.75ch;
|
padding: 0.5ch 0.75ch;
|
||||||
|
@ -52,15 +118,28 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<ul id="chat-messages">
|
<div id="end"></div>
|
||||||
|
<div id="notimeout"></div>
|
||||||
|
<aside id="chat-timeout">
|
||||||
|
<a class="button" href="{{ url_for('nojs_chat_redirect') }}">
|
||||||
|
<header>Timed out</header>
|
||||||
|
<small>Click to refresh</small>
|
||||||
|
</a>
|
||||||
|
</aside>
|
||||||
|
<ol id="chat-messages">
|
||||||
{% for message in messages | reverse %}
|
{% for message in messages | reverse %}
|
||||||
<li class="chat-message">
|
<li class="chat-message">
|
||||||
{% with user = users_by_token[message.token] %}
|
{% with user = users_by_token[message.token] %}
|
||||||
<span class="chat-message__name" style="color:{{ user.color }};" data-id="{{ message.id }}">{{ user.name or get_default_name(user) }}</span>{% if user.tripcode != none %}<span class="for-tripcode"> </span><span class="tripcode for-tripcode" style="background-color:{{ user.tripcode.background_color }};color:{{ user.tripcode.foreground_color }};">{{ user.tripcode.digest }}</span>{% endif %}: <span class="chat-message__markup">{{ message.markup }}</span>
|
<span class="chat-message__name" style="color:{{ user.color }};" data-seq="{{ message.seq }}" data-token-hash="{{ user.token_hash }}">{{ user.name or get_default_name(user) }}</span>{% if user.tripcode != none %}<span class="for-tripcode"> </span><span class="tripcode for-tripcode" style="background-color:{{ user.tripcode.background_color }};color:{{ user.tripcode.foreground_color }};">{{ user.tripcode.digest }}</span>{% endif %}: <span class="chat-message__markup">{{ message.markup }}</span>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ol>
|
||||||
<div id="bottom"></div>
|
<aside id="chat-timeout-dismiss">
|
||||||
|
<a class="button" href="#notimeout">Hide timeout notice</a>
|
||||||
|
</aside>
|
||||||
|
<aside id="chat-timeout-alt">
|
||||||
|
<a class="button" href="{{ url_for('nojs_chat_redirect') }}">Click to refresh</a>
|
||||||
|
</aside>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--link-color: #42a5d7;
|
--link-color: #42a5d7;
|
||||||
--padding-size: 1ch;
|
--padding-size: 0.5rem;
|
||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -191,7 +191,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form id="chat-form" action="{{ url_for('nojs_submit_message', token=user.token) }}" method="post">
|
<form id="chat-form" action="{{ url_for('nojs_submit_message', token=user.token) }}" method="post">
|
||||||
<input type="hidden" name="nonce" value="{{ nonce }}">
|
<input type="hidden" name="nonce" value="{{ nonce }}">
|
||||||
<textarea id="chat-form__comment" name="comment" placeholder="Send a message..." rows="1" tabindex="1" required></textarea>
|
<textarea id="chat-form__comment" name="comment" maxlength="512" required placeholder="Send a message..." rows="1" tabindex="1"></textarea>
|
||||||
<div id="chat-form__exit"><a href="#appearance">Settings</a></div>
|
<div id="chat-form__exit"><a href="#appearance">Settings</a></div>
|
||||||
<input id="chat-form__submit" type="submit" value="Chat" tabindex="2" accesskey="p">
|
<input id="chat-form__submit" type="submit" value="Chat" tabindex="2" accesskey="p">
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -20,7 +20,7 @@ class BadAppearance(Exception):
|
||||||
def add_notice(user, notice, verbose=False):
|
def add_notice(user, notice, verbose=False):
|
||||||
notice_id = time.time_ns() // 1_000_000
|
notice_id = time.time_ns() // 1_000_000
|
||||||
user['notices'][notice_id] = (notice, verbose)
|
user['notices'][notice_id] = (notice, verbose)
|
||||||
if len(user['notices']) > CONFIG['MAX_NOTICES']:
|
while len(user['notices']) > CONFIG['MAX_NOTICES']:
|
||||||
user['notices'].popitem(last=False)
|
user['notices'].popitem(last=False)
|
||||||
return notice_id
|
return notice_id
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ async def websocket_outbound(queue):
|
||||||
True: CONFIG['DEFAULT_HOST_NAME'],
|
True: CONFIG['DEFAULT_HOST_NAME'],
|
||||||
False: CONFIG['DEFAULT_ANON_NAME'],
|
False: CONFIG['DEFAULT_ANON_NAME'],
|
||||||
},
|
},
|
||||||
|
'scrollback': CONFIG['MAX_CHAT_SCROLLBACK'],
|
||||||
}
|
}
|
||||||
await websocket.send_json(payload)
|
await websocket.send_json(payload)
|
||||||
while True:
|
while True:
|
||||||
|
|
|
@ -26,5 +26,6 @@ user_absence = 8
|
||||||
captcha_expiry = 8
|
captcha_expiry = 8
|
||||||
|
|
||||||
[thresholds]
|
[thresholds]
|
||||||
idle = 12
|
user_idle = 12
|
||||||
absent = 1800
|
user_absent = 1800
|
||||||
|
nojs_chat_timeout = 24
|
||||||
|
|
読み込み中…
新しいイシューから参照