Merge branch 'dev'

このコミットが含まれているのは:
n9k 2022-06-15 21:18:04 +00:00
コミット 288c31a03c
12個のファイルの変更120行の追加79行の削除

ファイルの表示

@ -47,6 +47,7 @@ def create_app(config_file):
app.stream_title = None app.stream_title = None
app.stream_uptime = None app.stream_uptime = None
app.stream_viewership = None app.stream_viewership = None
app.last_info_task = None
# Background tasks' asyncio.sleep tasks, cancelled on shutdown # Background tasks' asyncio.sleep tasks, cancelled on shutdown
app.background_sleep = set() app.background_sleep = set()

ファイルの表示

@ -62,11 +62,16 @@ class ArgsInt(Args):
return self.spec, 1, [n] return self.spec, 1, [n]
class ArgsString(Args): class ArgsString(Args):
def transform_string(self, string):
return string
def consume(self, words, index): def consume(self, words, index):
try: try:
string = words[index] string = words[index]
except IndexError: except IndexError:
raise NoParse(f'incomplete: expected string') raise NoParse(f'incomplete: expected string')
else:
string = self.transform_string(string)
return self.spec, 1, [string] return self.spec, 1, [string]
class ArgsJson(Args): class ArgsJson(Args):

ファイルの表示

@ -20,10 +20,19 @@ class ArgsJsonTokenUser(ArgsJsonString):
raise NoParse(f'no user with token {token!r}') raise NoParse(f'no user with token {token!r}')
return user return user
class ArgsJsonHashUser(ArgsString):
def transform_string(self, token_hash):
for user in USERS:
if user['token_hash'] == token_hash:
break
else:
raise NoParse(f'no user with token_hash {token_hash!r}')
return user
def ArgsUser(spec): def ArgsUser(spec):
return Str({ return Str({
'token': ArgsJsonTokenUser(spec), 'token': ArgsJsonTokenUser(spec),
#'hash': ArgsJsonHashUser(spec), 'hash': ArgsJsonHashUser(spec),
}) })
async def cmd_user_help(): async def cmd_user_help():
@ -31,19 +40,18 @@ async def cmd_user_help():
response = ( response = (
'Usage: user [show | attr USER | get USER ATTR | set USER ATTR VALUE]\n' 'Usage: user [show | attr USER | get USER ATTR | set USER ATTR VALUE]\n'
'Commands:\n' 'Commands:\n'
' user [show].......................show all users\' tokens\n' ' user [show]...................show all users\' tokens\n'
' user attr USER....................show names of a user\'s attributes\n' ' user attr USER................show names of a user\'s attributes\n'
' user get USER ATTR................show an attribute of a user\n' ' user get USER ATTR............show an attribute of a user\n'
' user set USER ATTR................set an attribute of a user\n' ' user set USER ATTR............set an attribute of a user\n'
' user eyes USER [show].............show a user\'s active video responses\n' ' user eyes USER [show].........show a user\'s active video responses\n'
' user eyes USER delete EYES_ID.....end a video response to a user\n' ' user eyes USER delete EYES_ID.end a video response to a user\n'
'Definitions:\n' 'Definitions:\n'
#' USER..............................={token TOKEN | hash HASH}\n' ' USER..........................={token TOKEN | hash HASH}\n'
' USER..............................=token TOKEN\n' ' TOKEN.........................a token, json string\n'
' TOKEN..............................a token, json string\n' ' HASH..........................a token hash\n'
#' HASH..............................a token hash\n' ' ATTR..........................a user attribute, re:[a-z0-9_]+\n'
' ATTR...............................a user attribute, re:[a-z0-9_]+\n' ' EYES_ID.......................a user\'s eyes_id, base 10 integer\n'
' EYES_ID............................a user\'s eyes_id, base 10 integer\n'
) )
return normal, response return normal, response

ファイルの表示

@ -9,7 +9,7 @@ import aiofiles
import m3u8 import m3u8
from quart import current_app from quart import current_app
from anonstream.wrappers import ttl_cache, with_timestamp from anonstream.wrappers import ttl_cache
CONFIG = current_app.config CONFIG = current_app.config

