Initial noscript markup for chat form & stream info
Use with-user decorator on routes (instead of with-token)
このコミットが含まれているのは:
コミット
885c10b5de
|
@ -5,7 +5,7 @@ from collections import OrderedDict
|
|||
from quart import Quart
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
from anonstream.utils.token import generate_token
|
||||
from anonstream.utils.users import generate_token
|
||||
from anonstream.segments import DirectoryCache
|
||||
|
||||
async def create_app():
|
||||
|
@ -22,8 +22,11 @@ async def create_app():
|
|||
app.config['AUTH_USERNAME'] = config['auth_username']
|
||||
app.config['AUTH_PWHASH'] = auth_pwhash
|
||||
app.config['AUTH_TOKEN'] = generate_token()
|
||||
app.config['DEFAULT_HOST_NAME'] = config['default_host_name']
|
||||
app.config['DEFAULT_ANON_NAME'] = config['default_anon_name']
|
||||
app.chat = OrderedDict()
|
||||
app.websockets = {}
|
||||
app.users = {}
|
||||
app.websockets = set()
|
||||
app.segments_directory_cache = DirectoryCache(config['segments_dir'])
|
||||
|
||||
async with app.app_context():
|
||||
|
|
|
@ -2,21 +2,26 @@ import asyncio
|
|||
|
||||
from quart import current_app, request, render_template, make_response, redirect, websocket
|
||||
|
||||
from anonstream.segments import CatSegments
|
||||
from anonstream.wrappers import with_token_from, auth_required
|
||||
from anonstream.stream import get_stream_title
|
||||
from anonstream.segments import CatSegments, Offline
|
||||
from anonstream.users import get_default_name
|
||||
from anonstream.wrappers import with_user_from, auth_required
|
||||
from anonstream.websocket import websocket_outbound, websocket_inbound
|
||||
|
||||
@current_app.route('/')
|
||||
@with_token_from(request)
|
||||
async def home(token):
|
||||
return await render_template('home.html', token=token)
|
||||
@with_user_from(request)
|
||||
async def home(user):
|
||||
return await render_template('home.html', user=user)
|
||||
|
||||
@current_app.route('/stream.mp4')
|
||||
@with_token_from(request)
|
||||
async def stream(token):
|
||||
@with_user_from(request)
|
||||
async def stream(user):
|
||||
try:
|
||||
cat_segments = CatSegments(current_app.segments_directory_cache, token)
|
||||
except ValueError:
|
||||
cat_segments = CatSegments(
|
||||
directory_cache=current_app.segments_directory_cache,
|
||||
token=user['token']
|
||||
)
|
||||
except Offline:
|
||||
return 'offline', 404
|
||||
response = await make_response(cat_segments.stream())
|
||||
response.headers['Content-Type'] = 'video/mp4'
|
||||
|
@ -29,19 +34,43 @@ async def login():
|
|||
return redirect('/')
|
||||
|
||||
@current_app.websocket('/live')
|
||||
@with_token_from(websocket)
|
||||
async def live(token):
|
||||
@with_user_from(websocket)
|
||||
async def live(user):
|
||||
queue = asyncio.Queue()
|
||||
current_app.websockets[token] = queue
|
||||
current_app.websockets.add(queue)
|
||||
|
||||
producer = websocket_outbound(queue)
|
||||
consumer = websocket_inbound(
|
||||
queue=queue,
|
||||
connected_websockets=current_app.websockets,
|
||||
token=token,
|
||||
token=user['token'],
|
||||
secret=current_app.config['SECRET_KEY'],
|
||||
chat=current_app.chat,
|
||||
)
|
||||
try:
|
||||
await asyncio.gather(producer, consumer)
|
||||
finally:
|
||||
current_app.websockets.pop(token)
|
||||
current_app.websockets.remove(queue)
|
||||
|
||||
@current_app.route('/info.html')
|
||||
@with_user_from(request)
|
||||
async def nojs_info(user):
|
||||
return await render_template(
|
||||
'nojs_info.html',
|
||||
user=user,
|
||||
title=get_stream_title(),
|
||||
)
|
||||
|
||||
@current_app.route('/chat/messages.html')
|
||||
@with_user_from(request)
|
||||
async def nojs_chat(user):
|
||||
return await render_template('nojs_chat.html', user=user)
|
||||
|
||||
@current_app.route('/chat/form.html')
|
||||
@with_user_from(request)
|
||||
async def nojs_form(user):
|
||||
return await render_template(
|
||||
'nojs_form.html',
|
||||
user=user,
|
||||
default_name=get_default_name(user),
|
||||
)
|
||||
|
|
|
@ -8,8 +8,11 @@ import aiofiles
|
|||
|
||||
RE_SEGMENT = re.compile(r'^(?P<index>\d+)\.ts$')
|
||||
|
||||
class Offline(Exception):
|
||||
pass
|
||||
|
||||
class DirectoryCache:
|
||||
def __init__(self, directory, ttl=0.5):
|
||||
def __init__(self, directory, ttl=1.0):
|
||||
self.directory = directory
|
||||
self.ttl = ttl
|
||||
self.expires = None
|
||||
|
@ -41,7 +44,10 @@ class CatSegments:
|
|||
def __init__(self, directory_cache, token):
|
||||
self.directory_cache = directory_cache
|
||||
self.token = token
|
||||
self.index = max(self.directory_cache.segments())
|
||||
try:
|
||||
self.index = max(self.directory_cache.segments())
|
||||
except ValueError: # max of empty sequence, i.e. there are no segments
|
||||
raise Offline
|
||||
|
||||
async def stream(self):
|
||||
while True:
|
||||
|
|
|
@ -1,27 +1,35 @@
|
|||
/* token */
|
||||
const token = document.querySelector("body").dataset.token;
|
||||
|
||||
/* insert js-only markup */
|
||||
const jsmarkup_info_title = '<header id="info__title" data-js="true"></header>';
|
||||
const jsmarkup_chat_messages = '<ul id="chat-messages" data-js="true"></ul>';
|
||||
const jsmarkup_info = '<div id="info_js"></div>';
|
||||
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_form = `\
|
||||
<form id="chat-form" data-js="true" action="/chat" method="post">
|
||||
<input id="chat-form__nonce" type="hidden" name="nonce" value="">
|
||||
<textarea id="chat-form__message" name="message" maxlength="512" required placeholder="Send a message..." rows="1"></textarea>
|
||||
<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__message" name="message" maxlength="512" required placeholder="Send a message..." rows="1"></textarea>
|
||||
<div id="chat-live">
|
||||
<span id="chat-live__ball"></span>
|
||||
<span id="chat-live__status">Not connected to chat</span>
|
||||
</div>
|
||||
<input id="chat-form__submit" type="submit" value="Chat" disabled>
|
||||
<input id="chat-form_js__submit" type="submit" value="Chat" disabled>
|
||||
</form>`;
|
||||
|
||||
const insert_jsmarkup = () => {
|
||||
if (document.getElementById("info__title") === null) {
|
||||
if (document.getElementById("info_js") === null) {
|
||||
const parent = document.getElementById("info");
|
||||
parent.insertAdjacentHTML("beforeend", jsmarkup_info);
|
||||
}
|
||||
if (document.getElementById("info_js__title") === null) {
|
||||
const parent = document.getElementById("info_js");
|
||||
parent.insertAdjacentHTML("beforeend", jsmarkup_info_title);
|
||||
}
|
||||
if (document.getElementById("chat-messages") === null) {
|
||||
if (document.getElementById("chat-messages_js") === null) {
|
||||
const parent = document.getElementById("chat__messages");
|
||||
parent.insertAdjacentHTML("beforeend", jsmarkup_chat_messages);
|
||||
}
|
||||
if (document.getElementById("chat-form") === null) {
|
||||
if (document.getElementById("chat-form_js") === null) {
|
||||
const parent = document.getElementById("chat__form");
|
||||
parent.insertAdjacentHTML("beforeend", jsmarkup_chat_form);
|
||||
}
|
||||
|
@ -30,9 +38,9 @@ const insert_jsmarkup = () => {
|
|||
insert_jsmarkup();
|
||||
|
||||
/* create websocket */
|
||||
const info_title = document.getElementById("info__title");
|
||||
const info_title = document.getElementById("info_js__title");
|
||||
const chat_messages_parent = document.getElementById("chat__messages");
|
||||
const chat_messages = document.getElementById("chat-messages");
|
||||
const chat_messages = document.getElementById("chat-messages_js");
|
||||
const on_websocket_message = (event) => {
|
||||
const receipt = JSON.parse(event.data);
|
||||
switch (receipt.type) {
|
||||
|
@ -69,7 +77,8 @@ const on_websocket_message = (event) => {
|
|||
const chat_message_name = document.createElement("span");
|
||||
chat_message_name.classList.add("chat-message__name");
|
||||
chat_message_name.innerText = receipt.name;
|
||||
chat_message_name.style.color = receipt.color
|
||||
//chat_message_name.dataset.color = receipt.color; // not working in any browser
|
||||
chat_message_name.style.color = receipt.color;
|
||||
|
||||
const chat_message_text = document.createElement("span");
|
||||
chat_message_text.classList.add("chat-message__text");
|
||||
|
@ -104,7 +113,7 @@ const connect_websocket = () => {
|
|||
}
|
||||
chat_live_ball.style.borderColor = "gold";
|
||||
chat_live_status.innerText = "Connecting to chat...";
|
||||
ws = new WebSocket(`ws://${document.domain}:${location.port}/live`);
|
||||
ws = new WebSocket(`ws://${document.domain}:${location.port}/live?token=${encodeURIComponent(token)}`);
|
||||
ws.addEventListener("open", (event) => {
|
||||
chat_form_submit.disabled = false;
|
||||
chat_live_ball.style.borderColor = "green";
|
||||
|
@ -134,10 +143,10 @@ const connect_websocket = () => {
|
|||
connect_websocket();
|
||||
|
||||
/* override js-only chat form */
|
||||
const chat_form = document.getElementById("chat-form");
|
||||
const chat_form_nonce = document.getElementById("chat-form__nonce");
|
||||
const chat_form_message = document.getElementById("chat-form__message");
|
||||
const chat_form_submit = document.getElementById("chat-form__submit");
|
||||
const chat_form = document.getElementById("chat-form_js");
|
||||
const chat_form_nonce = document.getElementById("chat-form_js__nonce");
|
||||
const chat_form_message = document.getElementById("chat-form_js__message");
|
||||
const chat_form_submit = document.getElementById("chat-form_js__submit");
|
||||
chat_form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
const payload = {message: chat_form_message.value, nonce: chat_form_nonce.value};
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
|
||||
--pure-video-height: calc(100vw * var(--aspect-y) / var(--aspect-x));
|
||||
--video-height: max(144px, min(75vh, var(--pure-video-height)));
|
||||
|
||||
--button-height: 2rem;
|
||||
--nojs-info-height: 17ch;
|
||||
}
|
||||
|
||||
body {
|
||||
|
@ -35,6 +38,15 @@ body {
|
|||
a {
|
||||
color: #42a5d7;
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
border: 1px solid red;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
noscript {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#stream {
|
||||
background: black;
|
||||
|
@ -45,14 +57,18 @@ a {
|
|||
|
||||
#info {
|
||||
border-top: var(--main-border);
|
||||
box-sizing: border-box;
|
||||
padding: 0.75ch 1ch;
|
||||
overflow-y: scroll;
|
||||
grid-area: info;
|
||||
}
|
||||
#info__title {
|
||||
#info_js {
|
||||
overflow-y: auto;
|
||||
margin: 1ch 1.5ch;
|
||||
}
|
||||
#info_js__title {
|
||||
font-size: 18pt;
|
||||
}
|
||||
#info_nojs {
|
||||
height: var(--nojs-info-height);
|
||||
}
|
||||
|
||||
#chat {
|
||||
display: grid;
|
||||
|
@ -70,51 +86,57 @@ a {
|
|||
margin-bottom: 1ch;
|
||||
border-bottom: var(--chat-border);
|
||||
}
|
||||
#chat-form {
|
||||
#chat-form_js {
|
||||
display: grid;
|
||||
grid-template: auto 2rem / auto 8ch;
|
||||
grid-template: auto var(--button-height) / auto 8ch;
|
||||
grid-gap: 0.75ch;
|
||||
margin: 1ch;
|
||||
}
|
||||
#chat-form__submit {
|
||||
#chat-form_js__submit {
|
||||
grid-column: 2 / span 1;
|
||||
}
|
||||
#chat-form__message {
|
||||
#chat-form_js__message {
|
||||
grid-column: 1 / span 2;
|
||||
background-color: #434347;
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
transition: 0.25s;
|
||||
max-height: 16ch;
|
||||
min-height: 2.25ch;
|
||||
min-height: 1.75ch;
|
||||
padding: 1.5ch;
|
||||
color: #c3c3c7;
|
||||
resize: vertical;
|
||||
}
|
||||
#chat-form__message:not(:focus):hover {
|
||||
#chat-form_js__message:not(:focus):hover {
|
||||
border-color: #737377;
|
||||
}
|
||||
#chat-form__message:focus {
|
||||
#chat-form_js__message:focus {
|
||||
background-color: black;
|
||||
border-color: #3584e4;
|
||||
}
|
||||
#chat-form_nojs {
|
||||
height: 13ch;
|
||||
}
|
||||
#chat__messages {
|
||||
margin: 0 1ch;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
#chat-messages {
|
||||
#chat-messages_js {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 0 1ch;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
max-height: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
font-size: 11pt;
|
||||
}
|
||||
#chat-messages_nojs {
|
||||
height: 100%;
|
||||
}
|
||||
.chat-message {
|
||||
padding: 0.5ch 0.75ch ;
|
||||
padding: 0.5ch 0.75ch;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
|
@ -129,10 +151,11 @@ a {
|
|||
}
|
||||
.chat-message__text {
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.3125;
|
||||
}
|
||||
#chat-live {
|
||||
font-size: 9pt;
|
||||
line-height: 2rem;
|
||||
line-height: var(--button-height);
|
||||
}
|
||||
#chat-live__ball {
|
||||
border: 4px solid maroon;
|
||||
|
@ -186,6 +209,9 @@ footer {
|
|||
background-color: #3065a6;
|
||||
border-style: inset;
|
||||
}
|
||||
#both:target #info_nojs {
|
||||
height: 9ch;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
:root {
|
||||
|
@ -214,4 +240,7 @@ footer {
|
|||
border-left: var(--chat-border);
|
||||
min-height: 100vh;
|
||||
}
|
||||
#both:target #info_nojs {
|
||||
height: var(--nojs-info-height);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
def get_stream_title():
|
||||
return 'Stream title'
|
||||
|
||||
def get_stream_uptime():
|
||||
return None
|
|
@ -4,18 +4,18 @@
|
|||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="/static/style.css" type="text/css">
|
||||
</head>
|
||||
<body id="both" data-token="{{ token }}">
|
||||
<video id="stream" src="/stream.mp4" controls></video>
|
||||
<body id="both" data-token="{{ user.token }}">
|
||||
<video id="stream" src="{{ url_for('stream', token=user.token) }}" controls></video>
|
||||
<article id="info">
|
||||
<noscript><iframe id="info_js" data-js="false"></iframe></noscript>
|
||||
<noscript><iframe id="info_nojs" src="{{ url_for('nojs_info', token=user.token) }}" data-js="false"></iframe></noscript>
|
||||
</article>
|
||||
<aside id="chat">
|
||||
<header id="chat__header">Stream chat</header>
|
||||
<article id="chat__messages">
|
||||
<noscript><iframe id="chat-messages_nojs" data-js="false"></iframe></noscript>
|
||||
<noscript><iframe id="chat-messages_nojs" src="{{ url_for('nojs_chat', token=user.token) }}" data-js="false"></iframe></noscript>
|
||||
</article>
|
||||
<section id="chat__form">
|
||||
<noscript><iframe id="chat-form_nojs" data-js="false"></iframe></noscript>
|
||||
<noscript><iframe id="chat-form_nojs" src="{{ url_for('nojs_form', token=user.token, _anchor='notice') }}" data-js="false"></iframe></noscript>
|
||||
</section>
|
||||
</aside>
|
||||
<nav id="toggle">
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>#title {font-size: 18pt;}</style>
|
||||
</head>
|
||||
<body>
|
||||
<header id="title">{{ title }}</header>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,172 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
:root {
|
||||
--padding: 1ch;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
height: 13ch;
|
||||
color: white;
|
||||
}
|
||||
a {
|
||||
color: #42a5d7;
|
||||
}
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
.tripcode {
|
||||
padding: 0 4px;
|
||||
border-radius: 7px;
|
||||
font-family: monospace;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
#notice {
|
||||
padding: var(--margin);
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
}
|
||||
#notice h1 {
|
||||
font-size: 18pt;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#chat-form, #appearance-form {
|
||||
padding: var(--padding);
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
grid-gap: 0.75ch;
|
||||
}
|
||||
#chat-form {
|
||||
display: grid;
|
||||
grid: auto 2rem / auto 8ch;
|
||||
grid-gap: 0.75ch;
|
||||
}
|
||||
#chat-form__message {
|
||||
resize: none;
|
||||
grid-column: 1 / span 2;
|
||||
background-color: #434347;
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
transition: 0.25s;
|
||||
padding: 1.5ch;
|
||||
color: #c3c3c7;
|
||||
}
|
||||
#chat-form__message:not(:focus):hover {
|
||||
border-color: #737377;
|
||||
}
|
||||
#chat-form__message:focus {
|
||||
background-color: black;
|
||||
border-color: #3584e4;
|
||||
}
|
||||
|
||||
#appearance-form {
|
||||
grid-auto-rows: 1fr 1fr 2rem;
|
||||
grid-auto-columns: min-content 1fr min-content;
|
||||
}
|
||||
#appearance-form__label-password,
|
||||
#appearance-form__password,
|
||||
#password-column,
|
||||
#hide-password {
|
||||
grid-row: 2;
|
||||
}
|
||||
#appearance-form__buttons {
|
||||
grid-column: 1 / span 3;
|
||||
display: grid;
|
||||
grid-template-columns: auto 8ch;
|
||||
}
|
||||
|
||||
#password-toggle,
|
||||
#appearance-form__password {
|
||||
display: none;
|
||||
}
|
||||
#hide-password {
|
||||
visibility: hidden;
|
||||
}
|
||||
#password-toggle:checked + #cleared-toggle:not(:checked) ~ #appearance-form__password {
|
||||
display: inline;
|
||||
}
|
||||
#password-toggle:checked + #cleared-toggle:not(:checked) ~ #hide-password {
|
||||
visibility: visible;
|
||||
}
|
||||
#password-toggle:checked + #cleared-toggle:not(:checked) + #password-column {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#cleared-toggle,
|
||||
#cleared,
|
||||
#hide-cleared {
|
||||
display: none;
|
||||
}
|
||||
#cleared-toggle:checked + #password-column > #cleared,
|
||||
#cleared-toggle:checked + #password-column > #hide-cleared {
|
||||
display: inline;
|
||||
}
|
||||
#cleared-toggle:checked + #password-column > #tripcode,
|
||||
#cleared-toggle:checked + #password-column > #show-cleared {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#notice, #appearance-form {
|
||||
display: none;
|
||||
}
|
||||
#anchor:target > #chat-form {
|
||||
display: none;
|
||||
}
|
||||
#anchor:target > #appearance-form {
|
||||
display: grid;
|
||||
}
|
||||
#notice:target {
|
||||
display: grid;
|
||||
}
|
||||
#notice:target + #chat-form,
|
||||
#notice:target ~ #appearance-form {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body id="anchor">
|
||||
<a href="#chat-form" id="notice">
|
||||
<header><h1>No notice</h1></header>
|
||||
<small>Click to dismiss</small>
|
||||
</a>
|
||||
</article>
|
||||
<form id="chat-form" action="/chat/message">
|
||||
<input type="hidden" name="token" value="{{ user.token }}">
|
||||
<textarea id="chat-form__message" name="text" placeholder="Send a message..." required rows="1"></textarea>
|
||||
<div id="chat-form__exit"><a href="#anchor">Settings</a></div>
|
||||
<input id="chat-form__submit" type="submit" value="Chat">
|
||||
</form>
|
||||
<form id="appearance-form" action="/chat/appearance">
|
||||
<input type="hidden" name="token" value="{{ user.token }}">
|
||||
<label for="appearance-form__name">Name:</label>
|
||||
<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>
|
||||
<input id="password-toggle" name="set-tripcode" type="checkbox">
|
||||
<input id="cleared-toggle" name="clear-tripcode" type="checkbox">
|
||||
<div id="password-column">
|
||||
{% if user.tripcode == none %}
|
||||
<label class="show-password tripcode" for="password-toggle">(no tripcode)</label>
|
||||
{% 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="show-cleared" for="cleared-toggle">✗</label>
|
||||
<div id="cleared" class="tripcode">(cleared)</div>
|
||||
<label id="hide-cleared" for="cleared-toggle">undo</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
<input id="appearance-form__password" name="password" type="password"></div>
|
||||
<label id="hide-password" for="password-toggle">✗</label>
|
||||
<div id="appearance-form__buttons">
|
||||
<div><a href="#chat-form">Return to chat</a></div>
|
||||
<input type="submit" value="Update">
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,19 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
margin: 1ch 1.5ch;
|
||||
}
|
||||
#title {
|
||||
color: white;
|
||||
font-size: 18pt;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header id="title">{{ title }}</header>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
from quart import current_app
|
||||
|
||||
def get_default_name(user):
|
||||
return (
|
||||
current_app.config['DEFAULT_HOST_NAME']
|
||||
if user['broadcaster'] else
|
||||
current_app.config['DEFAULT_ANON_NAME']
|
||||
)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import secrets
|
||||
|
||||
def generate_token():
|
||||
return secrets.token_hex(16)
|
|
@ -0,0 +1,16 @@
|
|||
import secrets
|
||||
|
||||
def generate_token():
|
||||
return secrets.token_hex(16)
|
||||
|
||||
def generate_user(token, broadcaster, timestamp):
|
||||
return {
|
||||
'token': token,
|
||||
'broadcaster': broadcaster,
|
||||
'name': None,
|
||||
'tripcode': None,
|
||||
'seen': {
|
||||
'first': timestamp,
|
||||
'last': timestamp,
|
||||
},
|
||||
}
|
|
@ -2,6 +2,7 @@ import asyncio
|
|||
|
||||
from quart import websocket
|
||||
|
||||
from anonstream.stream import get_stream_title, get_stream_uptime
|
||||
from anonstream.chat import add_chat_message
|
||||
from anonstream.utils.chat import generate_nonce
|
||||
from anonstream.utils.websocket import parse
|
||||
|
@ -10,8 +11,8 @@ async def websocket_outbound(queue):
|
|||
payload = {
|
||||
'type': 'init',
|
||||
'nonce': generate_nonce(),
|
||||
'title': 'Stream title',
|
||||
'uptime': None,
|
||||
'title': get_stream_title(),
|
||||
'uptime': get_stream_uptime(),
|
||||
'chat': [],
|
||||
}
|
||||
await websocket.send_json(payload)
|
||||
|
@ -19,7 +20,7 @@ async def websocket_outbound(queue):
|
|||
payload = await queue.get()
|
||||
await websocket.send_json(payload)
|
||||
|
||||
async def websocket_inbound(connected_websockets, token, secret, chat):
|
||||
async def websocket_inbound(queue, connected_websockets, token, secret, chat):
|
||||
while True:
|
||||
receipt = await websocket.receive_json()
|
||||
receipt, error = parse(chat.keys(), secret, receipt)
|
||||
|
@ -36,7 +37,6 @@ async def websocket_inbound(connected_websockets, token, secret, chat):
|
|||
'nonce': nonce,
|
||||
'next': generate_nonce(),
|
||||
}
|
||||
queue = connected_websockets[token]
|
||||
await queue.put(payload)
|
||||
|
||||
if error is None:
|
||||
|
@ -46,5 +46,5 @@ async def websocket_inbound(connected_websockets, token, secret, chat):
|
|||
'name': 'Anonymous',
|
||||
'text': text,
|
||||
}
|
||||
for queue in connected_websockets.values():
|
||||
await queue.put(payload)
|
||||
for other_queue in connected_websockets:
|
||||
await other_queue.put(payload)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import time
|
||||
from functools import wraps
|
||||
|
||||
from quart import current_app, request, abort, make_response
|
||||
from werkzeug.security import check_password_hash
|
||||
|
||||
from anonstream.utils.token import generate_token
|
||||
from anonstream.utils.users import generate_token, generate_user
|
||||
|
||||
def check_auth(context):
|
||||
auth = context.authorization
|
||||
|
@ -31,15 +32,23 @@ def auth_required(f):
|
|||
|
||||
return wrapper
|
||||
|
||||
def with_token_from(context):
|
||||
def with_token_from_context(f):
|
||||
def with_user_from(context):
|
||||
def with_user_from_context(f):
|
||||
@wraps(f)
|
||||
async def wrapper(*args, **kwargs):
|
||||
if check_auth(context):
|
||||
broadcaster = check_auth(context)
|
||||
if broadcaster:
|
||||
token = current_app.config['AUTH_TOKEN']
|
||||
else:
|
||||
token = context.args.get('token') or generate_token()
|
||||
return await f(token, *args, **kwargs)
|
||||
timestamp = int(time.time())
|
||||
user = current_app.users.get(token)
|
||||
if user is not None:
|
||||
user['seen']['last'] = timestamp
|
||||
else:
|
||||
user = generate_user(token, broadcaster, timestamp)
|
||||
return await f(user, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
return with_token_from_context
|
||||
|
||||
return with_user_from_context
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
secret_key = "test"
|
||||
auth_username = "broadcaster"
|
||||
segments_dir = "stream/"
|
||||
default_host_name = "Broadcaster"
|
||||
default_anon_name = "Anonymous"
|
||||
|
|
読み込み中…
新しいイシューから参照