Some more project structure

このコミットが含まれているのは:
n9k 2022-02-18 01:32:34 +00:00
コミット 1e6563c4a2
12個のファイルの変更100行の追加104行の削除

ファイルの表示

@ -42,9 +42,10 @@ async def create_app():
assert app.config['MAX_CHAT_MESSAGES'] >= app.config['MAX_CHAT_SCROLLBACK'] assert app.config['MAX_CHAT_MESSAGES'] >= app.config['MAX_CHAT_SCROLLBACK']
assert app.config['THRESHOLD_ABSENT'] >= app.config['THRESHOLD_IDLE'] assert app.config['THRESHOLD_ABSENT'] >= app.config['THRESHOLD_IDLE']
app.chat = {'messages': OrderedDict(), 'nonce_hashes': set()} app.messages_by_id = OrderedDict()
app.users = {} app.users_by_token = {}
app.websockets = set() app.messages = app.messages_by_id.values()
app.users = app.users_by_token.values()
app.segments_directory_cache = DirectoryCache(config['stream']['segments_dir']) app.segments_directory_cache = DirectoryCache(config['stream']['segments_dir'])
async with app.app_context(): async with app.app_context():

ファイルの表示

@ -1,22 +1,37 @@
import time import time
from datetime import datetime from datetime import datetime
from quart import escape from quart import current_app, escape
from anonstream.user import users_for_websocket
from anonstream.helpers.chat import generate_nonce_hash from anonstream.helpers.chat import generate_nonce_hash
from anonstream.utils.chat import message_for_websocket
MESSAGES_BY_ID = current_app.messages_by_id
MESSAGES = current_app.messages
USERS_BY_TOKEN = current_app.users_by_token
USERS = current_app.users
class Rejected(Exception): class Rejected(Exception):
pass pass
async def broadcast(websockets, payload): async def broadcast(users, payload):
for queue in websockets: for user in users:
await queue.put(payload) for queue in user['websockets']:
await queue.put(payload)
async def add_chat_message(chat, users, websockets, user, nonce, comment): def messages_for_websocket():
return list(map(
lambda message: message_for_websocket(
user=USERS_BY_TOKEN[message['token']],
message=message,
),
MESSAGES,
))
async def add_chat_message(user, nonce, comment):
# check message # check message
nonce_hash = generate_nonce_hash(nonce) message_id = generate_nonce_hash(nonce)
if nonce_hash in chat['nonce_hashes']: if message_id in MESSAGES_BY_ID:
raise Rejected('Discarded suspected duplicate message') raise Rejected('Discarded suspected duplicate message')
if len(comment) == 0: if len(comment) == 0:
raise Rejected('Message was empty') raise Rejected('Message was empty')
@ -25,17 +40,19 @@ async def add_chat_message(chat, users, websockets, user, nonce, comment):
timestamp_ms = time.time_ns() // 1_000_000 timestamp_ms = time.time_ns() // 1_000_000
timestamp = timestamp_ms // 1000 timestamp = timestamp_ms // 1000
try: try:
last_message = next(reversed(chat['messages'].values())) last_message = next(reversed(MESSAGES))
except StopIteration: except StopIteration:
message_id = timestamp_ms seq = timestamp_ms
else: else:
if timestamp <= last_message['id']: if timestamp_ms > last_message['seq']:
message_id = last_message['id'] + 1 seq = timestamp_ms
else:
seq = last_message['seq'] + 1
dt = datetime.utcfromtimestamp(timestamp) dt = datetime.utcfromtimestamp(timestamp)
markup = escape(comment) markup = escape(comment)
chat['messages'][message_id] = { MESSAGES_BY_ID[message_id] = {
'id': message_id, 'id': message_id,
'nonce_hash': nonce_hash, 'seq': seq,
'token': user['token'], 'token': user['token'],
'timestamp': timestamp, 'timestamp': timestamp,
'date': dt.strftime('%Y-%m-%d'), 'date': dt.strftime('%Y-%m-%d'),
@ -45,15 +62,12 @@ async def add_chat_message(chat, users, websockets, user, nonce, comment):
'markup': markup, 'markup': markup,
} }
# collect nonce hash
chat['nonce_hashes'].add(nonce_hash)
# broadcast message to websockets # broadcast message to websockets
await broadcast( await broadcast(
websockets, USERS,
payload={ payload={
'type': 'chat', 'type': 'chat',
'id': message_id, 'seq': seq,
'token_hash': user['token_hash'], 'token_hash': user['token_hash'],
'markup': markup, 'markup': markup,
} }

ファイルの表示

@ -14,7 +14,7 @@ def generate_token_hash(token):
digest = hashlib.sha256(parts).digest() digest = hashlib.sha256(parts).digest()
return base64.b32encode(digest)[:26].lower().decode() return base64.b32encode(digest)[:26].lower().decode()
def generate_user(token, broadcaster, timestamp): def generate_user(timestamp, token, broadcaster):
colour = generate_colour( colour = generate_colour(
seed='name\0' + token, seed='name\0' + token,
bg=CONFIG['CHAT_BACKGROUND_COLOUR'], bg=CONFIG['CHAT_BACKGROUND_COLOUR'],
@ -23,6 +23,7 @@ def generate_user(token, broadcaster, timestamp):
return { return {
'token': token, 'token': token,
'token_hash': generate_token_hash(token), 'token_hash': generate_token_hash(token),
'websockets': set(),
'broadcaster': broadcaster, 'broadcaster': broadcaster,
'name': None, 'name': None,
'color': colour_to_color(colour), 'color': colour_to_color(colour),
@ -49,7 +50,10 @@ def is_idle(timestamp, user):
return is_present(timestamp, user) and not is_watching(timestamp, user) return is_present(timestamp, user) and not is_watching(timestamp, user)
def is_present(timestamp, user): def is_present(timestamp, user):
return user['seen']['last'] >= timestamp - CONFIG['THRESHOLD_ABSENT'] return (
user['seen']['last'] >= timestamp - CONFIG['THRESHOLD_ABSENT']
or len(user['websockets']) > 0
)
def is_absent(timestamp, user): def is_absent(timestamp, user):
return not is_present(timestamp, user) return not is_present(timestamp, user)

ファイルの表示

@ -24,8 +24,8 @@ async def nojs_chat(user):
return await render_template( return await render_template(
'nojs_chat.html', 'nojs_chat.html',
user=user, user=user,
users=current_app.users, users_by_token=current_app.users_by_token,
messages=current_app.chat['messages'].values(), messages=current_app.messages,
get_default_name=get_default_name, get_default_name=get_default_name,
) )
@ -53,14 +53,7 @@ async def nojs_submit_message(user):
nonce = form.get('nonce', '') nonce = form.get('nonce', '')
try: try:
await add_chat_message( await add_chat_message(user, nonce, comment)
chat=current_app.chat,
users=current_app.users,
websockets=current_app.websockets,
user=user,
nonce=nonce,
comment=comment,
)
except Rejected as e: except Rejected as e:
notice, *_ = e.args notice, *_ = e.args
notice_id = add_notice(user, notice) notice_id = add_notice(user, notice)

ファイルの表示

@ -9,21 +9,11 @@ from anonstream.routes.wrappers import with_user_from
@with_user_from(websocket) @with_user_from(websocket)
async def live(user): async def live(user):
queue = asyncio.Queue() queue = asyncio.Queue()
current_app.websockets.add(queue) user['websockets'].add(queue)
producer = websocket_outbound( producer = websocket_outbound(queue)
queue=queue, consumer = websocket_inbound(queue, user)
messages=current_app.chat['messages'].values(),
users=current_app.users,
)
consumer = websocket_inbound(
queue=queue,
chat=current_app.chat,
users=current_app.users,
connected_websockets=current_app.websockets,
user=user,
)
try: try:
await asyncio.gather(producer, consumer) await asyncio.gather(producer, consumer)
finally: finally:
current_app.websockets.remove(queue) user['websockets'].remove(queue)

ファイルの表示

@ -5,17 +5,22 @@ from quart import current_app, request, abort, make_response
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
from anonstream.user import sunset, user_for_websocket from anonstream.user import sunset, user_for_websocket
from anonstream.websocket import broadcast from anonstream.chat import broadcast
from anonstream.helpers.user import generate_user from anonstream.helpers.user import generate_user
from anonstream.utils.user import generate_token from anonstream.utils.user import generate_token
CONFIG = current_app.config
MESSAGES = current_app.messages
USERS_BY_TOKEN = current_app.users_by_token
USERS = current_app.users
def check_auth(context): def check_auth(context):
auth = context.authorization auth = context.authorization
return ( return (
auth is not None auth is not None
and auth.type == "basic" and auth.type == "basic"
and auth.username == current_app.config["AUTH_USERNAME"] and auth.username == CONFIG["AUTH_USERNAME"]
and check_password_hash(current_app.config["AUTH_PWHASH"], auth.password) and check_password_hash(CONFIG["AUTH_PWHASH"], auth.password)
) )
def auth_required(f): def auth_required(f):
@ -44,40 +49,40 @@ def with_user_from(context):
# Check if broadcaster # Check if broadcaster
broadcaster = check_auth(context) broadcaster = check_auth(context)
if broadcaster: if broadcaster:
token = current_app.config['AUTH_TOKEN'] token = CONFIG['AUTH_TOKEN']
else: else:
token = context.args.get('token') or context.cookies.get('token') or generate_token() token = context.args.get('token') or context.cookies.get('token') or generate_token()
# Remove non-visible absent users # Remove non-visible absent users
token_hashes = sunset( sunsetted_token_hashes = sunset(
messages=current_app.chat['messages'].values(), messages=MESSAGES,
users=current_app.users, users_by_token=USERS_BY_TOKEN,
) )
if len(token_hashes) > 0: if sunsetted_token_hashes:
await broadcast( await broadcast(
current_app.websockets, users=USERS,
payload={ payload={
'type': 'rem-users', 'type': 'rem-users',
'token_hashes': token_hashes, 'token_hashes': sunsetted_token_hashes,
} }
) )
# Update / create user # Update / create user
user = current_app.users.get(token) user = USERS_BY_TOKEN.get(token)
if user is not None: if user is not None:
user['seen']['last'] = timestamp user['seen']['last'] = timestamp
else: else:
user = generate_user( user = generate_user(
secret=current_app.config['SECRET_KEY'], timestamp=timestamp,
token=token, token=token,
broadcaster=broadcaster, broadcaster=broadcaster,
timestamp=timestamp,
) )
current_app.users[token] = user USERS_BY_TOKEN[token] = user
await broadcast( await broadcast(
current_app.websockets, USERS,
payload={ payload={
'type': 'add-user', 'type': 'add-user',
'token_hash': user['token_hash'],
'user': user_for_websocket(user), 'user': user_for_websocket(user),
} }
) )

ファイルの表示

@ -52,7 +52,7 @@ const create_chat_message = (object) => {
const chat_message = document.createElement("li"); const chat_message = document.createElement("li");
chat_message.classList.add("chat-message"); chat_message.classList.add("chat-message");
chat_message.dataset.id = object.id; chat_message.dataset.seq = object.seq;
chat_message.dataset.tokenHash = object.token_hash; chat_message.dataset.tokenHash = object.token_hash;
const chat_message_name = document.createElement("span"); const chat_message_name = document.createElement("span");
@ -126,11 +126,11 @@ const on_websocket_message = (event) => {
users = receipt.users; users = receipt.users;
update_user_styles(); update_user_styles();
const ids = new Set(receipt.chat.map((message) => {return message.id;})); const seqs = new Set(receipt.messages.map((message) => {return message.seq;}));
const to_delete = []; const to_delete = [];
for (const chat_message of chat_messages.children) { for (const chat_message of chat_messages.children) {
const chat_message_id = parseInt(chat_message.dataset.id); const chat_message_seq = parseInt(chat_message.dataset.seq);
if (!ids.has(chat_message_id)) { if (!seqs.has(chat_message_seq)) {
to_delete.push(chat_message); to_delete.push(chat_message);
} }
} }
@ -138,9 +138,10 @@ const on_websocket_message = (event) => {
chat_message.remove(); chat_message.remove();
} }
const last_id = Math.max(...[...chat_messages.children].map((element) => parseInt(element.dataset.id))); const last = chat_messages.children.length == 0 ? null : chat_messages.children[chat_messages.children.length - 1];
for (const message of receipt.chat) { const last_seq = last === null ? null : parseInt(last.dataset.seq);
if (message.id > last_id) { for (const message of receipt.messages) {
if (message.seq > last_seq) {
const chat_message = create_chat_message(message); const chat_message = create_chat_message(message);
chat_messages.insertAdjacentElement("beforeend", chat_message); chat_messages.insertAdjacentElement("beforeend", chat_message);
} }
@ -184,7 +185,7 @@ const on_websocket_message = (event) => {
case "add-user": case "add-user":
console.log("ws add-user", receipt); console.log("ws add-user", receipt);
users[receipt.user.token_hash] = receipt.user; users[receipt.token_hash] = receipt.user;
update_user_styles(); update_user_styles();
break; break;

ファイルの表示

@ -48,7 +48,7 @@
<ul id="chat-messages"> <ul id="chat-messages">
{% for message in messages | reverse %} {% for message in messages | reverse %}
<li class="chat-message"> <li class="chat-message">
{% with user = users[message.token] %} {% with user = users_by_token[message.token] %}
<span class="chat-message__name" style="color:{{ user.color }};" data-id="{{ message.id }}">{{ user.name or get_default_name(user) }}</span>:&nbsp;<span class="chat-message__markup">{{ message.markup }}</span> <span class="chat-message__name" style="color:{{ user.color }};" data-id="{{ message.id }}">{{ user.name or get_default_name(user) }}</span>:&nbsp;<span class="chat-message__markup">{{ message.markup }}</span>
{% endwith %} {% endwith %}
</li> </li>

ファイルの表示

@ -10,6 +10,8 @@ from anonstream.utils.colour import color_to_colour, get_contrast, NotAColor
from anonstream.utils.user import user_for_websocket from anonstream.utils.user import user_for_websocket
CONFIG = current_app.config CONFIG = current_app.config
MESSAGES = current_app.messages
USERS = current_app.users
class BadAppearance(Exception): class BadAppearance(Exception):
pass pass
@ -71,19 +73,19 @@ def see(user):
user['seen']['last'] = int(time.time()) user['seen']['last'] = int(time.time())
@with_timestamp @with_timestamp
def users_for_websocket(timestamp, messages, users): def users_for_websocket(timestamp):
visible_users = filter( visible_users = filter(
lambda user: is_visible(timestamp, messages, user), lambda user: is_visible(timestamp, MESSAGES, user),
users.values(), USERS,
) )
return { return {
user['token_hash']: user_for_websocket(user, include_token_hash=False) user['token_hash']: user_for_websocket(user)
for user in visible_users for user in visible_users
} }
last_checkup = -inf last_checkup = -inf
def sunset(messages, users): def sunset(messages, users_by_token):
global last_checkup global last_checkup
timestamp = int(time.time()) timestamp = int(time.time())
@ -91,13 +93,13 @@ def sunset(messages, users):
return [] return []
to_delete = [] to_delete = []
for token in users: for token in users_by_token:
user = users[token] user = users_by_token[token]
if not is_visible(timestamp, messages, user): if not is_visible(timestamp, messages, user):
to_delete.append(token) to_delete.append(token)
for index, token in enumerate(to_delete): for index, token in enumerate(to_delete):
to_delete[index] = users.pop(token)['token_hash'] to_delete[index] = users_by_token.pop(token)['token_hash']
last_checkup = timestamp last_checkup = timestamp
return to_delete return to_delete

ファイルの表示

@ -8,11 +8,9 @@ class NonceReuse(Exception):
def generate_nonce(): def generate_nonce():
return secrets.token_urlsafe(16) return secrets.token_urlsafe(16)
def message_for_websocket(users, message): def message_for_websocket(user, message):
message_keys = ('id', 'date', 'time_minutes', 'time_seconds', 'markup') message_keys = ('seq', 'date', 'time_minutes', 'time_seconds', 'markup')
user_keys = ('token_hash',) user_keys = ('token_hash',)
user = users[message['token']]
return { return {
**{key: message[key] for key in message_keys}, **{key: message[key] for key in message_keys},
**{key: user[key] for key in user_keys}, **{key: user[key] for key in user_keys},

ファイルの表示

@ -9,10 +9,8 @@ from quart import escape, Markup
def generate_token(): def generate_token():
return secrets.token_hex(16) return secrets.token_hex(16)
def user_for_websocket(user, include_token_hash=True): def user_for_websocket(user):
keys = ['broadcaster', 'name', 'color', 'tripcode'] keys = ['broadcaster', 'name', 'color', 'tripcode']
if include_token_hash:
keys.append('token_hash')
return {key: user[key] for key in keys} return {key: user[key] for key in keys}
def concatenate_for_notice(string, *tuples): def concatenate_for_notice(string, *tuples):

ファイルの表示

@ -3,26 +3,23 @@ import asyncio
from quart import current_app, websocket from quart import current_app, websocket
from anonstream.stream import get_stream_title, get_stream_uptime from anonstream.stream import get_stream_title, get_stream_uptime
from anonstream.chat import broadcast, add_chat_message, Rejected from anonstream.chat import messages_for_websocket, add_chat_message, Rejected
from anonstream.user import users_for_websocket, see from anonstream.user import users_for_websocket, see
from anonstream.wrappers import with_first_argument from anonstream.wrappers import with_first_argument
from anonstream.helpers.user import is_present from anonstream.helpers.user import is_present
from anonstream.utils.chat import generate_nonce, message_for_websocket from anonstream.utils.chat import generate_nonce
from anonstream.utils.websocket import parse_websocket_data, Malformed from anonstream.utils.websocket import parse_websocket_data, Malformed
CONFIG = current_app.config CONFIG = current_app.config
async def websocket_outbound(queue, messages, users): async def websocket_outbound(queue):
payload = { payload = {
'type': 'init', 'type': 'init',
'nonce': generate_nonce(), 'nonce': generate_nonce(),
'title': get_stream_title(), 'title': get_stream_title(),
'uptime': get_stream_uptime(), 'uptime': get_stream_uptime(),
'chat': list(map( 'messages': messages_for_websocket(),
with_first_argument(users)(message_for_websocket), 'users': users_for_websocket(),
messages,
)),
'users': users_for_websocket(messages, users),
'default': { 'default': {
True: CONFIG['DEFAULT_HOST_NAME'], True: CONFIG['DEFAULT_HOST_NAME'],
False: CONFIG['DEFAULT_ANON_NAME'], False: CONFIG['DEFAULT_ANON_NAME'],
@ -33,7 +30,7 @@ async def websocket_outbound(queue, messages, users):
payload = await queue.get() payload = await queue.get()
await websocket.send_json(payload) await websocket.send_json(payload)
async def websocket_inbound(queue, chat, users, connected_websockets, user): async def websocket_inbound(queue, user):
while True: while True:
receipt = await websocket.receive_json() receipt = await websocket.receive_json()
see(user) see(user)
@ -47,14 +44,7 @@ async def websocket_inbound(queue, chat, users, connected_websockets, user):
} }
else: else:
try: try:
markup = await add_chat_message( markup = await add_chat_message(user, nonce, comment)
chat,
users,
connected_websockets,
user,
nonce,
comment,
)
except Rejected as e: except Rejected as e:
notice, *_ = e.args notice, *_ = e.args
payload = { payload = {