2022-03-07 23:51:59 +09:00
|
|
|
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
|
|
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
|
2022-02-19 12:14:37 +09:00
|
|
|
import asyncio
|
2022-02-23 07:51:29 +09:00
|
|
|
import itertools
|
2022-02-19 12:14:37 +09:00
|
|
|
from functools import wraps
|
|
|
|
|
2022-04-02 13:46:24 +09:00
|
|
|
from quart import current_app, websocket
|
2022-02-19 12:14:37 +09:00
|
|
|
|
2022-02-20 16:20:43 +09:00
|
|
|
from anonstream.broadcast import broadcast, broadcast_users_update
|
2022-02-28 20:01:24 +09:00
|
|
|
from anonstream.stream import is_online, get_stream_title, get_stream_uptime_and_viewership
|
2022-02-27 10:01:32 +09:00
|
|
|
from anonstream.user import get_sunsettable_users
|
2022-02-19 12:14:37 +09:00
|
|
|
from anonstream.wrappers import with_timestamp
|
|
|
|
|
|
|
|
CONFIG = current_app.config
|
|
|
|
MESSAGES = current_app.messages
|
|
|
|
USERS_BY_TOKEN = current_app.users_by_token
|
|
|
|
USERS = current_app.users
|
2022-02-20 18:09:35 +09:00
|
|
|
CAPTCHAS = current_app.captchas
|
|
|
|
CAPTCHA_SIGNER = current_app.captcha_signer
|
2022-02-19 12:14:37 +09:00
|
|
|
|
2022-02-20 10:06:13 +09:00
|
|
|
async def sleep_and_collect_task(delay):
|
|
|
|
coro = asyncio.sleep(delay)
|
|
|
|
task = asyncio.create_task(coro)
|
|
|
|
current_app.background_sleep.add(task)
|
|
|
|
try:
|
|
|
|
await task
|
|
|
|
finally:
|
|
|
|
current_app.background_sleep.remove(task)
|
|
|
|
|
2022-02-19 12:14:37 +09:00
|
|
|
def with_period(period):
|
|
|
|
def periodically(f):
|
|
|
|
@wraps(f)
|
|
|
|
async def wrapper(*args, **kwargs):
|
2022-02-23 07:51:29 +09:00
|
|
|
for iteration in itertools.count():
|
|
|
|
await f(iteration, *args, **kwargs)
|
2022-02-20 10:06:13 +09:00
|
|
|
try:
|
|
|
|
await sleep_and_collect_task(period)
|
|
|
|
except asyncio.CancelledError:
|
2022-02-19 12:14:37 +09:00
|
|
|
break
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
return periodically
|
|
|
|
|
2022-02-20 16:20:43 +09:00
|
|
|
@with_period(CONFIG['TASK_PERIOD_ROTATE_USERS'])
|
2022-02-19 12:14:37 +09:00
|
|
|
@with_timestamp
|
2022-02-26 08:06:36 +09:00
|
|
|
async def t_sunset_users(timestamp, iteration):
|
2022-02-23 07:51:29 +09:00
|
|
|
if iteration == 0:
|
|
|
|
return
|
|
|
|
|
2022-02-27 10:01:32 +09:00
|
|
|
# Broadcast a users update, in case any users being
|
|
|
|
# removed have been mutated or are new.
|
|
|
|
broadcast_users_update()
|
2022-02-19 12:14:37 +09:00
|
|
|
|
|
|
|
token_hashes = []
|
2022-02-27 10:01:32 +09:00
|
|
|
users = list(get_sunsettable_users(timestamp))
|
|
|
|
while users:
|
|
|
|
user = users.pop()
|
|
|
|
USERS_BY_TOKEN.pop(user['token'])
|
|
|
|
token_hashes.append(user['token_hash'])
|
2022-02-20 16:20:43 +09:00
|
|
|
|
2022-02-19 12:14:37 +09:00
|
|
|
if token_hashes:
|
|
|
|
broadcast(
|
|
|
|
users=USERS,
|
|
|
|
payload={
|
|
|
|
'type': 'rem-users',
|
|
|
|
'token_hashes': token_hashes,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
2022-02-20 18:09:35 +09:00
|
|
|
@with_period(CONFIG['TASK_PERIOD_ROTATE_CAPTCHAS'])
|
2022-02-23 07:51:29 +09:00
|
|
|
async def t_expire_captchas(iteration):
|
|
|
|
if iteration == 0:
|
|
|
|
return
|
|
|
|
|
2022-02-20 18:09:35 +09:00
|
|
|
to_delete = []
|
|
|
|
for digest in CAPTCHAS:
|
|
|
|
valid = CAPTCHA_SIGNER.validate(
|
|
|
|
digest,
|
|
|
|
max_age=CONFIG['CAPTCHA_LIFETIME'],
|
|
|
|
)
|
|
|
|
if not valid:
|
|
|
|
to_delete.append(digest)
|
2022-02-23 07:51:29 +09:00
|
|
|
|
2022-02-20 18:09:35 +09:00
|
|
|
for digest in to_delete:
|
|
|
|
CAPTCHAS.pop(digest)
|
|
|
|
|
2022-04-02 13:46:24 +09:00
|
|
|
@with_period(CONFIG['TASK_PERIOD_ROTATE_WEBSOCKETS'])
|
|
|
|
@with_timestamp
|
|
|
|
async def t_close_websockets(timestamp, iteration):
|
|
|
|
THRESHOLD = CONFIG['TASK_PERIOD_BROADCAST_PING'] * 1.5 + 4.0
|
|
|
|
if iteration == 0:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
for user in USERS:
|
|
|
|
for queue in user['websockets']:
|
|
|
|
last_pong = user['websockets'][queue]
|
|
|
|
last_pong_ago = timestamp - last_pong
|
|
|
|
if last_pong_ago > THRESHOLD:
|
|
|
|
queue.put_nowait({'type': 'close'})
|
|
|
|
|
|
|
|
@with_period(CONFIG['TASK_PERIOD_BROADCAST_PING'])
|
|
|
|
async def t_broadcast_ping(iteration):
|
|
|
|
if iteration == 0:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
broadcast(USERS, payload={'type': 'ping'})
|
|
|
|
|
2022-02-20 16:20:43 +09:00
|
|
|
@with_period(CONFIG['TASK_PERIOD_BROADCAST_USERS_UPDATE'])
|
2022-02-23 07:51:29 +09:00
|
|
|
async def t_broadcast_users_update(iteration):
|
|
|
|
if iteration == 0:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
broadcast_users_update()
|
|
|
|
|
|
|
|
@with_period(CONFIG['TASK_PERIOD_BROADCAST_STREAM_INFO_UPDATE'])
|
|
|
|
async def t_broadcast_stream_info_update(iteration):
|
|
|
|
if iteration == 0:
|
2022-02-26 08:06:36 +09:00
|
|
|
title = await get_stream_title()
|
2022-02-28 20:01:24 +09:00
|
|
|
uptime, viewership = get_stream_uptime_and_viewership()
|
2022-02-26 08:06:36 +09:00
|
|
|
current_app.stream_title = title
|
|
|
|
current_app.stream_uptime = uptime
|
|
|
|
current_app.stream_viewership = viewership
|
2022-02-23 07:51:29 +09:00
|
|
|
else:
|
|
|
|
payload = {}
|
|
|
|
|
|
|
|
title = await get_stream_title()
|
2022-02-28 20:01:24 +09:00
|
|
|
uptime, viewership = get_stream_uptime_and_viewership()
|
|
|
|
|
|
|
|
# Check if the stream title has changed
|
2022-02-23 07:51:29 +09:00
|
|
|
if current_app.stream_title != title:
|
|
|
|
current_app.stream_title = title
|
|
|
|
payload['title'] = title
|
|
|
|
|
2022-02-28 20:01:24 +09:00
|
|
|
# Check if the stream uptime has changed unexpectedly
|
2022-02-23 07:51:29 +09:00
|
|
|
if current_app.stream_uptime is None:
|
|
|
|
expected_uptime = None
|
|
|
|
else:
|
|
|
|
expected_uptime = (
|
|
|
|
current_app.stream_uptime
|
|
|
|
+ CONFIG['TASK_PERIOD_BROADCAST_STREAM_INFO_UPDATE']
|
|
|
|
)
|
|
|
|
current_app.stream_uptime = uptime
|
|
|
|
if uptime is None and expected_uptime is None:
|
2022-02-28 20:01:24 +09:00
|
|
|
stats_changed = False
|
2022-02-23 07:51:29 +09:00
|
|
|
elif uptime is None or expected_uptime is None:
|
2022-02-28 20:01:24 +09:00
|
|
|
stats_changed = True
|
|
|
|
else:
|
|
|
|
stats_changed = abs(uptime - expected_uptime) >= 0.0625
|
2022-02-23 07:51:29 +09:00
|
|
|
|
2022-02-26 08:06:36 +09:00
|
|
|
# Check if viewership has changed
|
|
|
|
if current_app.stream_viewership != viewership:
|
|
|
|
current_app.stream_viewership = viewership
|
2022-02-28 20:01:24 +09:00
|
|
|
stats_changed = True
|
|
|
|
|
|
|
|
if stats_changed:
|
|
|
|
if uptime is None:
|
|
|
|
payload['stats'] = None
|
|
|
|
else:
|
|
|
|
payload['stats'] = {
|
|
|
|
'uptime': uptime,
|
|
|
|
'viewership': viewership,
|
|
|
|
}
|
2022-02-26 08:06:36 +09:00
|
|
|
|
2022-02-23 07:51:29 +09:00
|
|
|
if payload:
|
|
|
|
broadcast(USERS, payload={'type': 'info', **payload})
|
2022-02-20 16:20:43 +09:00
|
|
|
|
2022-02-19 12:14:37 +09:00
|
|
|
current_app.add_background_task(t_sunset_users)
|
2022-02-20 18:09:35 +09:00
|
|
|
current_app.add_background_task(t_expire_captchas)
|
2022-04-02 13:46:24 +09:00
|
|
|
current_app.add_background_task(t_close_websockets)
|
|
|
|
current_app.add_background_task(t_broadcast_ping)
|
2022-02-20 16:20:43 +09:00
|
|
|
current_app.add_background_task(t_broadcast_users_update)
|
2022-02-23 07:51:29 +09:00
|
|
|
current_app.add_background_task(t_broadcast_stream_info_update)
|