ファイルの表示

@ -215,7 +215,7 @@ const create_chat_message = (object) => {
const create_chat_user_name = (user) => { const create_chat_user_name = (user) => {
const chat_user_name = document.createElement("span"); const chat_user_name = document.createElement("span");
chat_user_name.classList.add("chat-name"); chat_user_name.classList.add("chat-name");
chat_user_name.innerText = get_user_name({user}); chat_user_name.innerText = get_user_name({user}).replaceAll(/\r?\n/g, " ");
//chat_user_name.dataset.color = user.color; // not working in any browser //chat_user_name.dataset.color = user.color; // not working in any browser
if (!user.broadcaster && user.name === null) { if (!user.broadcaster && user.name === null) {
const b = document.createElement("b"); const b = document.createElement("b");
@ -648,6 +648,10 @@ const on_websocket_message = (event) => {
default_name = receipt.default; default_name = receipt.default;
max_chat_scrollback = receipt.scrollback; max_chat_scrollback = receipt.scrollback;
// if the chat is scrolled all the way to the bottom, make sure this is
// still the case after updating user names and tripcodes
at_bottom = chat_messages.scrollTop === chat_messages.scrollTopMax;
// update users // update users
users = receipt.users; users = receipt.users;
update_user_names(); update_user_names();
@ -655,6 +659,15 @@ const on_websocket_message = (event) => {
update_user_tripcodes(); update_user_tripcodes();
update_users_list() update_users_list()
// ensure chat scroll (see above)
if (at_bottom) {
chat_messages.scrollTo({
left: 0,
top: chat_messages.scrollTopMax,
behavior: "instant",
});
}
// appearance form default values // appearance form default values
const user = users[TOKEN_HASH]; const user = users[TOKEN_HASH];
if (user.name !== null) { if (user.name !== null) {
@ -739,10 +752,26 @@ const on_websocket_message = (event) => {
for (const token_hash of Object.keys(receipt.users)) { for (const token_hash of Object.keys(receipt.users)) {
users[token_hash] = receipt.users[token_hash]; users[token_hash] = receipt.users[token_hash];
} }
// if the chat is scrolled all the way to the bottom, make sure this is
// still the case after updating user names and tripcodes
at_bottom = chat_messages.scrollTop === chat_messages.scrollTopMax;
// update users
update_user_names(); update_user_names();
update_user_colors(); update_user_colors();
update_user_tripcodes(); update_user_tripcodes();
update_users_list() update_users_list()
// ensure chat scroll (see above)
if (at_bottom) {
chat_messages.scrollTo({
left: 0,
top: chat_messages.scrollTopMax,
behavior: "instant",
});
}
break; break;
case "rem-users": case "rem-users":

ファイルの表示

@ -552,6 +552,9 @@ footer {
#chat-form_js[data-captcha] #chat-live__status [data-verbose="false"] { #chat-form_js[data-captcha] #chat-live__status [data-verbose="false"] {
display: inline; display: inline;
} }
#info_nojs {
height: 100% !important;
}
#nochat:target { #nochat:target {
--chat-width: 0px; --chat-width: 0px;
} }

ファイルの表示

@ -44,7 +44,7 @@ def get_stream_uptime(rounded=True):
uptime = round(uptime, 2) if rounded else uptime uptime = round(uptime, 2) if rounded else uptime
return uptime return uptime
@with_timestamp @with_timestamp()
def get_raw_viewership(timestamp): def get_raw_viewership(timestamp):
users = get_watching_users(timestamp) users = get_watching_users(timestamp)
return max( return max(
@ -52,8 +52,8 @@ def get_raw_viewership(timestamp):
default=0, default=0,
) )
def get_stream_uptime_and_viewership(for_websocket=False): def get_stream_uptime_and_viewership(rounded=True, for_websocket=False):
uptime = get_stream_uptime() uptime = get_stream_uptime(rounded=rounded)
if not for_websocket: if not for_websocket:
viewership = None if uptime is None else get_raw_viewership() viewership = None if uptime is None else get_raw_viewership()
result = (uptime, viewership) result = (uptime, viewership)

ファイルの表示

@ -44,7 +44,7 @@ def with_period(period):
return periodically return periodically
@with_period(CONFIG['TASK_ROTATE_EYES']) @with_period(CONFIG['TASK_ROTATE_EYES'])
@with_timestamp @with_timestamp()
async def t_delete_eyes(timestamp, iteration): async def t_delete_eyes(timestamp, iteration):
if iteration == 0: if iteration == 0:
return return
@ -59,7 +59,7 @@ async def t_delete_eyes(timestamp, iteration):
user['eyes']['current'].pop(eyes_id) user['eyes']['current'].pop(eyes_id)
@with_period(CONFIG['TASK_ROTATE_USERS']) @with_period(CONFIG['TASK_ROTATE_USERS'])
@with_timestamp @with_timestamp()
async def t_sunset_users(timestamp, iteration): async def t_sunset_users(timestamp, iteration):
if iteration == 0: if iteration == 0:
return return
@ -102,7 +102,7 @@ async def t_expire_captchas(iteration):
CAPTCHAS.pop(digest) CAPTCHAS.pop(digest)
@with_period(CONFIG['TASK_ROTATE_WEBSOCKETS']) @with_period(CONFIG['TASK_ROTATE_WEBSOCKETS'])
@with_timestamp @with_timestamp()
async def t_close_websockets(timestamp, iteration): async def t_close_websockets(timestamp, iteration):
THRESHOLD = CONFIG['TASK_BROADCAST_PING'] * 1.5 + 4.0 THRESHOLD = CONFIG['TASK_BROADCAST_PING'] * 1.5 + 4.0
if iteration == 0: if iteration == 0:
@ -130,56 +130,58 @@ async def t_broadcast_users_update(iteration):
broadcast_users_update() broadcast_users_update()
@with_period(CONFIG['TASK_BROADCAST_STREAM_INFO_UPDATE']) @with_period(CONFIG['TASK_BROADCAST_STREAM_INFO_UPDATE'])
async def t_broadcast_stream_info_update(iteration): @with_timestamp(precise=True)
async def t_broadcast_stream_info_update(timestamp, iteration):
if iteration == 0: if iteration == 0:
title = await get_stream_title() title = await get_stream_title()
uptime, viewership = get_stream_uptime_and_viewership() uptime, viewership = get_stream_uptime_and_viewership(rounded=False)
current_app.stream_title = title current_app.stream_title = title
current_app.stream_uptime = uptime current_app.stream_uptime = uptime
current_app.stream_viewership = viewership current_app.stream_viewership = viewership
else: else:
payload = {} info = {}
title = await get_stream_title() title = await get_stream_title()
uptime, viewership = get_stream_uptime_and_viewership() uptime, viewership = get_stream_uptime_and_viewership(rounded=False)
# Check if the stream title has changed # Check if the stream title has changed
if current_app.stream_title != title: if current_app.stream_title != title:
current_app.stream_title = title current_app.stream_title = title
payload['title'] = title info['title'] = title
# Check if the stream uptime has changed unexpectedly # Check if the stream uptime has changed unexpectedly
if current_app.stream_uptime is None: last_uptime, current_app.stream_uptime = (
expected_uptime = None current_app.stream_uptime, uptime
)
if last_uptime is None:
projected_uptime = None
else: else:
expected_uptime = ( last_info_task_ago = timestamp - current_app.last_info_task
current_app.stream_uptime projected_uptime = last_uptime + last_info_task_ago
+ CONFIG['TASK_BROADCAST_STREAM_INFO_UPDATE'] if uptime is None and projected_uptime is None:
)
current_app.stream_uptime = uptime
if uptime is None and expected_uptime is None:
stats_changed = False stats_changed = False
elif uptime is None or expected_uptime is None: elif uptime is None or projected_uptime is None:
stats_changed = True stats_changed = True
else: else:
stats_changed = abs(uptime - expected_uptime) >= 0.5 stats_changed = abs(uptime - projected_uptime) >= 0.5
# Check if viewership has changed # Check if viewership has changed
if current_app.stream_viewership != viewership: if current_app.stream_viewership != viewership:
current_app.stream_viewership = viewership current_app.stream_viewership = viewership
stats_changed = True stats_changed = True
# Broadcast iff anything has changed
if stats_changed: if stats_changed:
if uptime is None: if uptime is None:
payload['stats'] = None info['stats'] = None
else: else:
payload['stats'] = { info['stats'] = {
'uptime': uptime, 'uptime': round(uptime, 2),
'viewership': viewership, 'viewership': viewership,
} }
if info:
broadcast(USERS, payload={'type': 'info', **info})
if payload: current_app.last_info_task = timestamp
broadcast(USERS, payload={'type': 'info', **payload})
current_app.add_background_task(t_delete_eyes) current_app.add_background_task(t_delete_eyes)
current_app.add_background_task(t_sunset_users) current_app.add_background_task(t_sunset_users)

ファイルの表示

@ -75,13 +75,13 @@
animation: appear step-end both; animation: appear step-end both;
} }
#m1 { #m1 {
animation-duration: {{ 600 - uptime }}s; animation-delay: {{ 600 - uptime }}s;
} }
#h0 { #h0 {
animation-duration: {{ 3600 - uptime }}s; animation-delay: {{ 3600 - uptime }}s;
} }
#h1 { #h1 {
animation-duration: {{ 36000 - uptime }}s; animation-delay: {{ 36000 - uptime }}s;
} }
#uptime-dynamic { #uptime-dynamic {
animation: disappear step-end {{ 360000 - uptime }}s forwards; animation: disappear step-end {{ 360000 - uptime }}s forwards;
@ -95,12 +95,15 @@
@keyframes appear { @keyframes appear {
from { from {
width: 0; width: 0;
height: 0;
visibility: hidden; visibility: hidden;
} }
} }
@keyframes disappear { @keyframes disappear {
to { to {
width: 0;
height: 0; height: 0;
visibility: hidden;
opacity: 0; opacity: 0;
} }
} }

