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

このコミットが含まれているのは:
n9k 2021-07-16 22:02:19 +00:00
コミット 08a0981daa
9個のファイルの変更158行の追加64行の削除

ファイルの表示

@ -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"> &lt;</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">&gt;</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"> &lt;</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">&gt;</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"> &lt;</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">&gt;</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"> &lt;</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">&gt;</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"> &lt;</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">&gt;</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">&lt;</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">&gt;</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">&times;</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">&times;</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">&times;</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>&NoBreak;<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"> &lt;</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">&gt;</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"> &lt;</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">&gt;</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"> &lt;</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">&gt;</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"> &lt;</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">&gt;</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: