Eyes: delete old eyes

Also implements stack/queue behaviour where if the eyes limit would be
exceeded, either the new eyes cause the oldest eyes to be deleted OR
the new eyes aren't created at all. The default is the first option.
このコミットが含まれているのは:
n9k 2022-06-14 02:44:08 +00:00
コミット 51265fb277
6個のファイルの変更65行の追加10行の削除

ファイルの表示

@ -82,6 +82,7 @@ def toml_to_flask_section_memory(config):
def toml_to_flask_section_tasks(config): def toml_to_flask_section_tasks(config):
cfg = config['tasks'] cfg = config['tasks']
return { return {
'TASK_ROTATE_EYES': cfg['rotate_eyes'],
'TASK_ROTATE_USERS': cfg['rotate_users'], 'TASK_ROTATE_USERS': cfg['rotate_users'],
'TASK_ROTATE_CAPTCHAS': cfg['rotate_captchas'], 'TASK_ROTATE_CAPTCHAS': cfg['rotate_captchas'],
'TASK_ROTATE_WEBSOCKETS': cfg['rotate_websockets'], 'TASK_ROTATE_WEBSOCKETS': cfg['rotate_websockets'],
@ -112,11 +113,16 @@ def toml_to_flask_section_chat(config):
def toml_to_flask_section_flood(config): def toml_to_flask_section_flood(config):
cfg = config['flood'] cfg = config['flood']
assert cfg['video']['max_eyes'] >= 0
return { return {
'FLOOD_MESSAGE_DURATION': cfg['messages']['duration'], 'FLOOD_MESSAGE_DURATION': cfg['messages']['duration'],
'FLOOD_MESSAGE_THRESHOLD': cfg['messages']['threshold'], 'FLOOD_MESSAGE_THRESHOLD': cfg['messages']['threshold'],
'FLOOD_LINE_DURATION': cfg['lines']['duration'], 'FLOOD_LINE_DURATION': cfg['lines']['duration'],
'FLOOD_LINE_THRESHOLD': cfg['lines']['threshold'], 'FLOOD_LINE_THRESHOLD': cfg['lines']['threshold'],
'FLOOD_VIDEO_MAX_EYES': cfg['video']['max_eyes'],
#'FLOOD_VIDEO_COOLDOWN': cfg['video']['cooldown'],
'FLOOD_VIDEO_EYES_EXPIRE_AFTER': cfg['video']['expire_after'],
'FLOOD_VIDEO_OVERWRITE': cfg['video']['overwrite'],
} }
def toml_to_flask_section_captcha(config): def toml_to_flask_section_captcha(config):

ファイルの表示

@ -6,7 +6,7 @@ from quart.asgi import ASGIHTTPConnection as ASGIHTTPConnection_
from quart.utils import encode_headers from quart.utils import encode_headers
RESPONSE_ITERATOR_TIMEOUT = 10 RESPONSE_ITERATOR_TIMEOUT = 10.0
class ASGIHTTPConnection(ASGIHTTPConnection_): class ASGIHTTPConnection(ASGIHTTPConnection_):

ファイルの表示

@ -6,7 +6,7 @@ from quart import current_app, request, render_template, abort, make_response, r
from anonstream.captcha import get_captcha_image from anonstream.captcha import get_captcha_image
from anonstream.segments import segments, StopSendingSegments from anonstream.segments import segments, StopSendingSegments
from anonstream.stream import is_online, get_stream_uptime from anonstream.stream import is_online, get_stream_uptime
from anonstream.user import watched, create_eyes, renew_eyes, ExpiredEyes from anonstream.user import watched, create_eyes, renew_eyes, EyesException
from anonstream.routes.wrappers import with_user_from, auth_required from anonstream.routes.wrappers import with_user_from, auth_required
from anonstream.utils.security import generate_csp from anonstream.utils.security import generate_csp
@ -25,12 +25,16 @@ async def stream(user):
if not is_online(): if not is_online():
return abort(404) return abort(404)
eyes_id = create_eyes(user, dict(request.headers)) try:
eyes_id = create_eyes(user, dict(request.headers))
except EyesException:
return abort(429)
def segment_read_hook(uri): def segment_read_hook(uri):
try: try:
renew_eyes(user, eyes_id) renew_eyes(user, eyes_id, just_read_new_segment=True)
except ExpiredEyes as e: except EyesException as e:
raise StopSendingSegments(f'eyes {eyes_id} expired: {e}') from e raise StopSendingSegments(f'eyes {eyes_id} not allowed: {e!r}') from e
print(f'{uri}: {eyes_id}~{user["token"]}') print(f'{uri}: {eyes_id}~{user["token"]}')
watched(user) watched(user)

ファイルの表示

@ -43,6 +43,21 @@ def with_period(period):
return periodically return periodically
@with_period(CONFIG['TASK_ROTATE_EYES'])
@with_timestamp
async def t_delete_eyes(timestamp, iteration):
if iteration == 0:
return
else:
for user in USERS:
to_delete = []
for eyes_id, eyes in user['eyes']['current'].items():
renewed_ago = timestamp - eyes['renewed']
if renewed_ago >= CONFIG['FLOOD_VIDEO_EYES_EXPIRE_AFTER']:
to_delete.append(eyes_id)
for eyes_id in to_delete:
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):
@ -166,6 +181,7 @@ async def t_broadcast_stream_info_update(iteration):
if payload: if payload:
broadcast(USERS, payload={'type': 'info', **payload}) broadcast(USERS, payload={'type': 'info', **payload})
current_app.add_background_task(t_delete_eyes)
current_app.add_background_task(t_sunset_users) current_app.add_background_task(t_sunset_users)
current_app.add_background_task(t_expire_captchas) current_app.add_background_task(t_expire_captchas)
current_app.add_background_task(t_close_websockets) current_app.add_background_task(t_close_websockets)

ファイルの表示

@ -25,7 +25,16 @@ class BadAppearance(ValueError):
class BadCaptcha(ValueError): class BadCaptcha(ValueError):
pass pass
class ExpiredEyes(Exception): class EyesException(Exception):
pass
class TooManyEyes(EyesException):
pass
class DeletedEyes(EyesException):
pass
class ExpiredEyes(EyesException):
pass pass
def add_state(user, **state): def add_state(user, **state):
@ -225,6 +234,16 @@ def get_users_by_presence(timestamp):
@with_timestamp @with_timestamp
def create_eyes(timestamp, user, headers): def create_eyes(timestamp, user, headers):
if len(user['eyes']['current']) >= CONFIG['FLOOD_VIDEO_MAX_EYES']:
# treat eyes as a stack, do not create new eyes if it would
# cause the limit to be exceeded
if not CONFIG['FLOOD_VIDEO_OVERWRITE']:
raise TooManyEyes
# treat eyes as a queue, expire old eyes upon creating new eyes
# if the limit would have been exceeded
elif user['eyes']['current']:
oldest_eyes_id = min(user['eyes']['current'])
user['eyes']['current'].pop(oldest_eyes_id)
eyes_id = user['eyes']['total'] eyes_id = user['eyes']['total']
user['eyes']['total'] += 1 user['eyes']['total'] += 1
user['eyes']['current'][eyes_id] = { user['eyes']['current'][eyes_id] = {
@ -238,12 +257,15 @@ def create_eyes(timestamp, user, headers):
return eyes_id return eyes_id
@with_timestamp @with_timestamp
def renew_eyes(timestamp, user, eyes_id): 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]
except KeyError: except KeyError:
raise ExpiredEyes(None) raise DeletedEyes(eyes_id)
if timestamp - eyes['renewed'] >= 20.0: # TODO remove magic number renewed_ago = timestamp - eyes['renewed']
if renewed_ago >= CONFIG['FLOOD_VIDEO_EYES_EXPIRE_AFTER']:
user['eyes']['current'].pop(eyes_id) user['eyes']['current'].pop(eyes_id)
raise ExpiredEyes(eyes) raise ExpiredEyes(eyes)
eyes['renewed'] = timestamp eyes['renewed'] = timestamp
if just_read_new_segment:
eyes['n_segments'] += 1

ファイルの表示

@ -34,6 +34,7 @@ chat_messages = 8192
chat_scrollback = 256 chat_scrollback = 256
[tasks] [tasks]
rotate_eyes = 3.0
rotate_users = 60.0 rotate_users = 60.0
rotate_captchas = 60.0 rotate_captchas = 60.0
rotate_websockets = 2.0 rotate_websockets = 2.0
@ -60,6 +61,12 @@ threshold = 4
duration = 20.0 duration = 20.0
threshold = 20 threshold = 20
[flood.video]
max_eyes = 3
#cooldown = 12.0
expire_after = 5.0
overwrite = true
[thresholds] [thresholds]
user_notwatching = 8.0 user_notwatching = 8.0
user_tentative = 20.0 user_tentative = 20.0