ファイルの表示

@ -127,16 +127,16 @@ def change_tripcode(user, password, dry_run=False):
def delete_tripcode(user): def delete_tripcode(user):
user['tripcode'] = None user['tripcode'] = None
@with_timestamp @with_timestamp()
def see(timestamp, user): def see(timestamp, user):
user['last']['seen'] = timestamp user['last']['seen'] = timestamp
@with_timestamp @with_timestamp()
def watched(timestamp, user): def watched(timestamp, user):
user['last']['seen'] = timestamp user['last']['seen'] = timestamp
user['last']['watching'] = timestamp user['last']['watching'] = timestamp
@with_timestamp @with_timestamp()
def get_all_users_for_websocket(timestamp): def get_all_users_for_websocket(timestamp):
return { return {
user['token_hash']: get_user_for_websocket(user) user['token_hash']: get_user_for_websocket(user)
@ -160,7 +160,7 @@ def verify(user, digest, answer):
return verification_happened return verification_happened
@with_timestamp @with_timestamp()
def deverify(timestamp, user): def deverify(timestamp, user):
if not user['verified']: if not user['verified']:
return return
@ -182,7 +182,7 @@ def _update_presence(timestamp, user):
USERS_UPDATE_BUFFER.add(user['token']) USERS_UPDATE_BUFFER.add(user['token'])
return user['presence'] return user['presence']
@with_timestamp @with_timestamp()
def update_presence(timestamp, user): def update_presence(timestamp, user):
return _update_presence(timestamp, user) return _update_presence(timestamp, user)
@ -224,7 +224,7 @@ def get_unsunsettable_users(timestamp):
get_users_and_update_presence(timestamp), get_users_and_update_presence(timestamp),
) )
@with_timestamp @with_timestamp()
def get_users_by_presence(timestamp): def get_users_by_presence(timestamp):
users_by_presence = { users_by_presence = {
Presence.WATCHING: [], Presence.WATCHING: [],
@ -236,7 +236,7 @@ def get_users_by_presence(timestamp):
users_by_presence[user['presence']].append(user) users_by_presence[user['presence']].append(user)
return users_by_presence return users_by_presence
@with_timestamp @with_timestamp(precise=True)
def create_eyes(timestamp, user, headers): def create_eyes(timestamp, user, headers):
# Enforce cooldown # Enforce cooldown
last_created_ago = timestamp - user['last']['eyes'] last_created_ago = timestamp - user['last']['eyes']
@ -271,7 +271,7 @@ def create_eyes(timestamp, user, headers):
} }
return eyes_id return eyes_id
@with_timestamp @with_timestamp(precise=True)
def renew_eyes(timestamp, user, eyes_id, just_read_new_segment=False): def renew_eyes(timestamp, user, eyes_id, just_read_new_segment=False):
try: try:
eyes = user['eyes']['current'][eyes_id] eyes = user['eyes']['current'][eyes_id]

