コミットを比較

...

9 コミット

作成者 SHA1 メッセージ 日付
n9k c07910b6c5 v1.2.1 2022-06-15 21:19:21 +00:00
n9k 288c31a03c Merge branch 'dev' 2022-06-15 21:18:04 +00:00
n9k 50d03ba8d5 Control socket: specify users by token hash 2022-06-15 21:17:25 +00:00
n9k f3e58fd3fa Refactor info update background task
We now time the interval between consecutive tasks. This is more precise
than using the constant interval the task is supposed to run at since
there is some drift on each run (~0.004s).
2022-06-15 21:03:08 +00:00
n9k e449caff5f Reimplement `with_timestamp`, allow ints & floats 2022-06-15 20:54:55 +00:00
n9k 1f56e635b9 Ensure chat stays at bottom if names/tripcodes change 2022-06-15 20:39:06 +00:00
n9k 55a713991c Nojs info: fix invisible uptime counter taking up space
Also adds "visibility: hidden;" to `disappear`. It would replace
"opacity: 0;" but Firefox acts weird with only visibility set, it only
half works until you switch between desktop and mobile view. IDK this
isn't that important.
2022-06-15 10:14:37 +00:00
n9k 46cd032510 CSS: always fullheight info in desktop view 2022-06-15 10:10:04 +00:00
n9k d06a279be6 Sanitize newlines in usernames with js
Previously usernames with newlines in them actually went over multiple
lines.
2022-06-15 09:38:47 +00:00
13個のファイルの変更121行の追加80行の削除

ファイルの表示

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

ファイルの表示

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

ファイルの表示

@ -20,10 +20,19 @@ class ArgsJsonTokenUser(ArgsJsonString):
raise NoParse(f'no user with token {token!r}')
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):
return Str({
'token': ArgsJsonTokenUser(spec),
#'hash': ArgsJsonHashUser(spec),
'hash': ArgsJsonHashUser(spec),
})
async def cmd_user_help():
@ -31,19 +40,18 @@ async def cmd_user_help():
response = (
'Usage: user [show | attr USER | get USER ATTR | set USER ATTR VALUE]\n'
'Commands:\n'
' user [show].......................show all users\' tokens\n'
' user attr USER....................show names of a user\'s attributes\n'
' user get USER ATTR................show 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 delete EYES_ID.....end a video response to a user\n'
' user [show]...................show all users\' tokens\n'
' user attr USER................show names of a user\'s attributes\n'
' user get USER ATTR............show 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 delete EYES_ID.end a video response to a user\n'
'Definitions:\n'
#' USER..............................={token TOKEN | hash HASH}\n'
' USER..............................=token TOKEN\n'
' TOKEN..............................a token, json string\n'
#' HASH..............................a token hash\n'
' ATTR...............................a user attribute, re:[a-z0-9_]+\n'
' EYES_ID............................a user\'s eyes_id, base 10 integer\n'
' USER..........................={token TOKEN | hash HASH}\n'
' TOKEN.........................a token, json string\n'
' HASH..........................a token hash\n'
' ATTR..........................a user attribute, re:[a-z0-9_]+\n'
' EYES_ID.......................a user\'s eyes_id, base 10 integer\n'
)
return normal, response

ファイルの表示

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

ファイルの表示

@ -215,7 +215,7 @@ const create_chat_message = (object) => {
const create_chat_user_name = (user) => {
const chat_user_name = document.createElement("span");
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
if (!user.broadcaster && user.name === null) {
const b = document.createElement("b");
@ -648,6 +648,10 @@ const on_websocket_message = (event) => {
default_name = receipt.default;
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
users = receipt.users;
update_user_names();
@ -655,6 +659,15 @@ const on_websocket_message = (event) => {
update_user_tripcodes();
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
const user = users[TOKEN_HASH];
if (user.name !== null) {
@ -739,10 +752,26 @@ const on_websocket_message = (event) => {
for (const token_hash of Object.keys(receipt.users)) {
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_colors();
update_user_tripcodes();
update_users_list()
// ensure chat scroll (see above)
if (at_bottom) {
chat_messages.scrollTo({
left: 0,
top: chat_messages.scrollTopMax,
behavior: "instant",
});
}
break;
case "rem-users":

ファイルの表示

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

ファイルの表示

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

ファイルの表示

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

ファイルの表示

@ -41,7 +41,7 @@
<a href="#chat">chat</a>
<a href="#both">both</a>
</nav>
<footer>anonstream 1.2.0 &mdash; <a href="https://git.076.ne.jp/ninya9k/anonstream" target="_blank">source</a></footer>
<footer>anonstream 1.2.1 &mdash; <a href="https://git.076.ne.jp/ninya9k/anonstream" target="_blank">source</a></footer>
<script src="{{ url_for('static', filename='anonstream.js') }}" type="text/javascript"></script>
</body>
</html>

ファイルの表示

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

ファイルの表示

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

ファイルの表示

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

ファイルの表示

@ -4,23 +4,25 @@
import time
from functools import wraps
def with_timestamp(f):
@wraps(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):
def with_function_call(fn, *fn_args, **fn_kwargs):
def with_result(f):
@wraps(f)
def wrapper(*args, **kwargs):
return f(x, *args, **kwargs)
result = fn(*fn_args, **fn_kwargs)
return f(result, *args, **kwargs)
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_specific(f):
@ -30,9 +32,7 @@ def try_except_log(errors, exception_class):
return f(*args, **kwargs)
except exception_class as e:
errors.append(e)
return wrapper
return try_except_log_specific
def ttl_cache(ttl):
@ -42,19 +42,14 @@ def ttl_cache(ttl):
'''
def ttl_cache_specific(f):
value, expires = None, None
@wraps(f)
def wrapper():
nonlocal value, expires
if expires is None or time.monotonic() >= expires:
value = f()
expires = time.monotonic() + ttl
return value
return wrapper
return ttl_cache_specific
def ttl_cache_async(ttl):
@ -63,17 +58,12 @@ def ttl_cache_async(ttl):
'''
def ttl_cache_specific(f):
value, expires = None, None
@wraps(f)
async def wrapper():
nonlocal value, expires
if expires is None or time.monotonic() >= expires:
value = await f()
expires = time.monotonic() + ttl
return value
return wrapper
return ttl_cache_specific