diff --git a/anonstream/__init__.py b/anonstream/__init__.py
index 6a45237..dd86639 100644
--- a/anonstream/__init__.py
+++ b/anonstream/__init__.py
@@ -14,20 +14,21 @@ async def create_app():
auth_password = secrets.token_urlsafe(6)
auth_pwhash = generate_password_hash(auth_password)
- print('Broadcaster username:', config['auth_username'])
+ print('Broadcaster username:', config['auth']['username'])
print('Broadcaster password:', auth_password)
app = Quart('anonstream')
app.config['SECRET_KEY'] = config['secret_key'].encode()
- app.config['AUTH_USERNAME'] = config['auth_username']
+ app.config['AUTH_USERNAME'] = config['auth']['username']
app.config['AUTH_PWHASH'] = auth_pwhash
app.config['AUTH_TOKEN'] = generate_token()
- app.config['DEFAULT_HOST_NAME'] = config['default_host_name']
- app.config['DEFAULT_ANON_NAME'] = config['default_anon_name']
+ app.config['DEFAULT_HOST_NAME'] = config['names']['broadcaster']
+ app.config['DEFAULT_ANON_NAME'] = config['names']['anonymous']
+ app.config['LIMIT_NOTICES'] = config['limits']['notices']
app.chat = OrderedDict()
app.users = {}
app.websockets = set()
- app.segments_directory_cache = DirectoryCache(config['segments_dir'])
+ app.segments_directory_cache = DirectoryCache(config['stream']['segments_dir'])
async with app.app_context():
import anonstream.routes
diff --git a/anonstream/chat.py b/anonstream/chat.py
index 3a6cd09..68726dc 100644
--- a/anonstream/chat.py
+++ b/anonstream/chat.py
@@ -2,9 +2,21 @@ from datetime import datetime
from quart import escape
-def add_chat_message(chat, message_id, token, text):
+class Rejected(Exception):
+ pass
+
+async def broadcast(websockets, payload):
+ for queue in websockets:
+ await queue.put(payload)
+
+async def add_chat_message(chat, websockets, token, message_id, comment):
+ # check message
+ if len(comment) == 0:
+ raise Rejected('Message was empty')
+
+ # add message
dt = datetime.utcnow()
- markup = escape(text)
+ markup = escape(comment)
chat[message_id] = {
'id': message_id,
'token': token,
@@ -12,6 +24,19 @@ def add_chat_message(chat, message_id, token, text):
'date': dt.strftime('%Y-%m-%d'),
'time_minutes': dt.strftime('%H:%M'),
'time_seconds': dt.strftime('%H:%M:%S'),
- 'nomarkup': text,
+ 'nomarkup': comment,
'markup': markup,
}
+
+ # broadcast message to websockets
+ await broadcast(
+ websockets,
+ payload={
+ 'type': 'chat',
+ 'color': '#c7007f',
+ 'name': 'Anonymous',
+ 'markup': markup,
+ }
+ )
+
+ return markup
diff --git a/anonstream/routes.py b/anonstream/routes.py
index 1dfdb06..a496ed8 100644
--- a/anonstream/routes.py
+++ b/anonstream/routes.py
@@ -1,12 +1,14 @@
import asyncio
-from quart import current_app, request, render_template, make_response, redirect, websocket
+from quart import current_app, request, render_template, make_response, redirect, websocket, url_for
from anonstream.stream import get_stream_title
from anonstream.segments import CatSegments, Offline
-from anonstream.users import get_default_name
+from anonstream.users import get_default_name, add_notice, pop_notice
from anonstream.wrappers import with_user_from, auth_required
from anonstream.websocket import websocket_outbound, websocket_inbound
+from anonstream.chat import add_chat_message, Rejected
+from anonstream.utils.chat import create_message, generate_nonce, NonceReuse
@current_app.route('/')
@with_user_from(request)
@@ -69,8 +71,51 @@ async def nojs_chat(user):
@current_app.route('/chat/form.html')
@with_user_from(request)
async def nojs_form(user):
+ notice_id = request.args.get('notice', type=int)
+ prefer_chat_form = request.args.get('landing') != 'appearance'
return await render_template(
'nojs_form.html',
user=user,
+ notice=pop_notice(user, notice_id),
+ prefer_chat_form=prefer_chat_form,
+ nonce=generate_nonce(),
default_name=get_default_name(user),
)
+
+@current_app.post('/chat/message')
+@with_user_from(request)
+async def nojs_submit_message(user):
+ form = await request.form
+ comment = form.get('comment', '')
+ nonce = form.get('nonce', '')
+
+ try:
+ message_id, _, _ = create_message(
+ message_ids=current_app.chat.keys(),
+ secret=current_app.config['SECRET_KEY'],
+ nonce=nonce,
+ comment=comment,
+ )
+ except NonceReuse:
+ notice_id = add_notice(user, 'Discarded suspected duplicate message')
+ else:
+ try:
+ await add_chat_message(
+ current_app.chat,
+ current_app.websockets,
+ user['token'],
+ message_id,
+ comment
+ )
+ except Rejected as e:
+ notice, *_ = e.args
+ notice_id = add_notice(user, notice)
+ else:
+ notice_id = None
+
+ return redirect(url_for('nojs_form', token=user['token'], notice=notice_id))
+
+@current_app.post('/chat/appearance')
+@with_user_from(request)
+async def nojs_submit_appearance(user):
+ pass
diff --git a/anonstream/static/anonstream.js b/anonstream/static/anonstream.js
index f45427c..cf392b7 100644
--- a/anonstream/static/anonstream.js
+++ b/anonstream/static/anonstream.js
@@ -8,7 +8,7 @@ const jsmarkup_chat_messages = '
';
const jsmarkup_chat_form = `\
diff --git a/anonstream/users.py b/anonstream/users.py
index a0c8103..f27810d 100644
--- a/anonstream/users.py
+++ b/anonstream/users.py
@@ -1,3 +1,5 @@
+import time
+
from quart import current_app
def get_default_name(user):
@@ -7,3 +9,16 @@ def get_default_name(user):
current_app.config['DEFAULT_ANON_NAME']
)
+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']:
+ user['notices'].popitem(last=False)
+ return notice_id
+
+def pop_notice(user, notice_id):
+ try:
+ notice = user['notices'].pop(notice_id)
+ except KeyError:
+ notice = None
+ return notice
diff --git a/anonstream/utils/chat.py b/anonstream/utils/chat.py
index af7cee1..300664b 100644
--- a/anonstream/utils/chat.py
+++ b/anonstream/utils/chat.py
@@ -2,6 +2,9 @@ import base64
import hashlib
import secrets
+class NonceReuse(Exception):
+ pass
+
def generate_nonce():
return secrets.token_urlsafe(16)
@@ -9,3 +12,9 @@ def generate_message_id(secret, nonce):
parts = secret + b'message-id\0' + nonce.encode()
digest = hashlib.sha256(parts).digest()
return base64.urlsafe_b64encode(digest)[:22].decode()
+
+def create_message(message_ids, secret, nonce, comment):
+ message_id = generate_message_id(secret, nonce)
+ if message_id in message_ids:
+ raise NonceReuse
+ return message_id, nonce, comment
diff --git a/anonstream/utils/users.py b/anonstream/utils/users.py
index fd58d58..26c4b0a 100644
--- a/anonstream/utils/users.py
+++ b/anonstream/utils/users.py
@@ -1,4 +1,5 @@
import secrets
+from collections import OrderedDict
def generate_token():
return secrets.token_hex(16)
@@ -9,6 +10,7 @@ def generate_user(token, broadcaster, timestamp):
'broadcaster': broadcaster,
'name': None,
'tripcode': None,
+ 'notices': OrderedDict(),
'seen': {
'first': timestamp,
'last': timestamp,
diff --git a/anonstream/utils/websocket.py b/anonstream/utils/websocket.py
index 7c5ebb0..d250dce 100644
--- a/anonstream/utils/websocket.py
+++ b/anonstream/utils/websocket.py
@@ -1,19 +1,23 @@
-from anonstream.utils.chat import generate_message_id
+from anonstream.utils.chat import create_message, NonceReuse
-def parse(message_ids, secret, receipt):
+class Malformed(Exception):
+ pass
+
+def parse_websocket_data(message_ids, secret, receipt):
if not isinstance(receipt, dict):
- return None, 'not a json object'
+ raise Malformed('not a json object')
- message = receipt.get('message')
- if not isinstance(message, str):
- return None, 'malformed chat message'
+ comment = receipt.get('comment')
+ if not isinstance(comment, str):
+ raise Malformed('malformed comment')
nonce = receipt.get('nonce')
if not isinstance(nonce, str):
- return None, 'malformed nonce'
+ raise Malformed('malformed nonce')
- message_id = generate_message_id(secret, nonce)
- if message_id in message_ids:
- return None, 'nonce already used'
+ try:
+ message = create_message(message_ids, secret, nonce, comment)
+ except NonceReuse:
+ raise Malformed('nonce already used')
- return (message, nonce, message_id), None
+ return message
diff --git a/anonstream/websocket.py b/anonstream/websocket.py
index 9171e78..807a757 100644
--- a/anonstream/websocket.py
+++ b/anonstream/websocket.py
@@ -3,9 +3,9 @@ import asyncio
from quart import websocket
from anonstream.stream import get_stream_title, get_stream_uptime
-from anonstream.chat import add_chat_message
+from anonstream.chat import broadcast, add_chat_message, Rejected
from anonstream.utils.chat import generate_nonce
-from anonstream.utils.websocket import parse
+from anonstream.utils.websocket import parse_websocket_data
async def websocket_outbound(queue):
payload = {
@@ -23,28 +23,33 @@ async def websocket_outbound(queue):
async def websocket_inbound(queue, connected_websockets, token, secret, chat):
while True:
receipt = await websocket.receive_json()
- receipt, error = parse(chat.keys(), secret, receipt)
- if error is not None:
+ try:
+ message_id, nonce, comment = parse_websocket_data(chat.keys(), secret, receipt)
+ except Malformed as e:
+ error , *_ = e.args
payload = {
'type': 'error',
'because': error,
}
else:
- text, nonce, message_id = receipt
- add_chat_message(chat, message_id, token, text)
- payload = {
- 'type': 'ack',
- 'nonce': nonce,
- 'next': generate_nonce(),
- }
+ try:
+ markup = await add_chat_message(
+ chat,
+ connected_websockets,
+ token,
+ message_id,
+ comment
+ )
+ except Rejected as e:
+ notice, *_ = e.args
+ payload = {
+ 'type': 'reject',
+ 'notice': notice,
+ }
+ else:
+ payload = {
+ 'type': 'ack',
+ 'nonce': nonce,
+ 'next': generate_nonce(),
+ }
await queue.put(payload)
-
- if error is None:
- payload = {
- 'type': 'chat',
- 'color': '#c7007f',
- 'name': 'Anonymous',
- 'text': text,
- }
- for other_queue in connected_websockets:
- await other_queue.put(payload)
diff --git a/anonstream/wrappers.py b/anonstream/wrappers.py
index 09c8c94..86454f3 100644
--- a/anonstream/wrappers.py
+++ b/anonstream/wrappers.py
@@ -47,6 +47,7 @@ def with_user_from(context):
user['seen']['last'] = timestamp
else:
user = generate_user(token, broadcaster, timestamp)
+ current_app.users[token] = user
return await f(user, *args, **kwargs)
return wrapper
diff --git a/config.toml b/config.toml
index 0339443..a7da9cf 100644
--- a/config.toml
+++ b/config.toml
@@ -1,5 +1,14 @@
secret_key = "test"
-auth_username = "broadcaster"
+
+[auth]
+username = "broadcaster"
+
+[stream]
segments_dir = "stream/"
-default_host_name = "Broadcaster"
-default_anon_name = "Anonymous"
+
+[names]
+broadcaster = "Broadcaster"
+anonymous = "Anonymous"
+
+[limits]
+notices = 32