2022-06-16 10:12:37 +09:00
|
|
|
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
|
2022-03-07 23:51:59 +09:00
|
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
|
2022-02-13 13:00:10 +09:00
|
|
|
import asyncio
|
2022-02-22 20:43:36 +09:00
|
|
|
import json
|
2022-02-13 13:00:10 +09:00
|
|
|
|
2022-02-17 09:20:51 +09:00
|
|
|
from quart import current_app, websocket
|
2022-02-13 13:00:10 +09:00
|
|
|
|
2022-02-28 20:01:24 +09:00
|
|
|
from anonstream.stream import get_stream_title, get_stream_uptime_and_viewership
|
2022-02-21 07:07:32 +09:00
|
|
|
from anonstream.captcha import get_random_captcha_digest_for
|
2022-02-20 16:20:43 +09:00
|
|
|
from anonstream.chat import get_all_messages_for_websocket, add_chat_message, Rejected
|
2022-06-25 13:21:39 +09:00
|
|
|
from anonstream.user import get_all_users_for_websocket, see, reading, verify, deverify, BadCaptcha, try_change_appearance, ensure_allowedness, AllowednessException
|
2022-06-25 13:14:31 +09:00
|
|
|
from anonstream.wrappers import with_timestamp, get_timestamp
|
2022-02-18 10:32:34 +09:00
|
|
|
from anonstream.utils.chat import generate_nonce
|
2022-06-23 12:28:32 +09:00
|
|
|
from anonstream.utils.user import identifying_string
|
2022-03-07 14:39:06 +09:00
|
|
|
from anonstream.utils.websocket import parse_websocket_data, Malformed, WS
|
2022-02-13 13:00:10 +09:00
|
|
|
|
2022-02-17 09:20:51 +09:00
|
|
|
CONFIG = current_app.config
|
|
|
|
|
2022-02-20 13:23:32 +09:00
|
|
|
async def websocket_outbound(queue, user):
|
2022-06-25 13:21:39 +09:00
|
|
|
# This function does NOT check alllowedness at first, only later.
|
|
|
|
# Allowedness is assumed to be checked beforehand (by the route handler).
|
|
|
|
# These first two websocket messages are always sent.
|
2022-06-14 09:34:24 +09:00
|
|
|
await websocket.send_json({'type': 'ping'})
|
|
|
|
await websocket.send_json({
|
2022-02-13 13:00:10 +09:00
|
|
|
'type': 'init',
|
|
|
|
'nonce': generate_nonce(),
|
2022-02-22 19:43:09 +09:00
|
|
|
'title': await get_stream_title(),
|
2022-02-28 20:01:24 +09:00
|
|
|
'stats': get_stream_uptime_and_viewership(for_websocket=True),
|
2022-02-20 16:20:43 +09:00
|
|
|
'messages': get_all_messages_for_websocket(),
|
|
|
|
'users': get_all_users_for_websocket(),
|
2022-02-17 09:20:51 +09:00
|
|
|
'default': {
|
|
|
|
True: CONFIG['DEFAULT_HOST_NAME'],
|
|
|
|
False: CONFIG['DEFAULT_ANON_NAME'],
|
|
|
|
},
|
2022-02-18 21:24:19 +09:00
|
|
|
'scrollback': CONFIG['MAX_CHAT_SCROLLBACK'],
|
2022-02-21 07:07:32 +09:00
|
|
|
'digest': get_random_captcha_digest_for(user),
|
2022-06-12 17:10:24 +09:00
|
|
|
'pingpong': CONFIG['TASK_BROADCAST_PING'],
|
2022-06-14 09:34:24 +09:00
|
|
|
})
|
2022-02-13 13:00:10 +09:00
|
|
|
while True:
|
|
|
|
payload = await queue.get()
|
2022-06-25 13:21:39 +09:00
|
|
|
if payload['type'] == 'kick':
|
|
|
|
await websocket.send_json(payload)
|
|
|
|
await websocket.close(1001)
|
|
|
|
break
|
|
|
|
elif payload['type'] == 'close':
|
2022-04-02 13:46:24 +09:00
|
|
|
await websocket.close(1011)
|
|
|
|
break
|
|
|
|
else:
|
2022-06-25 13:21:39 +09:00
|
|
|
try:
|
|
|
|
ensure_allowedness(user)
|
|
|
|
except AllowednessException:
|
2022-07-20 16:12:11 +09:00
|
|
|
await websocket.send_json({'type': 'kick'})
|
2022-06-25 13:21:39 +09:00
|
|
|
await websocket.close(1001)
|
|
|
|
break
|
|
|
|
else:
|
2022-07-20 16:39:33 +09:00
|
|
|
if user['verified'] is None:
|
|
|
|
await websocket.send_json({'type': 'kick'})
|
|
|
|
await websocket.close(1001)
|
|
|
|
else:
|
|
|
|
await websocket.send_json(payload)
|
2022-02-13 13:00:10 +09:00
|
|
|
|
2022-02-18 10:32:34 +09:00
|
|
|
async def websocket_inbound(queue, user):
|
2022-02-13 13:00:10 +09:00
|
|
|
while True:
|
2022-06-25 13:21:39 +09:00
|
|
|
# Read from websocket
|
2022-02-22 20:43:36 +09:00
|
|
|
try:
|
|
|
|
receipt = await websocket.receive_json()
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
receipt = None
|
|
|
|
finally:
|
2022-06-25 13:14:31 +09:00
|
|
|
timestamp = get_timestamp()
|
|
|
|
see(user, timestamp=timestamp)
|
2022-06-25 13:21:39 +09:00
|
|
|
|
|
|
|
# Prepare response
|
2022-02-15 19:11:53 +09:00
|
|
|
try:
|
2022-06-25 13:21:39 +09:00
|
|
|
ensure_allowedness(user)
|
|
|
|
except AllowednessException:
|
|
|
|
payload = {'type': 'kick'}
|
2022-02-13 13:00:10 +09:00
|
|
|
else:
|
2022-07-20 16:39:33 +09:00
|
|
|
if user['verified'] is None:
|
|
|
|
payload = {'type': 'kick'}
|
2022-06-25 13:21:39 +09:00
|
|
|
else:
|
2022-07-20 16:39:33 +09:00
|
|
|
try:
|
|
|
|
receipt_type, parsed = parse_websocket_data(receipt)
|
|
|
|
except Malformed as e:
|
|
|
|
error , *_ = e.args
|
|
|
|
payload = {
|
|
|
|
'type': 'error',
|
|
|
|
'because': error,
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
match receipt_type:
|
|
|
|
case WS.MESSAGE:
|
|
|
|
handle = handle_inbound_message
|
|
|
|
case WS.APPEARANCE:
|
|
|
|
handle = handle_inbound_appearance
|
|
|
|
case WS.CAPTCHA:
|
|
|
|
handle = handle_inbound_captcha
|
|
|
|
case WS.PONG:
|
|
|
|
handle = handle_inbound_pong
|
|
|
|
payload = handle(timestamp, queue, user, *parsed)
|
2022-04-02 13:46:24 +09:00
|
|
|
|
2022-06-25 13:21:39 +09:00
|
|
|
# Write to websocket
|
2022-04-02 13:46:24 +09:00
|
|
|
if payload is not None:
|
|
|
|
queue.put_nowait(payload)
|
2022-02-26 08:44:54 +09:00
|
|
|
|
2022-04-02 13:46:24 +09:00
|
|
|
def handle_inbound_pong(timestamp, queue, user):
|
2022-06-23 12:28:32 +09:00
|
|
|
print(f'[pong] {identifying_string(user)}')
|
2022-06-25 13:14:31 +09:00
|
|
|
user['last']['reading'] = timestamp
|
2022-04-02 13:46:24 +09:00
|
|
|
user['websockets'][queue] = timestamp
|
|
|
|
return None
|
2022-02-26 08:44:54 +09:00
|
|
|
|
2022-06-25 13:14:31 +09:00
|
|
|
def handle_inbound_captcha(timestamp, queue, user):
|
2022-02-26 08:44:54 +09:00
|
|
|
return {
|
|
|
|
'type': 'captcha',
|
|
|
|
'digest': get_random_captcha_digest_for(user),
|
|
|
|
}
|
|
|
|
|
2022-06-25 13:14:31 +09:00
|
|
|
def handle_inbound_appearance(timestamp, queue, user, name, color, password, want_tripcode):
|
2022-03-07 14:39:06 +09:00
|
|
|
errors = try_change_appearance(user, name, color, password, want_tripcode)
|
|
|
|
if errors:
|
|
|
|
return {
|
|
|
|
'type': 'appearance',
|
|
|
|
'errors': [error.args for error in errors],
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
return {
|
|
|
|
'type': 'appearance',
|
|
|
|
'result': 'Changed appearance',
|
|
|
|
'name': user['name'],
|
|
|
|
'color': user['color'],
|
|
|
|
#'tripcode': user['tripcode'],
|
|
|
|
}
|
|
|
|
|
2022-06-25 13:14:31 +09:00
|
|
|
def handle_inbound_message(timestamp, queue, user, nonce, comment, digest, answer):
|
2022-02-26 08:44:54 +09:00
|
|
|
try:
|
|
|
|
verification_happened = verify(user, digest, answer)
|
|
|
|
except BadCaptcha as e:
|
|
|
|
notice, *_ = e.args
|
|
|
|
message_was_added = False
|
|
|
|
else:
|
|
|
|
try:
|
2022-08-02 13:46:38 +09:00
|
|
|
seq = add_chat_message(
|
2022-02-26 08:44:54 +09:00
|
|
|
user,
|
|
|
|
nonce,
|
|
|
|
comment,
|
|
|
|
ignore_empty=verification_happened,
|
|
|
|
)
|
2022-08-02 13:46:38 +09:00
|
|
|
message_was_added = seq is not None
|
2022-02-26 08:44:54 +09:00
|
|
|
except Rejected as e:
|
|
|
|
notice, *_ = e.args
|
|
|
|
message_was_added = False
|
|
|
|
else:
|
|
|
|
notice = None
|
|
|
|
if message_was_added:
|
2022-06-29 11:35:24 +09:00
|
|
|
deverify(user, timestamp=timestamp)
|
2022-02-26 08:44:54 +09:00
|
|
|
return {
|
|
|
|
'type': 'ack',
|
|
|
|
'nonce': nonce,
|
|
|
|
'next': generate_nonce(),
|
|
|
|
'notice': notice,
|
|
|
|
'clear': message_was_added,
|
|
|
|
'digest': get_random_captcha_digest_for(user),
|
|
|
|
}
|