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.
このコミットが含まれているのは:
コミット
7dbcd43f30
|
@ -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:
|
||||
|
|
|
@ -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()
|
|
@ -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',)
|
||||
|
|
|
@ -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,
|
||||
|
|
読み込み中…
新しいイシューから参照