secret club mode (private/whitelist); usability improvements in text browsers

このコミットが含まれているのは:
n9k 2021-07-17 04:57:55 +00:00
コミット 7bf84413e6
10個のファイルの変更180行の追加28行の削除

ファイルの表示

@ -29,16 +29,25 @@ def get_token(form=False):
token = None token = None
return token return token
@current_app.route('/') @current_app.route('/', methods=['GET', 'POST'])
def index(token=None): def index(token=None):
token = token or get_token() or new_token() 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: try:
viewership.video_was_corrupted.remove(token) viewership.video_was_corrupted.remove(token)
except KeyError: except KeyError:
pass pass
use_videojs = bool(request.args.get('videojs', default=int(VIDEOJS_ENABLED_BY_DEFAULT), type=int)) use_videojs = bool(request.args.get('videojs', default=int(VIDEOJS_ENABLED_BY_DEFAULT), type=int))
online = stream.is_online() online = stream.is_online()
viewership.made_request(token)
response = render_template('index.html', response = render_template('index.html',
token=token, token=token,
@ -60,11 +69,13 @@ def playlist():
if not stream.is_online(): if not stream.is_online():
return abort(404) return abort(404)
token = get_token() token = get_token()
viewership.made_request(token)
if not viewership.is_allowed(token):
return abort(403)
try: try:
viewership.video_was_corrupted.remove(token) viewership.video_was_corrupted.remove(token)
except KeyError: except KeyError:
pass pass
viewership.made_request(token)
try: try:
token_playlist = stream.token_playlist(token) token_playlist = stream.token_playlist(token)
@ -79,13 +90,15 @@ def playlist():
def segment_init(): def segment_init():
if not stream.is_online(): if not stream.is_online():
return abort(404) return abort(404)
token = get_token() or new_token() token = get_token() or new_token()
viewership.made_request(token)
if not viewership.is_allowed(token):
return abort(403)
try: try:
viewership.video_was_corrupted.remove(token) viewership.video_was_corrupted.remove(token)
except KeyError: except KeyError:
pass pass
viewership.made_request(token)
response = send_from_directory(SEGMENTS_DIR, f'init.mp4', add_etags=False) response = send_from_directory(SEGMENTS_DIR, f'init.mp4', add_etags=False)
response.headers['Cache-Control'] = 'no-cache' response.headers['Cache-Control'] = 'no-cache'
response.set_cookie('token', token) response.set_cookie('token', token)
@ -95,8 +108,9 @@ def segment_init():
def segment_arbitrary(n): def segment_arbitrary(n):
if not stream.is_online(): if not stream.is_online():
return abort(404) return abort(404)
token = get_token() token = get_token()
if not viewership.is_allowed(token):
return abort(403)
try: try:
viewership.video_was_corrupted.remove(token) viewership.video_was_corrupted.remove(token)
except KeyError: except KeyError:
@ -116,11 +130,20 @@ def segments():
if not stream.is_online(): if not stream.is_online():
return abort(404) return abort(404)
token = get_token() or new_token() token = get_token() or new_token()
viewership.made_request(token)
if not viewership.is_allowed(token):
return abort(403)
try: try:
viewership.video_was_corrupted.remove(token) viewership.video_was_corrupted.remove(token)
except KeyError: except KeyError:
pass 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) start_number = request.args.get('segment', type=int)
if start_number == None: if start_number == None:
@ -129,7 +152,7 @@ def segments():
concatenated_segments = ConcatenatedSegments(start_number=start_number, concatenated_segments = ConcatenatedSegments(start_number=start_number,
segment_hook=lambda n: viewership.view_segment(n, token, check_exists=False), segment_hook=lambda n: viewership.view_segment(n, token, check_exists=False),
corrupt_hook=lambda: viewership.video_was_corrupted.add(token), # lock? 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: except FileNotFoundError:
return abort(404) return abort(404)
@ -148,6 +171,8 @@ def segments():
def chat_iframe(): def chat_iframe():
token = get_token() token = get_token()
viewership.made_request(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)) 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 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=token == BROADCASTER_TOKEN,
broadcaster_colour=BROADCASTER_COLOUR, broadcaster_colour=BROADCASTER_COLOUR,
background_colour=BACKGROUND_COLOUR, background_colour=BACKGROUND_COLOUR,
stream_title=stream.get_title(),
stream_viewers=viewership.count(),
stream_uptime=stream.readable_uptime(),
debug=request.args.get('debug'), debug=request.args.get('debug'),
RE_WHITESPACE=RE_WHITESPACE, RE_WHITESPACE=RE_WHITESPACE,
len=len, len=len,
@ -174,6 +202,8 @@ def chat_iframe():
def heartbeat(): def heartbeat():
token = get_token() token = get_token()
viewership.made_request(token) viewership.made_request(token)
if not viewership.is_allowed(token):
return abort(403)
online = stream.is_online() online = stream.is_online()
start_abs, start_rel = stream.get_start(absolute=True, relative=True) start_abs, start_rel = stream.get_start(absolute=True, relative=True)
@ -193,6 +223,8 @@ def heartbeat():
def comment_iframe(token=None): def comment_iframe(token=None):
token = token or get_token() or new_token() token = token or get_token() or new_token()
viewership.made_request(token) viewership.made_request(token)
if not viewership.is_allowed(token):
return abort(403)
try: try:
preset = viewership.preset_comment_iframe.pop(token) preset = viewership.preset_comment_iframe.pop(token)
@ -224,13 +256,15 @@ def comment_iframe(token=None):
@current_app.route('/comment', methods=['POST']) @current_app.route('/comment', methods=['POST'])
def comment(): def comment():
token = get_token(form=True) or new_token() 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') nonce = request.form.get('nonce')
message = request.form.get('message', '').replace('\r', '').replace('\n', ' ').strip() message = request.form.get('message', '').replace('\r', '').replace('\n', ' ').strip()
c_response = request.form.get('captcha') c_response = request.form.get('captcha')
c_ciphertext = request.form.get('captcha-ciphertext') c_ciphertext = request.form.get('captcha-ciphertext')
viewership.made_request(token)
failure_reason = chat.comment(message, token, c_response, c_ciphertext, nonce) 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 ''} 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']) @current_app.route('/settings', methods=['POST'])
def settings(): def settings():
token = get_token(form=True) or new_token() 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', '') nickname = request.form.get('nickname', '')
password = request.form.get('password', '') 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) note, ok = chat.set_nickname(nickname, token)
if ok: if ok:
@ -255,6 +294,13 @@ def settings():
elif request.form.get('set-tripcode'): elif request.form.get('set-tripcode'):
note, _ = chat.set_tripcode(password, token) 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} viewership.preset_comment_iframe[token] = {'note': note, 'show_settings': True}
return redirect(url_for('comment_iframe', token=token)) return redirect(url_for('comment_iframe', token=token))
@ -280,9 +326,11 @@ def mod_users():
@current_app.route('/stream-info') @current_app.route('/stream-info')
def stream_info(): def stream_info():
token = get_token() or new_token() token = get_token() or new_token()
embed_images = bool(request.args.get('embed', type=int))
viewership.made_request(token) 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) start_abs, start_rel = stream.get_start(absolute=True, relative=True)
online = stream.is_online() online = stream.is_online()
@ -301,7 +349,9 @@ def stream_info():
@current_app.route('/users') @current_app.route('/users')
def users(): def users():
token = get_token() 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', return render_template('users-iframe.html',
token=token, token=token,
people=viewership.get_people_list(), people=viewership.get_people_list(),
@ -312,21 +362,26 @@ def users():
background_colour=BACKGROUND_COLOUR, background_colour=BACKGROUND_COLOUR,
len=len) 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>') @current_app.route('/static/<fn>')
def _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 = 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 return response
@current_app.route('/static/external/<fn>') @current_app.route('/static/external/<fn>')
def third_party(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 = send_from_directory(DIR_STATIC_EXTERNAL, fn, add_etags=False)
response.headers['Cache-Control'] = 'public, max-age=604800, immutable' response.headers['Cache-Control'] = 'public, max-age=604800, immutable'
response.expires = response.date + datetime.timedelta(days=7) response.expires = response.date + datetime.timedelta(days=7)

バイナリ
website/static/eye.png ノーマルファイル

バイナリファイルは表示されません。

変更後

幅:  |  高さ:  |  サイズ: 3.8 KiB

ファイルの表示

@ -150,8 +150,14 @@ body.dark-theme {
.dark-theme textarea { .dark-theme textarea {
color: #232323; color: #232323;
} }
/* workaround to make some custom user css work /* workaround to make some custom user css work
e.g. https://raw.githubusercontent.com/33kk/uso-archive/flomaster/data/usercss/2154.user.css */ e.g. https://raw.githubusercontent.com/33kk/uso-archive/flomaster/data/usercss/2154.user.css */
div[class="vjs-text-track-display"] { div[class="vjs-text-track-display"] {
display: none; display: none;
} }
/* for text-based browsers */
.textonly {
display: none
}

ファイルの表示

@ -36,7 +36,7 @@
cursor: default; cursor: default;
} }
.barrier {display:inline-block;margin-right:0.5em;} .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;} .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;} .time {font-size: 80%;color: gray;vertical-align:middle;cursor:default;}
@ -99,6 +99,13 @@
</style> </style>
</head> </head>
<body> <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"> <div id="messages">
{% if broadcaster %} {% if broadcaster %}
<form action="{{ url_for('mod_chat') }}" method="post"> <form action="{{ url_for('mod_chat') }}" method="post">
@ -109,7 +116,7 @@
</div> </div>
{% endif %} {% endif %}
<!-- TODO: mobile tooltip / title --> <!-- 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> <tbody>
{% for message in messages %} {% for message in messages %}
{% if message.get('special') == 'date' %} {% if message.get('special') == 'date' %}
@ -171,7 +178,7 @@
<div class="group-name">Users watching ({{ len(people['watching']) }})</div> <div class="group-name">Users watching ({{ len(people['watching']) }})</div>
{% for person in people['watching'] %} {% for person in people['watching'] %}
<div class="person"> <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"> &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 %} {% 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> </div>
{% endfor %} {% endfor %}
</div> </div>
@ -180,7 +187,7 @@
<div class="group-name">Users not watching ({{ len(people['not_watching']) }})</div> <div class="group-name">Users not watching ({{ len(people['not_watching']) }})</div>
{% for person in people['not_watching'] %} {% for person in people['not_watching'] %}
<div class="person"> <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"> &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 %} {% 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> </div>
{% endfor %} {% endfor %}
</div> </div>

