From 7bf84413e6e920cf359b4632d5dcfe07ae5c305b Mon Sep 17 00:00:00 2001 From: ninya9k Date: Sat, 17 Jul 2021 04:57:55 +0000 Subject: [PATCH] secret club mode (private/whitelist); usability improvements in text browsers --- website/routes.py | 99 ++++++++++++++---- website/static/eye.png | Bin 0 -> 3930 bytes website/static/platform.css | 6 ++ website/templates/chat-iframe.html | 15 ++- website/templates/comment-confirm-iframe.html | 34 ++++++ website/templates/index.html | 8 ++ website/templates/secret-club.html | 23 ++++ website/templates/users-iframe.html | 4 +- website/utils/stream.py | 10 ++ website/viewership.py | 9 ++ 10 files changed, 180 insertions(+), 28 deletions(-) create mode 100644 website/static/eye.png create mode 100644 website/templates/comment-confirm-iframe.html create mode 100644 website/templates/secret-club.html diff --git a/website/routes.py b/website/routes.py index e3f3e85..03faa6e 100644 --- a/website/routes.py +++ b/website/routes.py @@ -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/') 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/') 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) diff --git a/website/static/eye.png b/website/static/eye.png new file mode 100644 index 0000000000000000000000000000000000000000..a8ea09d80b7af54bc05405e40fd0446d711df230 GIT binary patch literal 3930 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 literal 0 HcmV?d00001 diff --git a/website/static/platform.css b/website/static/platform.css index 5ca9b76..8d60171 100644 --- a/website/static/platform.css +++ b/website/static/platform.css @@ -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 +} diff --git a/website/templates/chat-iframe.html b/website/templates/chat-iframe.html index 74db767..fb03093 100644 --- a/website/templates/chat-iframe.html +++ b/website/templates/chat-iframe.html @@ -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 @@ +
+
+
{{ stream_title }}
+
{{ stream_viewers }} viewer(s), {% if uptime %}{{ stream_uptime }} uptime{% else %}offline{% endif %}
+
+
== Chat ==
+
{% if broadcaster %}
@@ -109,7 +116,7 @@
{% endif %} - +
{% for message in messages %} {% if message.get('special') == 'date' %} @@ -171,7 +178,7 @@
Users watching ({{ len(people['watching']) }})
{% for person in people['watching'] %}
- {% if broadcaster %}{% endif %}{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}{{ person['tag'] }}{% endif %}{% if person['tripcode']['string'] %}{% if tag %}{% endif %} <{{ person['tripcode']['string'] }}>{% endif %}{% endwith %}{% if person['token'] == token %}(You){% endif %} + {% if broadcaster %}{% endif %}{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}{{ person['tag'] }}{% endif %}{% if person['tripcode']['string'] %}{% if tag %}{% endif %} <{{ person['tripcode']['string'] }}>{% endif %}{% endwith %}{% if person['token'] == token %} (You){% endif %}
{% endfor %} @@ -180,7 +187,7 @@
Users not watching ({{ len(people['not_watching']) }})
{% for person in people['not_watching'] %}
- {% if broadcaster %}{% endif %}{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}{{ person['tag'] }}{% endif %}{% if person['tripcode']['string'] %}{% if tag %}{% endif %} <{{ person['tripcode']['string'] }}>{% endif %}{% endwith %}{% if person['token'] == token %}(You){% endif %} + {% if broadcaster %}{% endif %}{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}{{ person['tag'] }}{% endif %}{% if person['tripcode']['string'] %}{% if tag %}{% endif %} <{{ person['tripcode']['string'] }}>{% endif %}{% endwith %}{% if person['token'] == token %} (You){% endif %}
{% endfor %} diff --git a/website/templates/comment-confirm-iframe.html b/website/templates/comment-confirm-iframe.html new file mode 100644 index 0000000..3b23409 --- /dev/null +++ b/website/templates/comment-confirm-iframe.html @@ -0,0 +1,34 @@ + + + + + + + + + + +
The stream is in secret club mode. Changing your tripcode would kick you out of the secret club. Click here to stay in the secret club, or to get kicked out.
+ + + diff --git a/website/templates/index.html b/website/templates/index.html index de7ac66..1757592 100644 --- a/website/templates/index.html +++ b/website/templates/index.html @@ -120,6 +120,14 @@ +
+
+
== Quick links for text-based browsers ==
+ + + +
If the stream is in secret club mode, the token component is necessary.
+