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):
cfg = config['tasks']
return {
'TASK_ROTATE_EYES': cfg['rotate_eyes'],
'TASK_ROTATE_USERS': cfg['rotate_users'],
'TASK_ROTATE_CAPTCHAS': cfg['rotate_captchas'],
'TASK_ROTATE_WEBSOCKETS': cfg['rotate_websockets'],
@ -112,11 +113,16 @@ def toml_to_flask_section_chat(config):
def toml_to_flask_section_flood(config):
cfg = config['flood']
assert cfg['video']['max_eyes'] >= 0
return {
'FLOOD_MESSAGE_DURATION': cfg['messages']['duration'],
'FLOOD_MESSAGE_THRESHOLD': cfg['messages']['threshold'],
'FLOOD_LINE_DURATION': cfg['lines']['duration'],
'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):

ファイルの表示

@ -6,7 +6,7 @@ from quart.asgi import ASGIHTTPConnection as ASGIHTTPConnection_
from quart.utils import encode_headers
RESPONSE_ITERATOR_TIMEOUT = 10
RESPONSE_ITERATOR_TIMEOUT = 10.0
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.segments import segments, StopSendingSegments
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.utils.security import generate_csp
@ -25,12 +25,16 @@ async def stream(user):
if not is_online():
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):
try:
renew_eyes(user, eyes_id)
except ExpiredEyes as e:
raise StopSendingSegments(f'eyes {eyes_id} expired: {e}') from e
renew_eyes(user, eyes_id, just_read_new_segment=True)
except EyesException as e:
raise StopSendingSegments(f'eyes {eyes_id} not allowed: {e!r}') from e
print(f'{uri}: {eyes_id}~{user["token"]}')
watched(user)

ファイルの表示

@ -43,6 +43,21 @@ def with_period(period):
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_timestamp
async def t_sunset_users(timestamp, iteration):
@ -166,6 +181,7 @@ async def t_broadcast_stream_info_update(iteration):
if 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_expire_captchas)
current_app.add_background_task(t_close_websockets)

ファイルの表示

@ -25,7 +25,16 @@ class BadAppearance(ValueError):
class BadCaptcha(ValueError):
pass
class ExpiredEyes(Exception):
class EyesException(Exception):
pass
class TooManyEyes(EyesException):
pass
class DeletedEyes(EyesException):
pass
class ExpiredEyes(EyesException):
pass
def add_state(user, **state):
@ -225,6 +234,16 @@ def get_users_by_presence(timestamp):
@with_timestamp
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']
user['eyes']['total'] += 1
user['eyes']['current'][eyes_id] = {
@ -238,12 +257,15 @@ def create_eyes(timestamp, user, headers):
return eyes_id
@with_timestamp
def renew_eyes(timestamp, user, eyes_id):
def renew_eyes(timestamp, user, eyes_id, just_read_new_segment=False):
try:
eyes = user['eyes']['current'][eyes_id]
except KeyError:
raise ExpiredEyes(None)
if timestamp - eyes['renewed'] >= 20.0: # TODO remove magic number
raise DeletedEyes(eyes_id)
renewed_ago = timestamp - eyes['renewed']
if renewed_ago >= CONFIG['FLOOD_VIDEO_EYES_EXPIRE_AFTER']:
user['eyes']['current'].pop(eyes_id)
raise ExpiredEyes(eyes)
eyes['renewed'] = timestamp
if just_read_new_segment:
eyes['n_segments'] += 1

ファイルの表示

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