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
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)

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

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

変更後

幅:  |  高さ:  |  サイズ: 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"> &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>
{% 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"> &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>
{% endfor %}
</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 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) {

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>
{% 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"> &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>
{% 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"> &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>
{% 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']