ファイルの表示

@ -72,7 +72,7 @@ async def websocket_inbound(queue, user):
if payload is not None: if payload is not None:
queue.put_nowait(payload) queue.put_nowait(payload)
@with_timestamp @with_timestamp()
def handle_inbound_pong(timestamp, queue, user): def handle_inbound_pong(timestamp, queue, user):
print(f'[pong] {user["token"]}') print(f'[pong] {user["token"]}')
user['websockets'][queue] = timestamp user['websockets'][queue] = timestamp

ファイルの表示

@ -4,23 +4,25 @@
import time import time
from functools import wraps from functools import wraps
def with_timestamp(f): def with_function_call(fn, *fn_args, **fn_kwargs):
@wraps(f) def with_result(f):
def wrapper(*args, **kwargs):
timestamp = int(time.time())
return f(timestamp, *args, **kwargs)
return wrapper
def with_first_argument(x):
def with_x(f):
@wraps(f) @wraps(f)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
return f(x, *args, **kwargs) result = fn(*fn_args, **fn_kwargs)
return f(result, *args, **kwargs)
return wrapper return wrapper
return with_result
return with_x def with_constant(x):
return with_function_call(lambda: x)
def with_timestamp(monotonic=False, precise=False):
n = 1_000_000_000
if monotonic:
fn = precise and time.monotonic or (lambda: time.monotonic_ns() // n)
else:
fn = precise and time.time or (lambda: time.time_ns() // n)
return with_function_call(fn)
def try_except_log(errors, exception_class): def try_except_log(errors, exception_class):
def try_except_log_specific(f): def try_except_log_specific(f):
@ -30,9 +32,7 @@ def try_except_log(errors, exception_class):
return f(*args, **kwargs) return f(*args, **kwargs)
except exception_class as e: except exception_class as e:
errors.append(e) errors.append(e)
return wrapper return wrapper
return try_except_log_specific return try_except_log_specific
def ttl_cache(ttl): def ttl_cache(ttl):
@ -42,19 +42,14 @@ def ttl_cache(ttl):
''' '''
def ttl_cache_specific(f): def ttl_cache_specific(f):
value, expires = None, None value, expires = None, None
@wraps(f) @wraps(f)
def wrapper(): def wrapper():
nonlocal value, expires nonlocal value, expires
if expires is None or time.monotonic() >= expires: if expires is None or time.monotonic() >= expires:
value = f() value = f()
expires = time.monotonic() + ttl expires = time.monotonic() + ttl
return value return value
return wrapper return wrapper
return ttl_cache_specific return ttl_cache_specific
def ttl_cache_async(ttl): def ttl_cache_async(ttl):
@ -63,17 +58,12 @@ def ttl_cache_async(ttl):
''' '''
def ttl_cache_specific(f): def ttl_cache_specific(f):
value, expires = None, None value, expires = None, None
@wraps(f) @wraps(f)
async def wrapper(): async def wrapper():
nonlocal value, expires nonlocal value, expires
if expires is None or time.monotonic() >= expires: if expires is None or time.monotonic() >= expires:
value = await f() value = await f()
expires = time.monotonic() + ttl expires = time.monotonic() + ttl
return value return value
return wrapper return wrapper
return ttl_cache_specific return ttl_cache_specific