move host & anon default names into config.toml; add tripcode_salt for secure tripcodes; consider segments viewed when first sent; slightly longer stream timeout; check for segments more frequently; correct background colour; send segments only if they are of the current livestream; improve usability for text-based browsers; light backgrounds when hovering over comments and names
このコミットが含まれているのは:
コミット
08a0981daa
|
@ -6,5 +6,12 @@ fonts = [
|
|||
"/usr/share/fonts/truetype/tlwg/TlwgMono.ttf"
|
||||
]
|
||||
|
||||
[chat]
|
||||
host_default_name = "Broadcaster"
|
||||
anon_default_name = "Anonymous"
|
||||
|
||||
[secrets]
|
||||
tripcode_salt = "" # put a random value here for secure tripcodes
|
||||
|
||||
[stream]
|
||||
hls_time = 2
|
||||
|
|
|
@ -5,7 +5,7 @@ from website.utils.stream import _is_segment, _segment_number, get_segments, is_
|
|||
|
||||
SEGMENT = 'stream{number}.m4s'
|
||||
CORRUPTING_SEGMENT = 'corrupt.m4s'
|
||||
STREAM_TIMEOUT = lambda: CONFIG['stream']['hls_time'] * 2 # consider the stream offline after this many seconds without a new segment
|
||||
STREAM_TIMEOUT = lambda: CONFIG['stream']['hls_time'] * 2 + 2 # consider the stream offline after this many seconds without a new segment
|
||||
|
||||
def resolve_segment_offset(segment_offset=1):
|
||||
'''
|
||||
|
@ -23,7 +23,7 @@ def get_next_segment(after, start_segment):
|
|||
raise SegmentUnavailable(f'stream went offline')
|
||||
start = time.time()
|
||||
while True:
|
||||
time.sleep(1)
|
||||
time.sleep(0.5)
|
||||
segments = get_segments()
|
||||
if after == None:
|
||||
try:
|
||||
|
@ -70,7 +70,7 @@ class ConcatenatedSegments:
|
|||
def __init__(self, start_number, segment_hook=None, corrupt_hook=None, should_close_connection=None):
|
||||
# start at this segment, after SEGMENT_INIT
|
||||
self.start_number = start_number
|
||||
# run this function after sending each segment
|
||||
# run this function before sending each segment (if we do it after then if someone gets the most of a segment but then stops, that wouldn't be counted, before = 0 viewers means nobody is retrieving the stream, after = slightly more accurate viewer count but 0 viewers doesn't necessarily mean nobody is retrieving the stream)
|
||||
self.segment_hook = segment_hook or (lambda n: None)
|
||||
# run this function when we send the corrupting segment
|
||||
self.corrupt_hook = corrupt_hook or (lambda: None)
|
||||
|
@ -83,6 +83,7 @@ class ConcatenatedSegments:
|
|||
self._closed = False
|
||||
self.segment_read_offset = 0
|
||||
self.segment = next(self.segments)
|
||||
self.segment_hook(_segment_number(self.segment))
|
||||
|
||||
def _read(self, n):
|
||||
chunk = b''
|
||||
|
@ -104,14 +105,9 @@ class ConcatenatedSegments:
|
|||
break
|
||||
|
||||
self.segment_read_offset = 0
|
||||
try:
|
||||
next_segment = next(self.segments)
|
||||
except SegmentUnavailable:
|
||||
self.segment_hook(_segment_number(self.segment))
|
||||
raise
|
||||
else:
|
||||
self.segment_hook(_segment_number(self.segment))
|
||||
self.segment = next_segment
|
||||
next_segment = next(self.segments)
|
||||
self.segment_hook(_segment_number(next_segment))
|
||||
self.segment = next_segment
|
||||
return chunk
|
||||
|
||||
def read(self, n):
|
||||
|
|
|
@ -27,9 +27,6 @@ CHAT_TIMEOUT = 3 # seconds between chat messages
|
|||
FLOOD_PERIOD = 20 # seconds
|
||||
FLOOD_THRESHOLD = 4 # messages in FLOOD_PERIOD seconds
|
||||
|
||||
HOST_DEFAULT_NICKNAME = 'Broadcaster'
|
||||
ANON_DEFAULT_NICKNAME = 'Anonymous'
|
||||
|
||||
CHAT_MAX_STORAGE = 1024
|
||||
CHAT_SCROLLBACK = 100
|
||||
MESSAGE_MAX_LENGTH = 256
|
||||
|
@ -39,7 +36,7 @@ CAPTCHA_LIFETIME = 3600
|
|||
|
||||
VIEWER_ABSENT_THRESHOLD = 86400
|
||||
|
||||
BACKGROUND_COLOUR = b'\x22\x22\x22'
|
||||
BACKGROUND_COLOUR = b'\x23\x23\x23' # the same as in platform.css
|
||||
BROADCASTER_COLOUR = b'\xff\x82\x80'
|
||||
|
||||
SEGMENT_INIT = 'init.mp4'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from flask import current_app, render_template, send_from_directory, request, abort, Response, redirect, url_for, make_response, send_file
|
||||
from flask import current_app, render_template, send_from_directory, request, abort, redirect, url_for, make_response, send_file
|
||||
from werkzeug import wrap_file
|
||||
import os
|
||||
import time
|
||||
|
@ -11,7 +11,7 @@ import toml
|
|||
import website.chat as chat
|
||||
import website.viewership as viewership
|
||||
import website.utils.stream as stream
|
||||
from website.constants import DIR_STATIC, DIR_STATIC_EXTERNAL, VIDEOJS_ENABLED_BY_DEFAULT, SEGMENT_INIT, CHAT_SCROLLBACK, BROADCASTER_COLOUR, BROADCASTER_TOKEN, SEGMENTS_DIR, VIEW_COUNTING_PERIOD, CONFIG, CONFIG_FILE, NOTES, N_NONE, MESSAGE_MAX_LENGTH
|
||||
from website.constants import DIR_STATIC, DIR_STATIC_EXTERNAL, VIDEOJS_ENABLED_BY_DEFAULT, SEGMENT_INIT, CHAT_SCROLLBACK, BROADCASTER_COLOUR, BROADCASTER_TOKEN, SEGMENTS_DIR, VIEW_COUNTING_PERIOD, CONFIG, CONFIG_FILE, NOTES, N_NONE, MESSAGE_MAX_LENGTH, BACKGROUND_COLOUR
|
||||
from website.concatenate import ConcatenatedSegments, resolve_segment_offset
|
||||
|
||||
RE_WHITESPACE = re.compile(r'\s+')
|
||||
|
@ -46,7 +46,7 @@ def index(token=None):
|
|||
online=online,
|
||||
start_number=resolve_segment_offset() if online else 0,
|
||||
hls_time=CONFIG['stream']['hls_time'])
|
||||
response = Response(response) # TODO: add a view of the chat only, either as an arg here or another route
|
||||
response = make_response(response) # TODO: add a view of the chat only, either as an arg here or another route
|
||||
response.set_cookie('token', token)
|
||||
return response
|
||||
|
||||
|
@ -101,6 +101,11 @@ def segment_arbitrary(n):
|
|||
viewership.video_was_corrupted.remove(token)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# only send segments that are listed in stream.m3u8
|
||||
# this stops old segments from previous streams being sent
|
||||
if f'stream{n}.m4s' not in stream.get_segments():
|
||||
return abort(404)
|
||||
viewership.view_segment(n, token)
|
||||
response = send_from_directory(SEGMENTS_DIR, f'stream{n}.m4s', add_etags=False)
|
||||
response.headers['Cache-Control'] = 'no-cache'
|
||||
|
@ -128,7 +133,13 @@ def segments():
|
|||
except FileNotFoundError:
|
||||
return abort(404)
|
||||
|
||||
response = send_file(concatenated_segments, mimetype='video/mp4', add_etags=False)
|
||||
def generate():
|
||||
while True:
|
||||
chunk = concatenated_segments.read(8192)
|
||||
if chunk == b'':
|
||||
return
|
||||
yield chunk
|
||||
response = current_app.response_class(generate(), mimetype='video/mp4')
|
||||
response.headers['Cache-Control'] = 'no-store'
|
||||
response.set_cookie('token', token)
|
||||
return response
|
||||
|
@ -153,6 +164,7 @@ def chat_iframe():
|
|||
default_nickname=viewership.default_nickname,
|
||||
broadcaster=token == BROADCASTER_TOKEN,
|
||||
broadcaster_colour=BROADCASTER_COLOUR,
|
||||
background_colour=BACKGROUND_COLOUR,
|
||||
debug=request.args.get('debug'),
|
||||
RE_WHITESPACE=RE_WHITESPACE,
|
||||
len=len,
|
||||
|
@ -205,7 +217,7 @@ def comment_iframe(token=None):
|
|||
nickname=viewers[token]['nickname'],
|
||||
viewer=viewers[token],
|
||||
show_settings=preset.get('show_settings', False))
|
||||
response = Response(response)
|
||||
response = make_response(response)
|
||||
response.set_cookie('token', token)
|
||||
return response
|
||||
|
||||
|
@ -297,6 +309,7 @@ def users():
|
|||
broadcaster=token == BROADCASTER_TOKEN,
|
||||
debug=request.args.get('debug'),
|
||||
broadcaster_colour=BROADCASTER_COLOUR,
|
||||
background_colour=BACKGROUND_COLOUR,
|
||||
len=len)
|
||||
|
||||
@current_app.route('/static/radial.apng')
|
||||
|
|
|
@ -11,11 +11,20 @@
|
|||
.rotate {transform: rotate(-180deg);}
|
||||
.reverse {direction: rtl;}
|
||||
|
||||
.comment {color: white;padding:4px 0;margin-top:-4px;margin-bottom:calc(-4px + 0.375em);overflow:hidden;}
|
||||
.date {color:gray;font-size:75%;text-align:center;border-bottom:1px solid #333;margin:0.5em 0 0.75em 0;}
|
||||
.comment {color:white;padding:3px 2px;overflow:hidden;}
|
||||
.comment:hover{background-color:#333;border-radius:4px;}
|
||||
.date {color:gray;font-size:75%;text-align:center;border-bottom:1px solid #333;margin:0.5em 0 0.75em 0;cursor:default;}
|
||||
|
||||
.name {font-weight: bold;unicode-bidi: isolate;}
|
||||
sup {margin: 0 -0.25em 0 0.125em;font-size: 90%;font-family:monospace;}
|
||||
.name {color:var(--name-color);font-weight:bold;unicode-bidi:isolate;}
|
||||
.name:hover{
|
||||
background-color: var(--name-bg-color);
|
||||
text-shadow: 0 0 1px {{ background_colour }};
|
||||
border-radius: 2px;
|
||||
padding: 2px;
|
||||
margin: -2px;
|
||||
cursor: default;
|
||||
}
|
||||
sup {margin-right: 0.125em;font-size: 90%;font-family:monospace;}
|
||||
.tripcode {
|
||||
margin-left: 0.5em;
|
||||
padding: 0 5px;
|
||||
|
@ -28,16 +37,16 @@
|
|||
.barrier {display:inline-block;margin-right:0.5em;}
|
||||
.message {overflow-wrap: break-word;unicode-bidi: isolate;}
|
||||
.camera {transform: scaleX(-1);text-shadow: 0px 0px 6px #{{ broadcaster_colour.hex() }};cursor: help;margin-right:0.25em;word-break: keep-all;}
|
||||
.time {font-size: 80%;color: gray;vertical-align:middle;}
|
||||
.time {font-size: 80%;color: gray;vertical-align:middle;cursor:default;}
|
||||
|
||||
{% if include_user_list %}
|
||||
#users {color:white;}
|
||||
.group {margin-bottom:1.5em;}
|
||||
.group-name {margin-bottom:0.25em;}
|
||||
.person {margin-left: 0.5em;}
|
||||
.person {margin: 0 0 2px 0.5em;}
|
||||
{% endif %}
|
||||
|
||||
input[type="submit"] {padding-left:0.25em;padding-right:0.25em;margin-bottom:0.5em;}
|
||||
input[type="submit"] {padding:0 0.25em;margin-bottom:0.5em;}
|
||||
.refresh {
|
||||
color: white;
|
||||
background-color: gray;
|
||||
|
@ -51,6 +60,8 @@
|
|||
box-shadow: 0px 2px 4px black;
|
||||
margin: 0;
|
||||
}
|
||||
.refresh::after {content: "Manual refresh required";}
|
||||
input[type="checkbox"]{vertical-align:middle;}
|
||||
.unhide {
|
||||
animation: unhide 0s forwards 30s;
|
||||
height: 0;
|
||||
|
@ -78,6 +89,12 @@
|
|||
margin-bottom: 1.25em;
|
||||
}
|
||||
}
|
||||
td{width:100vw;}
|
||||
|
||||
/* for text-based browsers */
|
||||
.textonly {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -90,42 +107,47 @@
|
|||
<input class="rotate" type="submit" name="ban_purge" value="Ban and hide all">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- TODO: mobile tooltip / title -->
|
||||
<!-- TODO: have a light background with border-radius appear when hovering over a comment -->
|
||||
<table style="border-spacing:0 2px;">
|
||||
<tbody>
|
||||
{% for message in messages %}
|
||||
{% if message.get('special') == 'date' %}
|
||||
<div class="date rotate">{{ message['content'] }}</div>
|
||||
{% else %}
|
||||
<div class="comment rotate">
|
||||
<tr>
|
||||
<td class="comment rotate">
|
||||
{% if not message['hidden'] %}
|
||||
{% if broadcaster %}
|
||||
<input type="checkbox" name="message_id[]" value="{{ message['id'] }}">
|
||||
{% endif %}
|
||||
<span class="time" title="{{ message['timestamp'] }}">{{ message['time'] }}</span><div class="barrier"></div
|
||||
<span class="time" title="{{ message['timestamp'] }}">{{ message['time'] }}</span><span class="barrier"></span><span class="textonly"> </span
|
||||
{% if message['viewer']['broadcaster'] %}
|
||||
><span class="camera" title="Broadcaster">🎥</span
|
||||
{% endif %}
|
||||
><span class="name" style="color:#{{ message['viewer']['colour'].hex() }};">{{ RE_WHITESPACE.sub(chr(160), message['viewer']['nickname'] or default_nickname(message['viewer']['token'])) }}{% with tag = message['viewer']['nickname'] == None and not message['viewer']['broadcaster'] %}{% if tag %}<sup>{{ message['viewer']['tag'] }}</sup>{% endif %}</span
|
||||
><span class="name" style="--name-color:#{{ message['viewer']['colour'].hex() }};--name-bg-color:#{{ message['viewer']['colour'].hex() }}20;">{{ RE_WHITESPACE.sub(chr(160), message['viewer']['nickname'] or default_nickname(message['viewer']['token'])) }}{% with tag = message['viewer']['nickname'] == None and not message['viewer']['broadcaster'] %}{% if tag %}<sup>{{ message['viewer']['tag'] }}</sup>{% endif %}</span
|
||||
{% if message['viewer']['tripcode']['string'] %}{% if tag %}><span style="margin-right:0.125em;"></span{% endif %}
|
||||
><div class="barrier" style="margin:0;"></div><div class="tripcode" style="background-color:#{{ message['viewer']['tripcode']['background_colour'].hex() }};color:#{{ message['viewer']['tripcode']['foreground_colour'].hex() }};">{{ message['viewer']['tripcode']['string'] }}</div
|
||||
><span class="barrier" style="margin:0;"></span><span class="textonly"> <</span><span class="tripcode" style="background-color:#{{ message['viewer']['tripcode']['background_colour'].hex() }};color:#{{ message['viewer']['tripcode']['foreground_colour'].hex() }};">{{ message['viewer']['tripcode']['string'] }}</span><span class="textonly">></span
|
||||
{% endif %}{% endwith %}
|
||||
><div class="barrier"></div><span class="message">{{ message['text'] }}</span>
|
||||
><span class="barrier"></span><br class="textonly"><span class="message">{{ message['text'] }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if broadcaster %}
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<a href="" style="text-decoration: none;"><div class="refresh unhide rotate">Manual refresh required</div></a>
|
||||
<a href="" style="text-decoration: none;"><div class="refresh unhide rotate"></div></a>
|
||||
</div>
|
||||
{% if include_user_list %}
|
||||
<div class="textonly">== List of users ==</div>
|
||||
<div id="users" style="overflow: hidden;height:0;">
|
||||
<a href="" style="text-decoration: none;">
|
||||
<div class="refresh unhide-margin" style="bottom:revert;top:0;">Manual refresh required</div>
|
||||
<div class="refresh unhide-margin" style="bottom:revert;top:0;"></div>
|
||||
</a>
|
||||
<div style="margin: 0.25em 1em;">
|
||||
{% with person = people['broadcaster'] %}
|
||||
|
@ -133,9 +155,10 @@
|
|||
<div class="group">
|
||||
<div class="group-name">Broadcaster</div>
|
||||
<div class="person">
|
||||
<span class="camera" title="Broadcaster">🎥</span><span class="name" style="color:#{{ person['colour'].hex() }};">{{ person['nickname'] or default_nickname(person['token']) }}</span>{% if person['tripcode']['string'] %}<div class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</div>{% endif %}
|
||||
<span class="camera" title="Broadcaster">🎥</span><span class="name" style="--name-color:#{{ person['colour'].hex() }};--name-bg-color:#{{ person['colour'].hex() }}20;">{{ person['nickname'] or default_nickname(person['token']) }}</span>{% if person['tripcode']['string'] %}<span class="textonly"> <</span><span class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</span><span class="textonly">></span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<br class="textonly">
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if broadcaster %}
|
||||
|
@ -147,18 +170,20 @@
|
|||
<div class="group-name">Users watching ({{ len(people['watching']) }})</div>
|
||||
{% for person in people['watching'] %}
|
||||
<div class="person">
|
||||
{% if broadcaster %}<input type="checkbox" name="token[]" value="{{ person['token'] }}">{% endif %}<span class="name" style="color:#{{ person['colour'].hex() }};">{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}<sup>{{ person['tag'] }}</sup>{% endif %}</span>{% if person['tripcode']['string'] %}{% if tag %}<span style="margin-right:0.125em;"></span>{% endif %}<div class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</div>{% endif %}{% endwith %}{% if person['token'] == token %}<span style="margin-left:0.5em;color:white;">(You)</span>{% endif %}
|
||||
{% if broadcaster %}<input type="checkbox" name="token[]" value="{{ person['token'] }}">{% endif %}<span class="name" style="--name-color:#{{ person['colour'].hex() }};--name-bg-color:#{{ person['colour'].hex() }}20;">{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}<sup>{{ person['tag'] }}</sup>{% endif %}</span>{% if person['tripcode']['string'] %}{% if tag %}<span style="margin-right:0.125em;"></span>{% endif %}<span class="textonly"> <</span><span class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</span><span class="textonly">></span>{% endif %}{% endwith %}{% if person['token'] == token %}<span style="margin-left:0.5em;color:white;">(You)</span>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<br class="textonly">
|
||||
<div class="group">
|
||||
<div class="group-name">Users not watching ({{ len(people['not_watching']) }})</div>
|
||||
{% for person in people['not_watching'] %}
|
||||
<div class="person">
|
||||
{% if broadcaster %}<input type="checkbox" name="token[]" value="{{ person['token'] }}">{% endif %}<span class="name" style="color:#{{ person['colour'].hex() }};">{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}<sup>{{ person['tag'] }}</sup>{% endif %}</span>{% if person['tripcode']['string'] %}{% if tag %}<span style="margin-right:0.125em;"></span>{% endif %}<div class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</div>{% endif %}{% endwith %}{% if person['token'] == token %}<span style="margin-left:0.5em;color:white;">(You)</span>{% endif %}
|
||||
{% if broadcaster %}<input type="checkbox" name="token[]" value="{{ person['token'] }}">{% endif %}<span class="name" style="--name-color:#{{ person['colour'].hex() }};--name-bg-color:#{{ person['colour'].hex() }}20;">{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}<sup>{{ person['tag'] }}</sup>{% endif %}</span>{% if person['tripcode']['string'] %}{% if tag %}<span style="margin-right:0.125em;"></span>{% endif %}<span class="textonly"> <</span><span class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</span><span class="textonly">></span>{% endif %}{% endwith %}{% if person['token'] == token %}<span style="margin-left:0.5em;color:white;">(You)</span>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<br class="textonly">
|
||||
{% if broadcaster %}
|
||||
<button>Ban</button>
|
||||
</form>
|
||||
|
@ -170,7 +195,7 @@
|
|||
<div class="group">
|
||||
{% for person in people['banned'] %}
|
||||
<div class="person">
|
||||
<input type="checkbox" name="token[]" value="{{ person['token'] }}"><span class="name" style="color:#{{ person['colour'].hex() }};">{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}<sup>{{ person['tag'] }}</sup>{% endif %}</span>{% if person['tripcode']['string'] %}{% if tag %}<span style="margin-right:0.125em;"></span>{% endif %}<div class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</div>{% endif %}{% endwith %}
|
||||
<input type="checkbox" name="token[]" value="{{ person['token'] }}"><span class="name" style="--name-color:#{{ person['colour'].hex() }};--name-bg-color:#{{ person['colour'].hex() }}20;">{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}<sup>{{ person['tag'] }}</sup>{% endif %}</span>{% if person['tripcode']['string'] %}{% if tag %}<span style="margin-right:0.125em;"></span>{% endif %}<span class="textonly"> <</span><span class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</span><span class="textonly">></span>{% endif %}{% endwith %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
|
@ -151,6 +151,28 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
#to-settings::after {
|
||||
content: "⚙️";
|
||||
}
|
||||
#to-chat::after {
|
||||
content: "💬";
|
||||
}
|
||||
.empty-tripcode-container::after {
|
||||
content: "(no tripcode)";
|
||||
}
|
||||
#cancel-password::after {
|
||||
content: "×";
|
||||
}
|
||||
#remove-tripcode::after {
|
||||
content: "×";
|
||||
}
|
||||
#blank-tripcode::after {
|
||||
content: "(cleared)";
|
||||
}
|
||||
#undo-remove-tripcode::after {
|
||||
content: "undo";
|
||||
}
|
||||
|
||||
#tripcode {
|
||||
padding: 0 5px;
|
||||
border-radius: 6px;
|
||||
|
@ -166,6 +188,11 @@
|
|||
.pseudolink:active, .pseudolink:hover {
|
||||
color: #00b6f0;
|
||||
}
|
||||
|
||||
/* for text-based browsers */
|
||||
.textonly {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -173,46 +200,55 @@
|
|||
<form id="comment-box" class="pure-form" action="{{ url_for('comment') }}" method="post">
|
||||
<input type="hidden" name="token" value="{{ token }}">
|
||||
<input type="hidden" name="nonce" value="{{ nonce }}">
|
||||
<div class="textonly">Send a message:</div>
|
||||
<input id="message" class="pure-u-1 smalltext" name="message" maxlength="250" placeholder="Send a message" value="{{ message }}">
|
||||
<button style="height:30px;font-weight:bold;bottom:1.8em;" class="pure-button pure-button-primary">Chat</button>
|
||||
<div class="note-space">
|
||||
<label for="settings-toggle">⚙️</label>
|
||||
<label for="settings-toggle" id="to-settings"></label>
|
||||
{% if captcha %}
|
||||
<input type="image" formaction="{{ url_for('comment_iframe') }}" title="Click for a new captcha" src="{{ captcha['src'] }}"></input>
|
||||
<input type="image" formaction="{{ url_for('comment_iframe') }}" title="Click for a new captcha" src="{{ captcha['src'] }}" alt="Your browser doesn't support the data URI scheme."></input>
|
||||
<input name="captcha-ciphertext" type="hidden" value="{{ captcha['ciphertext'] }}">
|
||||
<div class="textonly">Captcha:</div>
|
||||
<input id="captcha" class="smalltext" style="height:30;" name="captcha" placeholder="Captcha">
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
<br class="textonly">
|
||||
<form id="settings" action="{{ url_for('settings') }}" method="post">
|
||||
<input type="hidden" name="token" value="{{ token }}">
|
||||
<div class="textonly">Nickname:</div>
|
||||
<input id="nickname" style="width:60%;font-weight:bold;color:#{{ viewer['colour'].hex() }};" name="nickname" maxlength="24" placeholder="{{ default }}" value="{{ '' if nickname == None else nickname }}">
|
||||
<br class="textonly">
|
||||
<span class="textonly">Set tripcode:</span>
|
||||
<input type="checkbox" id="password-toggle" name="set-tripcode">
|
||||
<br class="textonly">
|
||||
<span class="textonly">Remove tripcode:</span>
|
||||
<input type="checkbox" id="blank-tripcode-toggle" name="remove-tripcode">
|
||||
<div class="textonly">Password (to set tripcode):</div>
|
||||
<input id="password" type="password" style="width:30%;" name="password" maxlength="256" placeholder="Password">
|
||||
{% if viewer['tripcode']['string'] %}
|
||||
<label for="password-toggle">
|
||||
<span id="tripcode-container">
|
||||
<span id="tripcode" style="background-color:#{{ viewer['tripcode']['background_colour'].hex() }};color:#{{ viewer['tripcode']['foreground_colour'].hex() }};">{{ viewer['tripcode']['string'] }}</span>
|
||||
<span class="textonly"><</span><span id="tripcode" style="background-color:#{{ viewer['tripcode']['background_colour'].hex() }};color:#{{ viewer['tripcode']['foreground_colour'].hex() }};">{{ viewer['tripcode']['string'] }}</span><span class="textonly">></span>
|
||||
</span>
|
||||
</label>
|
||||
<span id="blank-tripcode">(cleared)</span>
|
||||
<span id="blank-tripcode"></span>
|
||||
<label for="blank-tripcode-toggle">
|
||||
<span class="pseudolink" id="remove-tripcode">×</span>
|
||||
<span class="pseudolink" id="undo-remove-tripcode">undo</span>
|
||||
<span class="pseudolink" id="remove-tripcode"></span>
|
||||
<span class="pseudolink" id="undo-remove-tripcode"></span>
|
||||
</label>
|
||||
<label for="password-toggle">
|
||||
<span class="pseudolink" id="cancel-password">×</span>
|
||||
<span class="pseudolink" id="cancel-password"></span>
|
||||
</label>
|
||||
{% else %}
|
||||
<label for="password-toggle">
|
||||
<span class="pseudolink" id="tripcode-container">(no tripcode)</span>
|
||||
<span class="pseudolink" id="cancel-password">×</span>
|
||||
<span class="pseudolink empty-tripcode-container" id="tripcode-container"></span>
|
||||
<span class="pseudolink" id="cancel-password"></span>
|
||||
</label>
|
||||
{% endif %}
|
||||
<div>
|
||||
<div class="note-space">
|
||||
<label for="settings-toggle">💬</label>
|
||||
<label for="settings-toggle" id="to-chat"></label>
|
||||
<button style="padding: 0.5em;float:right;" class="pure-button pure-button-primary smalltext">Change appearance</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,10 +5,18 @@
|
|||
body {color:white;margin-top: 0;margin-bottom: 0;}
|
||||
.group {margin-bottom:1.5em;}
|
||||
.group-name {margin-bottom:0.25em;}
|
||||
.person {margin-left: 0.5em;}
|
||||
.person {margin: 0 0 2px 0.5em;}
|
||||
.camera {transform: scaleX(-1);text-shadow: 0px 0px 6px #{{ broadcaster_colour.hex() }};cursor: help;margin-right:0.25em;}
|
||||
.name {font-weight: bold;unicode-bidi: isolate;}
|
||||
sup {margin: 0 -0.25em 0 0.125em;font-size: 90%;font-family:monospace;}
|
||||
.name {color:var(--name-color);font-weight: bold;unicode-bidi: isolate;}
|
||||
.name:hover{
|
||||
background: 4px var(--name-bg-color);
|
||||
text-shadow: 0 0 1px {{ background_colour }};
|
||||
border-radius: 2px;
|
||||
padding: 2px 4px;
|
||||
margin: -2px -4px;
|
||||
cursor: default;
|
||||
}
|
||||
sup {margin-right: 0.125em;font-size: 90%;font-family:monospace;}
|
||||
.tripcode {
|
||||
margin-left: 0.5em;
|
||||
padding: 0 5px;
|
||||
|
@ -34,6 +42,7 @@
|
|||
box-shadow: 0px 2px 4px black;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.unhide-margin {
|
||||
animation: unhide-margin 0s forwards 30s;
|
||||
height: 0;
|
||||
|
@ -48,11 +57,16 @@
|
|||
margin-bottom: 1.25em;
|
||||
}
|
||||
}
|
||||
|
||||
/* for text-based browsers */
|
||||
.textonly {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="" style="text-decoration: none;">
|
||||
<div class="refresh unhide-margin" style="bottom:revert;top:0;">Manual refresh required</div>
|
||||
<div class="refresh unhide-margin" style="bottom:revert;top:0;"></div>
|
||||
</a>
|
||||
<div style="margin: 0.5em 1em;">
|
||||
{% with person = people['broadcaster'] %}
|
||||
|
@ -60,9 +74,10 @@
|
|||
<div class="group">
|
||||
<div class="group-name">Broadcaster</div>
|
||||
<div class="person">
|
||||
<span class="camera" title="Broadcaster">🎥</span>⁠<span class="name" style="color:#{{ person['colour'].hex() }};">{{ person['nickname'] or default_nickname(person['token']) }}</span>{% if person['tripcode']['string'] %}<div class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</div>{% endif %}
|
||||
<span class="camera" title="Broadcaster">🎥</span><span class="name" style="--name-color:#{{ person['colour'].hex() }};--name-bg-color:#{{ person['colour'].hex() }}20;">{{ person['nickname'] or default_nickname(person['token']) }}</span>{% if person['tripcode']['string'] %}<span class="textonly"> <</span><span class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</span><span class="textonly">></span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<br class="textonly">
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if broadcaster %}
|
||||
|
@ -74,18 +89,20 @@
|
|||
<div class="group-name">Users watching ({{ len(people['watching']) }})</div>
|
||||
{% for person in people['watching'] %}
|
||||
<div class="person">
|
||||
{% if broadcaster %}<input type="checkbox" name="token[]" value="{{ person['token'] }}">{% endif %}<span class="name" style="color:#{{ person['colour'].hex() }};">{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}<sup>{{ person['tag'] }}</sup>{% endif %}</span>{% if person['tripcode']['string'] %}{% if tag %}<span style="margin-right:0.125em;"></span>{% endif %}<div class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</div>{% endif %}{% endwith %}{% if person['token'] == token %}<span style="margin-left:0.5em;color:white;">(You)</span>{% endif %}
|
||||
{% if broadcaster %}<input type="checkbox" name="token[]" value="{{ person['token'] }}">{% endif %}<span class="name" style="--name-color:#{{ person['colour'].hex() }};--name-bg-color:#{{ person['colour'].hex() }}20;">{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}<sup>{{ person['tag'] }}</sup>{% endif %}</span>{% if person['tripcode']['string'] %}{% if tag %}<span style="margin-right:0.125em;"></span>{% endif %}<span class="textonly"> <</span><span class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</span><span class="textonly">></span>{% endif %}{% endwith %}{% if person['token'] == token %}<span class="textonly"> </span><span style="margin-left:0.5em;color:white;">(You)</span>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<br class="textonly">
|
||||
<div class="group">
|
||||
<div class="group-name">Users not watching ({{ len(people['not_watching']) }})</div>
|
||||
{% for person in people['not_watching'] %}
|
||||
<div class="person">
|
||||
{% if broadcaster %}<input type="checkbox" name="token[]" value="{{ person['token'] }}">{% endif %}<span class="name" style="color:#{{ person['colour'].hex() }};">{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}<sup>{{ person['tag'] }}</sup>{% endif %}</span>{% if person['tripcode']['string'] %}{% if tag %}<span style="margin-right:0.125em;"></span>{% endif %}<div class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</div>{% endif %}{% endwith %}{% if person['token'] == token %}<span style="margin-left:0.5em;color:white;">(You)</span>{% endif %}
|
||||
{% if broadcaster %}<input type="checkbox" name="token[]" value="{{ person['token'] }}">{% endif %}<span class="name" style="--name-color:#{{ person['colour'].hex() }};--name-bg-color:#{{ person['colour'].hex() }}20;">{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}<sup>{{ person['tag'] }}</sup>{% endif %}</span>{% if person['tripcode']['string'] %}{% if tag %}<span style="margin-right:0.125em;"></span>{% endif %}<span class="textonly"> <</span><span class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</span><span class="textonly">></span>{% endif %}{% endwith %}{% if person['token'] == token %}<span style="margin-left:0.5em;color:white;">(You)</span>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<br class="textonly">
|
||||
{% if broadcaster %}
|
||||
<button>Ban</button>
|
||||
</form>
|
||||
|
@ -97,7 +114,7 @@
|
|||
<div class="group">
|
||||
{% for person in people['banned'] %}
|
||||
<div class="person">
|
||||
<input type="checkbox" name="token[]" value="{{ person['token'] }}"><span class="name" style="color:#{{ person['colour'].hex() }};">{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}<sup>{{ person['tag'] }}</sup>{% endif %}</span>{% if person['tripcode']['string'] %}{% if tag %}<span style="margin-right:0.125em;"></span>{% endif %}<div class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</div>{% endif %}{% endwith %}
|
||||
<input type="checkbox" name="token[]" value="{{ person['token'] }}"><span class="name" style="--name-color:#{{ person['colour'].hex() }};--name-bg-color:#{{ person['colour'].hex() }}20;">{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}<sup>{{ person['tag'] }}</sup>{% endif %}</span>{% if person['tripcode']['string'] %}{% if tag %}<span style="margin-right:0.125em;"></span>{% endif %}<span class="textonly"> <</span><span class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</span><span class="textonly">></span>{% endif %}{% endwith %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import werkzeug.security
|
||||
import base64
|
||||
import website.utils.colour
|
||||
from website.constants import BACKGROUND_COLOUR
|
||||
from website.constants import BACKGROUND_COLOUR, CONFIG
|
||||
|
||||
FOREGROUND_COLOURS = (b'\0\0\0', b'\x3f\x3f\x3f', b'\x7f\x7f\x7f', b'\xbf\xbf\xbf', b'\xff\xff\xff')
|
||||
|
||||
def default():
|
||||
return {'string': None, 'background_colour': None, 'foreground_colour': None}
|
||||
|
||||
def tripcode_salt():
|
||||
return CONFIG['secrets']['tripcode_salt'].encode() or b'\0'
|
||||
|
||||
def gen_tripcode(password):
|
||||
tripcode = default()
|
||||
pwhash = werkzeug.security._hash_internal('pbkdf2:sha256', b'\0', password)[0]
|
||||
pwhash = werkzeug.security._hash_internal('pbkdf2:sha256', tripcode_salt(), password)[0]
|
||||
tripcode_data = bytes.fromhex(pwhash)[:6]
|
||||
tripcode['string'] = base64.b64encode(tripcode_data).decode()
|
||||
tripcode['background_colour'] = website.utils.colour.gen_colour(tripcode_data, BACKGROUND_COLOUR)
|
||||
|
|
|
@ -5,7 +5,7 @@ import time
|
|||
import website.utils.colour as colour
|
||||
import website.utils.tripcode as tripcode
|
||||
import website.chat as chat
|
||||
from website.constants import ANON_DEFAULT_NICKNAME, BROADCASTER_COLOUR, BROADCASTER_TOKEN, CONFIG, HOST_DEFAULT_NICKNAME, SEGMENTS_DIR, VIEW_COUNTING_PERIOD, VIEWER_ABSENT_THRESHOLD
|
||||
from website.constants import BROADCASTER_COLOUR, BROADCASTER_TOKEN, CONFIG, SEGMENTS_DIR, VIEW_COUNTING_PERIOD, VIEWER_ABSENT_THRESHOLD
|
||||
|
||||
viewers = {}
|
||||
segment_views = {}
|
||||
|
@ -35,8 +35,8 @@ preset_comment_iframe = {}
|
|||
|
||||
def default_nickname(token):
|
||||
if token == BROADCASTER_TOKEN:
|
||||
return HOST_DEFAULT_NICKNAME
|
||||
return ANON_DEFAULT_NICKNAME
|
||||
return CONFIG['chat']['host_default_name']
|
||||
return CONFIG['chat']['anon_default_name']
|
||||
|
||||
def setdefault(token):
|
||||
if token in viewers or token == None:
|
||||
|
|
読み込み中…
新しいイシューから参照