Nojs comment submission, notify for rejected comments
Fix with-user wrapper (wasn't collecting users)
このコミットが含まれているのは:
コミット
694c6a4995
|
@ -14,20 +14,21 @@ async def create_app():
|
||||||
|
|
||||||
auth_password = secrets.token_urlsafe(6)
|
auth_password = secrets.token_urlsafe(6)
|
||||||
auth_pwhash = generate_password_hash(auth_password)
|
auth_pwhash = generate_password_hash(auth_password)
|
||||||
print('Broadcaster username:', config['auth_username'])
|
print('Broadcaster username:', config['auth']['username'])
|
||||||
print('Broadcaster password:', auth_password)
|
print('Broadcaster password:', auth_password)
|
||||||
|
|
||||||
app = Quart('anonstream')
|
app = Quart('anonstream')
|
||||||
app.config['SECRET_KEY'] = config['secret_key'].encode()
|
app.config['SECRET_KEY'] = config['secret_key'].encode()
|
||||||
app.config['AUTH_USERNAME'] = config['auth_username']
|
app.config['AUTH_USERNAME'] = config['auth']['username']
|
||||||
app.config['AUTH_PWHASH'] = auth_pwhash
|
app.config['AUTH_PWHASH'] = auth_pwhash
|
||||||
app.config['AUTH_TOKEN'] = generate_token()
|
app.config['AUTH_TOKEN'] = generate_token()
|
||||||
app.config['DEFAULT_HOST_NAME'] = config['default_host_name']
|
app.config['DEFAULT_HOST_NAME'] = config['names']['broadcaster']
|
||||||
app.config['DEFAULT_ANON_NAME'] = config['default_anon_name']
|
app.config['DEFAULT_ANON_NAME'] = config['names']['anonymous']
|
||||||
|
app.config['LIMIT_NOTICES'] = config['limits']['notices']
|
||||||
app.chat = OrderedDict()
|
app.chat = OrderedDict()
|
||||||
app.users = {}
|
app.users = {}
|
||||||
app.websockets = set()
|
app.websockets = set()
|
||||||
app.segments_directory_cache = DirectoryCache(config['segments_dir'])
|
app.segments_directory_cache = DirectoryCache(config['stream']['segments_dir'])
|
||||||
|
|
||||||
async with app.app_context():
|
async with app.app_context():
|
||||||
import anonstream.routes
|
import anonstream.routes
|
||||||
|
|
|
@ -2,9 +2,21 @@ from datetime import datetime
|
||||||
|
|
||||||
from quart import escape
|
from quart import escape
|
||||||
|
|
||||||
def add_chat_message(chat, message_id, token, text):
|
class Rejected(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def broadcast(websockets, payload):
|
||||||
|
for queue in websockets:
|
||||||
|
await queue.put(payload)
|
||||||
|
|
||||||
|
async def add_chat_message(chat, websockets, token, message_id, comment):
|
||||||
|
# check message
|
||||||
|
if len(comment) == 0:
|
||||||
|
raise Rejected('Message was empty')
|
||||||
|
|
||||||
|
# add message
|
||||||
dt = datetime.utcnow()
|
dt = datetime.utcnow()
|
||||||
markup = escape(text)
|
markup = escape(comment)
|
||||||
chat[message_id] = {
|
chat[message_id] = {
|
||||||
'id': message_id,
|
'id': message_id,
|
||||||
'token': token,
|
'token': token,
|
||||||
|
@ -12,6 +24,19 @@ def add_chat_message(chat, message_id, token, text):
|
||||||
'date': dt.strftime('%Y-%m-%d'),
|
'date': dt.strftime('%Y-%m-%d'),
|
||||||
'time_minutes': dt.strftime('%H:%M'),
|
'time_minutes': dt.strftime('%H:%M'),
|
||||||
'time_seconds': dt.strftime('%H:%M:%S'),
|
'time_seconds': dt.strftime('%H:%M:%S'),
|
||||||
'nomarkup': text,
|
'nomarkup': comment,
|
||||||
'markup': markup,
|
'markup': markup,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# broadcast message to websockets
|
||||||
|
await broadcast(
|
||||||
|
websockets,
|
||||||
|
payload={
|
||||||
|
'type': 'chat',
|
||||||
|
'color': '#c7007f',
|
||||||
|
'name': 'Anonymous',
|
||||||
|
'markup': markup,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return markup
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from quart import current_app, request, render_template, make_response, redirect, websocket
|
from quart import current_app, request, render_template, make_response, redirect, websocket, url_for
|
||||||
|
|
||||||
from anonstream.stream import get_stream_title
|
from anonstream.stream import get_stream_title
|
||||||
from anonstream.segments import CatSegments, Offline
|
from anonstream.segments import CatSegments, Offline
|
||||||
from anonstream.users import get_default_name
|
from anonstream.users import get_default_name, add_notice, pop_notice
|
||||||
from anonstream.wrappers import with_user_from, auth_required
|
from anonstream.wrappers import with_user_from, auth_required
|
||||||
from anonstream.websocket import websocket_outbound, websocket_inbound
|
from anonstream.websocket import websocket_outbound, websocket_inbound
|
||||||
|
from anonstream.chat import add_chat_message, Rejected
|
||||||
|
from anonstream.utils.chat import create_message, generate_nonce, NonceReuse
|
||||||
|
|
||||||
@current_app.route('/')
|
@current_app.route('/')
|
||||||
@with_user_from(request)
|
@with_user_from(request)
|
||||||
|
@ -69,8 +71,51 @@ async def nojs_chat(user):
|
||||||
@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):
|
||||||
|
notice_id = request.args.get('notice', type=int)
|
||||||
|
prefer_chat_form = request.args.get('landing') != 'appearance'
|
||||||
return await render_template(
|
return await render_template(
|
||||||
'nojs_form.html',
|
'nojs_form.html',
|
||||||
user=user,
|
user=user,
|
||||||
|
notice=pop_notice(user, notice_id),
|
||||||
|
prefer_chat_form=prefer_chat_form,
|
||||||
|
nonce=generate_nonce(),
|
||||||
default_name=get_default_name(user),
|
default_name=get_default_name(user),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@current_app.post('/chat/message')
|
||||||
|
@with_user_from(request)
|
||||||
|
async def nojs_submit_message(user):
|
||||||
|
form = await request.form
|
||||||
|
comment = form.get('comment', '')
|
||||||
|
nonce = form.get('nonce', '')
|
||||||
|
|
||||||
|
try:
|
||||||
|
message_id, _, _ = create_message(
|
||||||
|
message_ids=current_app.chat.keys(),
|
||||||
|
secret=current_app.config['SECRET_KEY'],
|
||||||
|
nonce=nonce,
|
||||||
|
comment=comment,
|
||||||
|
)
|
||||||
|
except NonceReuse:
|
||||||
|
notice_id = add_notice(user, 'Discarded suspected duplicate message')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
await add_chat_message(
|
||||||
|
current_app.chat,
|
||||||
|
current_app.websockets,
|
||||||
|
user['token'],
|
||||||
|
message_id,
|
||||||
|
comment
|
||||||
|
)
|
||||||
|
except Rejected as e:
|
||||||
|
notice, *_ = e.args
|
||||||
|
notice_id = add_notice(user, notice)
|
||||||
|
else:
|
||||||
|
notice_id = None
|
||||||
|
|
||||||
|
return redirect(url_for('nojs_form', token=user['token'], notice=notice_id))
|
||||||
|
|
||||||
|
@current_app.post('/chat/appearance')
|
||||||
|
@with_user_from(request)
|
||||||
|
async def nojs_submit_appearance(user):
|
||||||
|
pass
|
||||||
|
|
|
@ -8,7 +8,7 @@ const jsmarkup_chat_messages = '<ul id="chat-messages_js" data-js="true"></ul>';
|
||||||
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="">
|
||||||
<textarea id="chat-form_js__message" name="message" 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"></textarea>
|
||||||
<div id="chat-live">
|
<div id="chat-live">
|
||||||
<span id="chat-live__ball"></span>
|
<span id="chat-live__ball"></span>
|
||||||
<span id="chat-live__status">Not connected to chat</span>
|
<span id="chat-live__status">Not connected to chat</span>
|
||||||
|
@ -45,24 +45,24 @@ const on_websocket_message = (event) => {
|
||||||
const receipt = JSON.parse(event.data);
|
const receipt = JSON.parse(event.data);
|
||||||
switch (receipt.type) {
|
switch (receipt.type) {
|
||||||
case "error":
|
case "error":
|
||||||
console.log("server sent error via websocket", receipt);
|
console.log("ws error", receipt);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "init":
|
case "init":
|
||||||
console.log("init", receipt);
|
console.log("ws init", receipt);
|
||||||
chat_form_nonce.value = receipt.nonce;
|
chat_form_nonce.value = receipt.nonce;
|
||||||
info_title.innerText = receipt.title;
|
info_title.innerText = receipt.title;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "title":
|
case "title":
|
||||||
console.log("title", receipt);
|
console.log("ws title", receipt);
|
||||||
info_title.innerText = receipt.title;
|
info_title.innerText = receipt.title;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "ack":
|
case "ack":
|
||||||
console.log("ack", receipt);
|
console.log("ws ack", receipt);
|
||||||
if (chat_form_nonce.value === receipt.nonce) {
|
if (chat_form_nonce.value === receipt.nonce) {
|
||||||
chat_form_message.value = "";
|
chat_form_comment.value = "";
|
||||||
} else {
|
} else {
|
||||||
console.log("nonce does not match ack", chat_form_nonce, receipt);
|
console.log("nonce does not match ack", chat_form_nonce, receipt);
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,15 @@ const on_websocket_message = (event) => {
|
||||||
chat_form_nonce.value = receipt.next;
|
chat_form_nonce.value = receipt.next;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "reject":
|
||||||
|
console.log("ws reject", receipt);
|
||||||
|
alert(`Rejected: ${receipt.notice}`);
|
||||||
|
chat_form_submit.disabled = false;
|
||||||
|
break;
|
||||||
|
|
||||||
case "chat":
|
case "chat":
|
||||||
|
console.log("ws chat", receipt);
|
||||||
|
|
||||||
const chat_message = document.createElement("li");
|
const chat_message = document.createElement("li");
|
||||||
chat_message.classList.add("chat-message");
|
chat_message.classList.add("chat-message");
|
||||||
|
|
||||||
|
@ -80,13 +88,13 @@ const on_websocket_message = (event) => {
|
||||||
//chat_message_name.dataset.color = receipt.color; // not working in any browser
|
//chat_message_name.dataset.color = receipt.color; // not working in any browser
|
||||||
chat_message_name.style.color = receipt.color;
|
chat_message_name.style.color = receipt.color;
|
||||||
|
|
||||||
const chat_message_text = document.createElement("span");
|
const chat_message_markup = document.createElement("span");
|
||||||
chat_message_text.classList.add("chat-message__text");
|
chat_message_markup.classList.add("chat-message__markup");
|
||||||
chat_message_text.innerText = receipt.text;
|
chat_message_markup.innerHTML = receipt.markup;
|
||||||
|
|
||||||
chat_message.insertAdjacentElement("beforeend", chat_message_name);
|
chat_message.insertAdjacentElement("beforeend", chat_message_name);
|
||||||
chat_message.insertAdjacentHTML("beforeend", ": ");
|
chat_message.insertAdjacentHTML("beforeend", ": ");
|
||||||
chat_message.insertAdjacentElement("beforeend", chat_message_text);
|
chat_message.insertAdjacentElement("beforeend", chat_message_markup);
|
||||||
|
|
||||||
chat_messages.insertAdjacentElement("beforeend", chat_message);
|
chat_messages.insertAdjacentElement("beforeend", chat_message);
|
||||||
chat_messages_parent.scrollTo({
|
chat_messages_parent.scrollTo({
|
||||||
|
@ -95,11 +103,10 @@ const on_websocket_message = (event) => {
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("chat", receipt);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log("incomprehensible websocket message", message);
|
console.log("incomprehensible websocket message", receipt);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const chat_live_ball = document.getElementById("chat-live__ball");
|
const chat_live_ball = document.getElementById("chat-live__ball");
|
||||||
|
@ -145,11 +152,11 @@ connect_websocket();
|
||||||
/* override js-only chat form */
|
/* override js-only chat form */
|
||||||
const chat_form = document.getElementById("chat-form_js");
|
const chat_form = document.getElementById("chat-form_js");
|
||||||
const chat_form_nonce = document.getElementById("chat-form_js__nonce");
|
const chat_form_nonce = document.getElementById("chat-form_js__nonce");
|
||||||
const chat_form_message = document.getElementById("chat-form_js__message");
|
const chat_form_comment = document.getElementById("chat-form_js__comment");
|
||||||
const chat_form_submit = document.getElementById("chat-form_js__submit");
|
const chat_form_submit = document.getElementById("chat-form_js__submit");
|
||||||
chat_form.addEventListener("submit", (event) => {
|
chat_form.addEventListener("submit", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const payload = {message: chat_form_message.value, nonce: chat_form_nonce.value};
|
const payload = {comment: chat_form_comment.value, nonce: chat_form_nonce.value};
|
||||||
chat_form_submit.disabled = true;
|
chat_form_submit.disabled = true;
|
||||||
ws.send(JSON.stringify(payload));
|
ws.send(JSON.stringify(payload));
|
||||||
});
|
});
|
||||||
|
|
|
@ -95,7 +95,7 @@ noscript {
|
||||||
#chat-form_js__submit {
|
#chat-form_js__submit {
|
||||||
grid-column: 2 / span 1;
|
grid-column: 2 / span 1;
|
||||||
}
|
}
|
||||||
#chat-form_js__message {
|
#chat-form_js__comment {
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
background-color: #434347;
|
background-color: #434347;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
@ -107,10 +107,10 @@ noscript {
|
||||||
color: #c3c3c7;
|
color: #c3c3c7;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
#chat-form_js__message:not(:focus):hover {
|
#chat-form_js__comment:not(:focus):hover {
|
||||||
border-color: #737377;
|
border-color: #737377;
|
||||||
}
|
}
|
||||||
#chat-form_js__message:focus {
|
#chat-form_js__comment:focus {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
border-color: #3584e4;
|
border-color: #3584e4;
|
||||||
}
|
}
|
||||||
|
@ -149,7 +149,7 @@ noscript {
|
||||||
color: attr("data-color");
|
color: attr("data-color");
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
.chat-message__text {
|
.chat-message__markup {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
line-height: 1.3125;
|
line-height: 1.3125;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
--link-color: #42a5d7;
|
||||||
--padding: 1ch;
|
--padding: 1ch;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
|
@ -12,20 +13,29 @@
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
color: #42a5d7;
|
color: var(--link-color);
|
||||||
}
|
}
|
||||||
label {
|
.pseudolink {
|
||||||
|
color: var(--link-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.pseudolink:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
.tripcode {
|
.tripcode {
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
align-self: center;
|
cursor: default;
|
||||||
|
}
|
||||||
|
#tripcode {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#notice {
|
#notice {
|
||||||
padding: var(--margin);
|
display: grid;
|
||||||
|
padding: var(--padding);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -36,6 +46,7 @@
|
||||||
#notice h1 {
|
#notice h1 {
|
||||||
font-size: 18pt;
|
font-size: 18pt;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-form, #appearance-form {
|
#chat-form, #appearance-form {
|
||||||
|
@ -49,7 +60,7 @@
|
||||||
grid: auto 2rem / auto 8ch;
|
grid: auto 2rem / auto 8ch;
|
||||||
grid-gap: 0.75ch;
|
grid-gap: 0.75ch;
|
||||||
}
|
}
|
||||||
#chat-form__message {
|
#chat-form__comment {
|
||||||
resize: none;
|
resize: none;
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
background-color: #434347;
|
background-color: #434347;
|
||||||
|
@ -59,10 +70,10 @@
|
||||||
padding: 1.5ch;
|
padding: 1.5ch;
|
||||||
color: #c3c3c7;
|
color: #c3c3c7;
|
||||||
}
|
}
|
||||||
#chat-form__message:not(:focus):hover {
|
#chat-form__comment:not(:focus):hover {
|
||||||
border-color: #737377;
|
border-color: #737377;
|
||||||
}
|
}
|
||||||
#chat-form__message:focus {
|
#chat-form__comment:focus {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
border-color: #3584e4;
|
border-color: #3584e4;
|
||||||
}
|
}
|
||||||
|
@ -114,38 +125,48 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#notice, #appearance-form {
|
#appearance-form {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#anchor:target > #chat-form {
|
#appearance:target ~ #appearance-form {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#anchor:target > #appearance-form {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
#notice:target {
|
#appearance:target ~ #chat-form {
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
#notice:target + #chat-form,
|
|
||||||
#notice:target ~ #appearance-form {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
#chat:target ~ #appearance-form {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
{% if notice != none %}
|
||||||
|
#chat-form {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#chat:target ~ #chat-form {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
#chat:target ~ #notice,
|
||||||
|
#appearance:target ~ #notice {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body id="anchor">
|
<body>
|
||||||
<a href="#chat-form" id="notice">
|
<div id="chat"></div>
|
||||||
<header><h1>No notice</h1></header>
|
<div id="appearance"></div>
|
||||||
<small>Click to dismiss</small>
|
{% if notice != none %}
|
||||||
</a>
|
<a id="notice" {% if prefer_chat_form %}href="#chat"{% else %}href="#appearance"{% endif %}>
|
||||||
</article>
|
<header><h1>{{ notice }}</h1></header>
|
||||||
<form id="chat-form" action="/chat/message">
|
<small>Click to dismiss</small>
|
||||||
<input type="hidden" name="token" value="{{ user.token }}">
|
</a>
|
||||||
<textarea id="chat-form__message" name="text" placeholder="Send a message..." required rows="1"></textarea>
|
{% endif %}
|
||||||
<div id="chat-form__exit"><a href="#anchor">Settings</a></div>
|
<form id="chat-form" action="{{ url_for('nojs_submit_message', token=user.token) }}" method="post">
|
||||||
<input id="chat-form__submit" type="submit" value="Chat">
|
<input type="hidden" name="nonce" value="{{ nonce }}">
|
||||||
|
<textarea id="chat-form__comment" name="comment" placeholder="Send a message..." required rows="1" tabindex="1"></textarea>
|
||||||
|
<div id="chat-form__exit"><a href="#appearance">Settings</a></div>
|
||||||
|
<input id="chat-form__submit" type="submit" value="Chat" tabindex="2">
|
||||||
</form>
|
</form>
|
||||||
<form id="appearance-form" action="/chat/appearance">
|
<form id="appearance-form" action="/{{ url_for('nojs_submit_appearance', token=user.token) }}" method="post">
|
||||||
<input type="hidden" name="token" value="{{ user.token }}">
|
|
||||||
<label for="appearance-form__name">Name:</label>
|
<label for="appearance-form__name">Name:</label>
|
||||||
<input id="appearance-form__name" name="name" value="{{ user.name or '' }}" placeholder="{{ user.name or default_name }}">
|
<input id="appearance-form__name" name="name" value="{{ user.name or '' }}" placeholder="{{ user.name or default_name }}">
|
||||||
<label id="appearance-form__label-password" for="appearance-form__password">Tripcode:</label>
|
<label id="appearance-form__label-password" for="appearance-form__password">Tripcode:</label>
|
||||||
|
@ -153,18 +174,19 @@
|
||||||
<input id="cleared-toggle" name="clear-tripcode" type="checkbox">
|
<input id="cleared-toggle" name="clear-tripcode" type="checkbox">
|
||||||
<div id="password-column">
|
<div id="password-column">
|
||||||
{% if user.tripcode == none %}
|
{% if user.tripcode == none %}
|
||||||
<label class="show-password tripcode" for="password-toggle">(no tripcode)</label>
|
<span class="tripcode">(no tripcode)</span>
|
||||||
|
<label for="password-toggle" class="show-password pseudolink" accesskey="s">set</label>
|
||||||
{% else %}
|
{% else %}
|
||||||
<label id="tripcode" for="password-toggle" class="show-password tripcode" style="background-color:{{ user.tripcode.background }};color:{{ user.tripcode.foreground }};">{{ user.tripcode.digest }}</label>
|
<label id="tripcode" for="password-toggle" class="show-password tripcode" style="background-color:{{ user.tripcode.background }};color:{{ user.tripcode.foreground }};" accesskey="s">{{ user.tripcode.digest }}digest</label>
|
||||||
<label id="show-cleared" for="cleared-toggle">✗</label>
|
<label id="show-cleared" for="cleared-toggle" class="pseudolink" accesskey="c">✗</label>
|
||||||
<div id="cleared" class="tripcode">(cleared)</div>
|
<div id="cleared" class="tripcode">(cleared)</div>
|
||||||
<label id="hide-cleared" for="cleared-toggle">undo</label>
|
<label id="hide-cleared" for="cleared-toggle" class="pseudolink" accesskey="c">undo</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<input id="appearance-form__password" name="password" type="password"></div>
|
<input id="appearance-form__password" name="password" type="password" placeholder="(tripcode password)">
|
||||||
<label id="hide-password" for="password-toggle">✗</label>
|
<label id="hide-password" for="password-toggle" class="pseudolink" accesskey="s">✗</label>
|
||||||
<div id="appearance-form__buttons">
|
<div id="appearance-form__buttons">
|
||||||
<div><a href="#chat-form">Return to chat</a></div>
|
<div><a href="#chat">Return to chat</a></div>
|
||||||
<input type="submit" value="Update">
|
<input type="submit" value="Update">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import time
|
||||||
|
|
||||||
from quart import current_app
|
from quart import current_app
|
||||||
|
|
||||||
def get_default_name(user):
|
def get_default_name(user):
|
||||||
|
@ -7,3 +9,16 @@ def get_default_name(user):
|
||||||
current_app.config['DEFAULT_ANON_NAME']
|
current_app.config['DEFAULT_ANON_NAME']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def add_notice(user, notice):
|
||||||
|
notice_id = time.time_ns() // 1_000_000
|
||||||
|
user['notices'][notice_id] = notice
|
||||||
|
if len(user['notices']) > current_app.config['LIMIT_NOTICES']:
|
||||||
|
user['notices'].popitem(last=False)
|
||||||
|
return notice_id
|
||||||
|
|
||||||
|
def pop_notice(user, notice_id):
|
||||||
|
try:
|
||||||
|
notice = user['notices'].pop(notice_id)
|
||||||
|
except KeyError:
|
||||||
|
notice = None
|
||||||
|
return notice
|
||||||
|
|
|
@ -2,6 +2,9 @@ import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
|
class NonceReuse(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
def generate_nonce():
|
def generate_nonce():
|
||||||
return secrets.token_urlsafe(16)
|
return secrets.token_urlsafe(16)
|
||||||
|
|
||||||
|
@ -9,3 +12,9 @@ def generate_message_id(secret, nonce):
|
||||||
parts = secret + b'message-id\0' + nonce.encode()
|
parts = secret + b'message-id\0' + nonce.encode()
|
||||||
digest = hashlib.sha256(parts).digest()
|
digest = hashlib.sha256(parts).digest()
|
||||||
return base64.urlsafe_b64encode(digest)[:22].decode()
|
return base64.urlsafe_b64encode(digest)[:22].decode()
|
||||||
|
|
||||||
|
def create_message(message_ids, secret, nonce, comment):
|
||||||
|
message_id = generate_message_id(secret, nonce)
|
||||||
|
if message_id in message_ids:
|
||||||
|
raise NonceReuse
|
||||||
|
return message_id, nonce, comment
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import secrets
|
import secrets
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
def generate_token():
|
def generate_token():
|
||||||
return secrets.token_hex(16)
|
return secrets.token_hex(16)
|
||||||
|
@ -9,6 +10,7 @@ def generate_user(token, broadcaster, timestamp):
|
||||||
'broadcaster': broadcaster,
|
'broadcaster': broadcaster,
|
||||||
'name': None,
|
'name': None,
|
||||||
'tripcode': None,
|
'tripcode': None,
|
||||||
|
'notices': OrderedDict(),
|
||||||
'seen': {
|
'seen': {
|
||||||
'first': timestamp,
|
'first': timestamp,
|
||||||
'last': timestamp,
|
'last': timestamp,
|
||||||
|
|
|
@ -1,19 +1,23 @@
|
||||||
from anonstream.utils.chat import generate_message_id
|
from anonstream.utils.chat import create_message, NonceReuse
|
||||||
|
|
||||||
def parse(message_ids, secret, receipt):
|
class Malformed(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def parse_websocket_data(message_ids, secret, receipt):
|
||||||
if not isinstance(receipt, dict):
|
if not isinstance(receipt, dict):
|
||||||
return None, 'not a json object'
|
raise Malformed('not a json object')
|
||||||
|
|
||||||
message = receipt.get('message')
|
comment = receipt.get('comment')
|
||||||
if not isinstance(message, str):
|
if not isinstance(comment, str):
|
||||||
return None, 'malformed chat message'
|
raise Malformed('malformed comment')
|
||||||
|
|
||||||
nonce = receipt.get('nonce')
|
nonce = receipt.get('nonce')
|
||||||
if not isinstance(nonce, str):
|
if not isinstance(nonce, str):
|
||||||
return None, 'malformed nonce'
|
raise Malformed('malformed nonce')
|
||||||
|
|
||||||
message_id = generate_message_id(secret, nonce)
|
try:
|
||||||
if message_id in message_ids:
|
message = create_message(message_ids, secret, nonce, comment)
|
||||||
return None, 'nonce already used'
|
except NonceReuse:
|
||||||
|
raise Malformed('nonce already used')
|
||||||
|
|
||||||
return (message, nonce, message_id), None
|
return message
|
||||||
|
|
|
@ -3,9 +3,9 @@ import asyncio
|
||||||
from quart import websocket
|
from quart import websocket
|
||||||
|
|
||||||
from anonstream.stream import get_stream_title, get_stream_uptime
|
from anonstream.stream import get_stream_title, get_stream_uptime
|
||||||
from anonstream.chat import add_chat_message
|
from anonstream.chat import broadcast, add_chat_message, Rejected
|
||||||
from anonstream.utils.chat import generate_nonce
|
from anonstream.utils.chat import generate_nonce
|
||||||
from anonstream.utils.websocket import parse
|
from anonstream.utils.websocket import parse_websocket_data
|
||||||
|
|
||||||
async def websocket_outbound(queue):
|
async def websocket_outbound(queue):
|
||||||
payload = {
|
payload = {
|
||||||
|
@ -23,28 +23,33 @@ async def websocket_outbound(queue):
|
||||||
async def websocket_inbound(queue, connected_websockets, token, secret, chat):
|
async def websocket_inbound(queue, connected_websockets, token, secret, chat):
|
||||||
while True:
|
while True:
|
||||||
receipt = await websocket.receive_json()
|
receipt = await websocket.receive_json()
|
||||||
receipt, error = parse(chat.keys(), secret, receipt)
|
try:
|
||||||
if error is not None:
|
message_id, nonce, comment = parse_websocket_data(chat.keys(), secret, receipt)
|
||||||
|
except Malformed as e:
|
||||||
|
error , *_ = e.args
|
||||||
payload = {
|
payload = {
|
||||||
'type': 'error',
|
'type': 'error',
|
||||||
'because': error,
|
'because': error,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
text, nonce, message_id = receipt
|
try:
|
||||||
add_chat_message(chat, message_id, token, text)
|
markup = await add_chat_message(
|
||||||
payload = {
|
chat,
|
||||||
'type': 'ack',
|
connected_websockets,
|
||||||
'nonce': nonce,
|
token,
|
||||||
'next': generate_nonce(),
|
message_id,
|
||||||
}
|
comment
|
||||||
|
)
|
||||||
|
except Rejected as e:
|
||||||
|
notice, *_ = e.args
|
||||||
|
payload = {
|
||||||
|
'type': 'reject',
|
||||||
|
'notice': notice,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
payload = {
|
||||||
|
'type': 'ack',
|
||||||
|
'nonce': nonce,
|
||||||
|
'next': generate_nonce(),
|
||||||
|
}
|
||||||
await queue.put(payload)
|
await queue.put(payload)
|
||||||
|
|
||||||
if error is None:
|
|
||||||
payload = {
|
|
||||||
'type': 'chat',
|
|
||||||
'color': '#c7007f',
|
|
||||||
'name': 'Anonymous',
|
|
||||||
'text': text,
|
|
||||||
}
|
|
||||||
for other_queue in connected_websockets:
|
|
||||||
await other_queue.put(payload)
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ def with_user_from(context):
|
||||||
user['seen']['last'] = timestamp
|
user['seen']['last'] = timestamp
|
||||||
else:
|
else:
|
||||||
user = generate_user(token, broadcaster, timestamp)
|
user = generate_user(token, broadcaster, timestamp)
|
||||||
|
current_app.users[token] = user
|
||||||
return await f(user, *args, **kwargs)
|
return await f(user, *args, **kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
15
config.toml
15
config.toml
|
@ -1,5 +1,14 @@
|
||||||
secret_key = "test"
|
secret_key = "test"
|
||||||
auth_username = "broadcaster"
|
|
||||||
|
[auth]
|
||||||
|
username = "broadcaster"
|
||||||
|
|
||||||
|
[stream]
|
||||||
segments_dir = "stream/"
|
segments_dir = "stream/"
|
||||||
default_host_name = "Broadcaster"
|
|
||||||
default_anon_name = "Anonymous"
|
[names]
|
||||||
|
broadcaster = "Broadcaster"
|
||||||
|
anonymous = "Anonymous"
|
||||||
|
|
||||||
|
[limits]
|
||||||
|
notices = 32
|
||||||
|
|
読み込み中…
新しいイシューから参照