Logicaler project structure, see rest of commit message

Incoming requests are handled in anonstream/routes/. Route handlers
mainly depend on files in anonstream/, which in turn depend on files in
anonstream/helpers/ and anonstream/utils/. Utils are pure functions and
helpers are almost pure functions; they don't mutate state but they
do depend on the global app config.
このコミットが含まれているのは:
n9k 2022-02-17 00:20:51 +00:00
コミット 7dbcd43f30
12個のファイルの変更103行の追加84行の削除

ファイルの表示

@ -5,7 +5,7 @@ from collections import OrderedDict
from quart import Quart
from werkzeug.security import generate_password_hash
from anonstream.utils.users import generate_token
from anonstream.utils.user import generate_token
from anonstream.segments import DirectoryCache
async def create_app():

ファイルの表示

@ -3,8 +3,8 @@ from datetime import datetime
from quart import escape
from anonstream.users import users_for_websocket
from anonstream.utils.chat import generate_nonce_hash
from anonstream.user import users_for_websocket
from anonstream.helpers.chat import generate_nonce_hash
class Rejected(Exception):
pass
@ -13,9 +13,9 @@ async def broadcast(websockets, payload):
for queue in websockets:
await queue.put(payload)
async def add_chat_message(chat, users, websockets, secret, user, nonce, comment):
async def add_chat_message(chat, users, websockets, user, nonce, comment):
# check message
nonce_hash = generate_nonce_hash(secret, nonce)
nonce_hash = generate_nonce_hash(nonce)
if nonce_hash in chat['nonce_hashes']:
raise Rejected('Discarded suspected duplicate message')
if len(comment) == 0:

9
anonstream/helpers/chat.py ノーマルファイル
ファイルの表示

@ -0,0 +1,9 @@
import hashlib
from quart import current_app
CONFIG = current_app.config
def generate_nonce_hash(nonce):
parts = CONFIG['SECRET_KEY'] + b'nonce-hash\0' + nonce.encode()
return hashlib.sha256(parts).digest()

54
anonstream/helpers/user.py ノーマルファイル
ファイルの表示

@ -0,0 +1,54 @@
import hashlib
import base64
from collections import OrderedDict
from math import inf
from quart import current_app
CONFIG = current_app.config
def generate_token_hash(token):
parts = CONFIG['SECRET_KEY'] + b'token-hash\0' + token.encode()
digest = hashlib.sha256(parts).digest()
return base64.b32encode(digest)[:26].lower().decode()
def generate_user(secret, token, broadcaster, timestamp):
return {
'token': token,
'token_hash': generate_token_hash(token),
'broadcaster': broadcaster,
'name': None,
'color': '#c7007f',
'tripcode': None,
'notices': OrderedDict(),
'seen': {
'first': timestamp,
'last': timestamp,
},
'watching_last': -inf,
}
def get_default_name(user):
return (
CONFIG['DEFAULT_HOST_NAME']
if user['broadcaster'] else
CONFIG['DEFAULT_ANON_NAME']
)
def is_watching(timestamp, user):
return user['watching_last'] >= timestamp - CONFIG['THRESHOLD_IDLE']
def is_idle(timestamp, user):
return is_present(timestamp, user) and not is_watching(timestamp, user)
def is_present(timestamp, user):
return user['seen']['last'] >= timestamp - CONFIG['THRESHOLD_ABSENT']
def is_absent(timestamp, user):
return not is_present(timestamp, user)
def is_visible(timestamp, messages, user):
has_visible_messages = any(
message['token'] == user['token'] for message in messages
)
return has_visible_messages or is_present(timestamp, user)

ファイルの表示

@ -1,9 +1,10 @@
from quart import current_app, request, render_template, redirect, url_for
from anonstream.stream import get_stream_title
from anonstream.users import get_default_name, add_notice, pop_notice
from anonstream.user import add_notice, pop_notice
from anonstream.chat import add_chat_message, Rejected
from anonstream.routes.wrappers import with_user_from
from anonstream.helpers.user import get_default_name
from anonstream.utils.chat import generate_nonce
@current_app.route('/info.html')
@ -52,7 +53,6 @@ async def nojs_submit_message(user):
chat=current_app.chat,
users=current_app.users,
websockets=current_app.websockets,
secret=current_app.config['SECRET_KEY'],
user=user,
nonce=nonce,
comment=comment,

ファイルの表示

@ -15,8 +15,6 @@ async def live(user):
queue=queue,
messages=current_app.chat['messages'].values(),
users=current_app.users,
default_host_name=current_app.config['DEFAULT_HOST_NAME'],
default_anon_name=current_app.config['DEFAULT_ANON_NAME'],
)
consumer = websocket_inbound(
queue=queue,
@ -24,7 +22,6 @@ async def live(user):
users=current_app.users,
connected_websockets=current_app.websockets,
user=user,
secret=current_app.config['SECRET_KEY'],
)
try:
await asyncio.gather(producer, consumer)

ファイルの表示

@ -4,9 +4,10 @@ from functools import wraps
from quart import current_app, request, abort, make_response
from werkzeug.security import check_password_hash
from anonstream.users import sunset, user_for_websocket
from anonstream.user import sunset, user_for_websocket
from anonstream.websocket import broadcast
from anonstream.utils.users import generate_token, generate_user
from anonstream.helpers.user import generate_user
from anonstream.utils.user import generate_token
def check_auth(context):
auth = context.authorization

