secret club mode (private/whitelist); usability improvements in text browsers
このコミットが含まれているのは:
コミット
7bf84413e6
|
@ -29,16 +29,25 @@ def get_token(form=False):
|
|||
token = None
|
||||
return token
|
||||
|
||||
@current_app.route('/')
|
||||
@current_app.route('/', methods=['GET', 'POST'])
|
||||
def index(token=None):
|
||||
token = token or get_token() or new_token()
|
||||
viewership.made_request(token)
|
||||
if request.method == 'POST':
|
||||
password = request.form.get('password', '')
|
||||
chat.set_tripcode(password, token)
|
||||
if not viewership.is_allowed(token):
|
||||
response = render_template('secret-club.html', token=token, tripcode=viewers[token]['tripcode'])
|
||||
response = make_response(response, 403)
|
||||
response.set_cookie('token', token)
|
||||
return response
|
||||
try:
|
||||
viewership.video_was_corrupted.remove(token)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
use_videojs = bool(request.args.get('videojs', default=int(VIDEOJS_ENABLED_BY_DEFAULT), type=int))
|
||||
online = stream.is_online()
|
||||
viewership.made_request(token)
|
||||
|
||||
response = render_template('index.html',
|
||||
token=token,
|
||||
|
@ -60,11 +69,13 @@ def playlist():
|
|||
if not stream.is_online():
|
||||
return abort(404)
|
||||
token = get_token()
|
||||
viewership.made_request(token)
|
||||
if not viewership.is_allowed(token):
|
||||
return abort(403)
|
||||
try:
|
||||
viewership.video_was_corrupted.remove(token)
|
||||
except KeyError:
|
||||
pass
|
||||
viewership.made_request(token)
|
||||
|
||||
try:
|
||||
token_playlist = stream.token_playlist(token)
|
||||
|
@ -79,13 +90,15 @@ def playlist():
|
|||
def segment_init():
|
||||
if not stream.is_online():
|
||||
return abort(404)
|
||||
|
||||
token = get_token() or new_token()
|
||||
viewership.made_request(token)
|
||||
if not viewership.is_allowed(token):
|
||||
return abort(403)
|
||||
try:
|
||||
viewership.video_was_corrupted.remove(token)
|
||||
except KeyError:
|
||||
pass
|
||||
viewership.made_request(token)
|
||||
|
||||
response = send_from_directory(SEGMENTS_DIR, f'init.mp4', add_etags=False)
|
||||
response.headers['Cache-Control'] = 'no-cache'
|
||||
response.set_cookie('token', token)
|
||||
|
@ -95,8 +108,9 @@ def segment_init():
|
|||
def segment_arbitrary(n):
|
||||
if not stream.is_online():
|
||||
return abort(404)
|
||||
|
||||
token = get_token()
|
||||
if not viewership.is_allowed(token):
|
||||
return abort(403)
|
||||
try:
|
||||
viewership.video_was_corrupted.remove(token)
|
||||
except KeyError:
|
||||
|
@ -116,11 +130,20 @@ def segments():
|
|||
if not stream.is_online():
|
||||
return abort(404)
|
||||
token = get_token() or new_token()
|
||||
viewership.made_request(token)
|
||||
if not viewership.is_allowed(token):
|
||||
return abort(403)
|
||||
try:
|
||||
viewership.video_was_corrupted.remove(token)
|
||||
except KeyError:
|
||||
pass
|
||||
viewership.made_request(token)
|
||||
|
||||
def should_close_connection():
|
||||
if not stream.is_online():
|
||||
return True
|
||||
if not viewership.is_allowed(token):
|
||||
return True
|
||||
return False
|
||||
|
||||
start_number = request.args.get('segment', type=int)
|
||||
if start_number == None:
|
||||
|
@ -129,7 +152,7 @@ def segments():
|
|||
concatenated_segments = ConcatenatedSegments(start_number=start_number,
|
||||
segment_hook=lambda n: viewership.view_segment(n, token, check_exists=False),
|
||||
corrupt_hook=lambda: viewership.video_was_corrupted.add(token), # lock?
|
||||
should_close_connection=lambda: not stream.is_online())
|
||||
should_close_connection=should_close_connection)
|
||||
except FileNotFoundError:
|
||||
return abort(404)
|
||||
|
||||
|
@ -148,6 +171,8 @@ def segments():
|
|||
def chat_iframe():
|
||||
token = get_token()
|
||||
viewership.made_request(token)
|
||||
if not viewership.is_allowed(token):
|
||||
return abort(403)
|
||||
|
||||
include_user_list = bool(request.args.get('users', default=1, type=int))
|
||||
with viewership.lock: # required because another thread can change chat.messages while we're iterating through it
|
||||
|
@ -165,6 +190,9 @@ def chat_iframe():
|
|||
broadcaster=token == BROADCASTER_TOKEN,
|
||||
broadcaster_colour=BROADCASTER_COLOUR,
|
||||
background_colour=BACKGROUND_COLOUR,
|
||||
stream_title=stream.get_title(),
|
||||
stream_viewers=viewership.count(),
|
||||
stream_uptime=stream.readable_uptime(),
|
||||
debug=request.args.get('debug'),
|
||||
RE_WHITESPACE=RE_WHITESPACE,
|
||||
len=len,
|
||||
|
@ -174,6 +202,8 @@ def chat_iframe():
|
|||
def heartbeat():
|
||||
token = get_token()
|
||||
viewership.made_request(token)
|
||||
if not viewership.is_allowed(token):
|
||||
return abort(403)
|
||||
online = stream.is_online()
|
||||
start_abs, start_rel = stream.get_start(absolute=True, relative=True)
|
||||
|
||||
|
@ -193,6 +223,8 @@ def heartbeat():
|
|||
def comment_iframe(token=None):
|
||||
token = token or get_token() or new_token()
|
||||
viewership.made_request(token)
|
||||
if not viewership.is_allowed(token):
|
||||
return abort(403)
|
||||
|
||||
try:
|
||||
preset = viewership.preset_comment_iframe.pop(token)
|
||||
|
@ -224,13 +256,15 @@ def comment_iframe(token=None):
|
|||
@current_app.route('/comment', methods=['POST'])
|
||||
def comment():
|
||||
token = get_token(form=True) or new_token()
|
||||
viewership.made_request(token)
|
||||
if not viewership.is_allowed(token):
|
||||
return abort(403)
|
||||
|
||||
nonce = request.form.get('nonce')
|
||||
message = request.form.get('message', '').replace('\r', '').replace('\n', ' ').strip()
|
||||
c_response = request.form.get('captcha')
|
||||
c_ciphertext = request.form.get('captcha-ciphertext')
|
||||
|
||||
viewership.made_request(token)
|
||||
|
||||
failure_reason = chat.comment(message, token, c_response, c_ciphertext, nonce)
|
||||
|
||||
viewership.preset_comment_iframe[token] = {'note': failure_reason, 'message': message if failure_reason else ''}
|
||||
|
@ -243,10 +277,15 @@ def comment():
|
|||
@current_app.route('/settings', methods=['POST'])
|
||||
def settings():
|
||||
token = get_token(form=True) or new_token()
|
||||
viewership.made_request(token)
|
||||
if not viewership.is_allowed(token):
|
||||
return abort(403)
|
||||
|
||||
nickname = request.form.get('nickname', '')
|
||||
password = request.form.get('password', '')
|
||||
|
||||
viewership.made_request(token)
|
||||
old_nickname = viewers[token]['nickname']
|
||||
old_tripcode = viewers[token]['tripcode']
|
||||
|
||||
note, ok = chat.set_nickname(nickname, token)
|
||||
if ok:
|
||||
|
@ -255,6 +294,13 @@ def settings():
|
|||
elif request.form.get('set-tripcode'):
|
||||
note, _ = chat.set_tripcode(password, token)
|
||||
|
||||
if not viewership.is_allowed(token):
|
||||
if request.form.get('confirm'):
|
||||
return redirect(url_for('index'))
|
||||
viewers[token]['nickname'] = old_nickname
|
||||
viewers[token]['tripcode'] = old_tripcode
|
||||
return render_template('comment-confirm-iframe.html', token=token, nickname=old_nickname or viewership.default_nickname(token))
|
||||
|
||||
viewership.preset_comment_iframe[token] = {'note': note, 'show_settings': True}
|
||||
return redirect(url_for('comment_iframe', token=token))
|
||||
|
||||
|
@ -280,9 +326,11 @@ def mod_users():
|
|||
@current_app.route('/stream-info')
|
||||
def stream_info():
|
||||
token = get_token() or new_token()
|
||||
embed_images = bool(request.args.get('embed', type=int))
|
||||
|
||||
viewership.made_request(token)
|
||||
if not viewership.is_allowed(token):
|
||||
return abort(403)
|
||||
|
||||
embed_images = bool(request.args.get('embed', type=int))
|
||||
|
||||
start_abs, start_rel = stream.get_start(absolute=True, relative=True)
|
||||
online = stream.is_online()
|
||||
|
@ -301,7 +349,9 @@ def stream_info():
|
|||
@current_app.route('/users')
|
||||
def users():
|
||||
token = get_token()
|
||||
viewership.made_request(token)
|
||||
viewership.made_request()
|
||||
if not viewership.is_allowed(token):
|
||||
return abort(403)
|
||||
return render_template('users-iframe.html',
|
||||
token=token,
|
||||
people=viewership.get_people_list(),
|
||||
|
@ -312,21 +362,26 @@ def users():
|
|||
background_colour=BACKGROUND_COLOUR,
|
||||
len=len)
|
||||
|
||||
@current_app.route('/static/radial.apng')
|
||||
def radial():
|
||||
response = send_from_directory(DIR_STATIC, 'radial.apng', mimetype='image/png', add_etags=False)
|
||||
response.headers['Cache-Control'] = 'no-store' # caching this in any way messes with the animation
|
||||
response.expires = response.date
|
||||
return response
|
||||
|
||||
@current_app.route('/static/<fn>')
|
||||
def _static(fn):
|
||||
token = get_token()
|
||||
if fn != 'eye.png' and not viewership.is_allowed(token):
|
||||
return abort(403)
|
||||
|
||||
response = send_from_directory(DIR_STATIC, fn, add_etags=False)
|
||||
response.headers['Cache-Control'] = 'no-cache'
|
||||
if fn == 'radial.apng':
|
||||
response.mimetype = 'image/png'
|
||||
response.headers['Cache-Control'] = 'no-store' # caching this in any way messes with the animation
|
||||
elif response.status_code == 200 and not fn.endswith('.png'):
|
||||
response.headers['Cache-Control'] = 'no-cache'
|
||||
return response
|
||||
|
||||
@current_app.route('/static/external/<fn>')
|
||||
def third_party(fn):
|
||||
token = get_token()
|
||||
if not viewership.is_allowed(token):
|
||||
return abort(403)
|
||||
|
||||
response = send_from_directory(DIR_STATIC_EXTERNAL, fn, add_etags=False)
|
||||
response.headers['Cache-Control'] = 'public, max-age=604800, immutable'
|
||||
response.expires = response.date + datetime.timedelta(days=7)
|
||||
|
|
バイナリファイルは表示されません。
変更後 幅: | 高さ: | サイズ: 3.8 KiB |
|
@ -150,8 +150,14 @@ body.dark-theme {
|
|||
.dark-theme textarea {
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
/* workaround to make some custom user css work
|
||||
e.g. https://raw.githubusercontent.com/33kk/uso-archive/flomaster/data/usercss/2154.user.css */
|
||||
div[class="vjs-text-track-display"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* for text-based browsers */
|
||||
.textonly {
|
||||
display: none
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
cursor: default;
|
||||
}
|
||||
.barrier {display:inline-block;margin-right:0.5em;}
|
||||
.message {overflow-wrap: break-word;unicode-bidi: isolate;}
|
||||
.message {overflow-wrap:break-word;unicode-bidi:isolate;display:inline-block;width:calc(100vw - 2em);}
|
||||
.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;cursor:default;}
|
||||
|
||||
|
@ -99,6 +99,13 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="textonly">
|
||||
<br>
|
||||
<div><b>{{ stream_title }}</b></div>
|
||||
<div>{{ stream_viewers }} viewer(s), {% if uptime %}{{ stream_uptime }} uptime{% else %}offline{% endif %}</div>
|
||||
<br>
|
||||
<div>== Chat ==</div>
|
||||
</div>
|
||||
<div id="messages">
|
||||
{% if broadcaster %}
|
||||
<form action="{{ url_for('mod_chat') }}" method="post">
|
||||
|
@ -109,7 +116,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
<!-- TODO: mobile tooltip / title -->
|
||||
<table style="border-spacing:0 2px;">
|
||||
<table border="1" frame="void" rules="rows" style="border-collapse:separate;border-spacing:0 2px;">
|
||||
<tbody>
|
||||
{% for message in messages %}
|
||||
{% if message.get('special') == 'date' %}
|
||||
|
@ -171,7 +178,7 @@
|
|||
<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="--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 %}
|
||||
{% 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>
|
||||
|
@ -180,7 +187,7 @@
|
|||
<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="--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 %}
|
||||
{% 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>
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
a, .pseudolink {
|
||||
color: #a0a0a0;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover, .pseudolink:hover {
|
||||
color: #00b6f0;
|
||||
}
|
||||
.pseudolink {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<form method="post" target="_parent">
|
||||
<input type="hidden" name="confirm" value="1">
|
||||
<input type="hidden" name="token" value="{{ token }}">
|
||||
<input type="hidden" name="nickname" value="{{ nickname }}">
|
||||
<input type="hidden" name="remove-tripcode" value="1">
|
||||
<div>The stream is in secret club mode. Changing your tripcode would kick you out of the secret club. <a href="{{ url_for('comment_iframe', token=token) }}">Click here</a> to stay in the secret club, or <input type="submit" class="pseudolink" value="click here"> to get kicked out.</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -120,6 +120,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="textonly">
|
||||
<br>
|
||||
<div>== Quick links for text-based browsers ==</div>
|
||||
<div>Livestream: <a href="{{ url_for('segments', token=token) }}">{{ url_for('segments', token=token) }}</a></div>
|
||||
<div>Chat: <a href="{{ url_for('chat_iframe', token=token) }}">{{ url_for('chat_iframe', token=token) }}</a></div>
|
||||
<div>Comment box: <a href="{{ url_for('comment_iframe', token=token) }}">{{ url_for('comment_iframe', token=token) }}</a></div>
|
||||
<div>If the stream is in secret club mode, the <i>token</i> component is necessary.</div>
|
||||
</div>
|
||||
<script>
|
||||
/* replace noscript frame URLs with with-script versions */
|
||||
function replaceFrameURL(frameContainerId, newUrl, newId, newName) {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<html style="display:table;width:100%;height:100%;">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body style="display:table-cell;vertical-align:middle;background-color:#232323;color:#f0f0f0;padding-bottom:64px;">
|
||||
<form method="post">
|
||||
<input type="hidden" name="token" value="{{ token }}">
|
||||
<div style="text-align:center;font-size:96px;font-weight:bold;margin:0 0 64px 0;padding:0;">SECRET CLUB</div>
|
||||
<div style="margin:32px 0;text-align:center;font-size:24px;">
|
||||
{% if tripcode['string'] %}
|
||||
<div>Your tripcode: <span class="tripcode" style="padding:0 5px;border-radius:12px;font-size:90%;font-family:monospace;vertical-align:middle;display:inline-block;cursor:default;background-color:#{{ tripcode['background_colour'].hex() }};color:#{{ tripcode['foreground_colour'].hex() }};">{{ tripcode['string'] }}</span></div>
|
||||
<div>This tripcode is not allowed in.</div>
|
||||
{% else %}
|
||||
<div>Your tripcode: (none)</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="text-align:center;width:100vw;">
|
||||
<input placeholder="Password" type="password" name="password" maxlength="256" style="background:#333;border:1px solid black;border-radius:4px;color:#f0f0f0;padding:6px;text-align:center;font-size:32px;width:min(1024px, 90%);">
|
||||
</div>
|
||||
<input type="image" width="160" height="106" style="margin:32px auto;display:block;padding:8px;border:1px outset #2b2b2b;" src="{{ url_for('static', filename='eye.png') }}" alt="Submit"></form>
|
||||
</body>
|
||||
</html>
|
|
@ -90,7 +90,7 @@
|
|||
<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="--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 %}
|
||||
{% 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 class="textonly"> </span><span style="margin-left:0.5em;color:white;">(You)</span>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@ -99,7 +99,7 @@
|
|||
<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="--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 %}
|
||||
{% 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>
|
||||
|
|
|
@ -96,3 +96,13 @@ def token_playlist(token):
|
|||
if line == '#EXT-X-ENDLIST':
|
||||
raise FileNotFoundError
|
||||
return '\n'.join(m3u8)
|
||||
|
||||
def readable_uptime():
|
||||
uptime = get_start(relative=False)
|
||||
if uptime == None:
|
||||
return None
|
||||
hours, uptime = divmod(uptime, 3600)
|
||||
minutes, seconds = divmod(uptime, 60)
|
||||
if hours:
|
||||
return f'{hours}:{minutes:02}:{seconds:02}'
|
||||
return f'{minutes:02}:{seconds:02}'
|
||||
|
|
|
@ -205,3 +205,12 @@ def remove_absent_viewers():
|
|||
viewers.pop(token)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def is_allowed(token):
|
||||
viewer = viewers.get(token)
|
||||
if viewer and viewer.get('broadcaster'):
|
||||
return True
|
||||
secret_club = CONFIG['secret_club']
|
||||
if not secret_club['active']:
|
||||
return True
|
||||
return viewer and viewer['tripcode']['string'] in secret_club['allowed_trips']
|
||||
|
|
読み込み中…
新しいイシューから参照