34
website/templates/comment-confirm-iframe.html ノーマルファイル
ファイルの表示

@ -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>
</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> <script>
/* replace noscript frame URLs with with-script versions */ /* replace noscript frame URLs with with-script versions */
function replaceFrameURL(frameContainerId, newUrl, newId, newName) { function replaceFrameURL(frameContainerId, newUrl, newId, newName) {

23
website/templates/secret-club.html ノーマルファイル
ファイルの表示

@ -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> <div class="group-name">Users watching ({{ len(people['watching']) }})</div>
{% for person in people['watching'] %} {% for person in people['watching'] %}
<div class="person"> <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"> &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 %} {% 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 class="textonly"> </span><span style="margin-left:0.5em;color:white;">(You)</span>{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@ -99,7 +99,7 @@
<div class="group-name">Users not watching ({{ len(people['not_watching']) }})</div> <div class="group-name">Users not watching ({{ len(people['not_watching']) }})</div>
{% for person in people['not_watching'] %} {% for person in people['not_watching'] %}
<div class="person"> <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"> &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 %} {% 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> </div>
{% endfor %} {% endfor %}
</div> </div>

ファイルの表示

@ -96,3 +96,13 @@ def token_playlist(token):
if line == '#EXT-X-ENDLIST': if line == '#EXT-X-ENDLIST':
raise FileNotFoundError raise FileNotFoundError
return '\n'.join(m3u8) 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) viewers.pop(token)
except KeyError: except KeyError:
pass 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']