ファイルの表示

@ -4,20 +4,16 @@ from math import inf
from quart import current_app
from anonstream.wrappers import with_timestamp, with_first_argument
from anonstream.utils.users import user_for_websocket
from anonstream.helpers.user import is_visible
from anonstream.utils.user import user_for_websocket
from anonstream.utils import listmap
def get_default_name(user):
return (
current_app.config['DEFAULT_HOST_NAME']
if user['broadcaster'] else
current_app.config['DEFAULT_ANON_NAME']
)
CONFIG = current_app.config
def add_notice(user, notice):
notice_id = time.time_ns() // 1_000_000
user['notices'][notice_id] = notice
if len(user['notices']) > current_app.config['LIMIT_NOTICES']:
if len(user['notices']) > CONFIG['MAX_NOTICES']:
user['notices'].popitem(last=False)
return notice_id
@ -42,31 +38,13 @@ def users_for_websocket(timestamp, messages, users):
for user in visible_users
}
def is_watching(timestamp, user):
return user['watching_last'] >= timestamp - current_app.config['THRESHOLD_IDLE']
def is_idle(timestamp, user):
return is_present(timestamp, user) and not is_watching(timestamp, user)
def is_present(timestamp, user):
return user['seen']['last'] >= timestamp - current_app.config['THRESHOLD_ABSENT']
def is_absent(timestamp, user):
return not is_present(timestamp, user)
def is_visible(timestamp, messages, user):
has_visible_messages = any(
message['token'] == user['token'] for message in messages
)
return has_visible_messages or is_present(timestamp, user)
last_checkup = -inf
def sunset(messages, users):
global last_checkup
timestamp = int(time.time())
if timestamp - last_checkup < current_app.config['USER_CHECKUP_PERIOD']:
if timestamp - last_checkup < CONFIG['USER_CHECKUP_PERIOD']:
return []
to_delete = []

ファイルの表示

@ -8,10 +8,6 @@ class NonceReuse(Exception):
def generate_nonce():
return secrets.token_urlsafe(16)
def generate_nonce_hash(secret, nonce):
parts = secret + b'nonce-hash\0' + nonce.encode()
return hashlib.sha256(parts).digest()
def message_for_websocket(users, message):
message_keys = ('id', 'date', 'time_minutes', 'time_seconds', 'markup')
user_keys = ('token_hash',)

14
anonstream/utils/user.py ノーマルファイル
ファイルの表示

@ -0,0 +1,14 @@
import base64
import hashlib
import secrets
from collections import OrderedDict
from math import inf
def generate_token():
return secrets.token_hex(16)
def user_for_websocket(user, include_token_hash=True):
keys = ['broadcaster', 'name', 'color', 'tripcode']
if include_token_hash:
keys.append('token_hash')
return {key: user[key] for key in keys}

ファイルの表示

@ -1,35 +0,0 @@
import base64
import hashlib
import secrets
from collections import OrderedDict
from math import inf
def generate_token():
return secrets.token_hex(16)
def generate_token_hash(secret, token):
parts = secret + b'token-hash\0' + token.encode()
digest = hashlib.sha256(parts).digest()
return base64.b32encode(digest)[:26].lower().decode()
def generate_user(secret, token, broadcaster, timestamp):
return {
'token': token,
'token_hash': generate_token_hash(secret, token),
'broadcaster': broadcaster,
'name': None,
'color': '#c7007f',
'tripcode': None,
'notices': OrderedDict(),
'seen': {
'first': timestamp,
'last': timestamp,
},
'watching_last': -inf,
}
def user_for_websocket(user, include_token_hash=True):
keys = ['broadcaster', 'name', 'color', 'tripcode']
if include_token_hash:
keys.append('token_hash')
return {key: user[key] for key in keys}

ファイルの表示

@ -1,16 +1,19 @@
import asyncio
from quart import websocket
from quart import current_app, websocket
from anonstream.stream import get_stream_title, get_stream_uptime
from anonstream.chat import broadcast, add_chat_message, Rejected
from anonstream.users import is_present, users_for_websocket, see
from anonstream.user import users_for_websocket, see
from anonstream.wrappers import with_first_argument
from anonstream.helpers.user import is_present
from anonstream.utils import listmap
from anonstream.utils.chat import generate_nonce, message_for_websocket
from anonstream.utils.websocket import parse_websocket_data, Malformed
async def websocket_outbound(queue, messages, users, default_host_name, default_anon_name):
CONFIG = current_app.config
async def websocket_outbound(queue, messages, users):
payload = {
'type': 'init',
'nonce': generate_nonce(),
@ -21,14 +24,17 @@ async def websocket_outbound(queue, messages, users, default_host_name, default_
messages,
),
'users': users_for_websocket(messages, users),
'default': {True: default_host_name, False: default_anon_name},
'default': {
True: CONFIG['DEFAULT_HOST_NAME'],
False: CONFIG['DEFAULT_ANON_NAME'],
},
}
await websocket.send_json(payload)
while True:
payload = await queue.get()
await websocket.send_json(payload)
async def websocket_inbound(queue, chat, users, connected_websockets, user, secret):
async def websocket_inbound(queue, chat, users, connected_websockets, user):
while True:
receipt = await websocket.receive_json()
see(user)
@ -46,7 +52,6 @@ async def websocket_inbound(queue, chat, users, connected_websockets, user, secr
chat,
users,
connected_websockets,
secret,
user,
nonce,
comment,