diff --git a/anonstream/__init__.py b/anonstream/__init__.py
index 7e22a47..aed7287 100644
--- a/anonstream/__init__.py
+++ b/anonstream/__init__.py
@@ -6,6 +6,7 @@ from quart import Quart
from werkzeug.security import generate_password_hash
from anonstream.utils.user import generate_token
+from anonstream.utils.colour import color_to_colour
from anonstream.segments import DirectoryCache
async def create_app():
@@ -25,15 +26,20 @@ async def create_app():
'AUTH_TOKEN': generate_token(),
'DEFAULT_HOST_NAME': config['names']['broadcaster'],
'DEFAULT_ANON_NAME': config['names']['anonymous'],
- 'MAX_NOTICES': config['limits']['notices'],
- 'MAX_CHAT_STORAGE': config['limits']['chat_storage'],
- 'MAX_CHAT_SCROLLBACK': config['limits']['chat_scrollback'],
- 'USER_CHECKUP_PERIOD': config['ratelimits']['user_absence'],
- 'CAPTCHA_CHECKUP_PERIOD': config['ratelimits']['captcha_expiry'],
+ 'MAX_NOTICES': config['memory']['notices'],
+ 'MAX_CHAT_MESSAGES': config['memory']['chat_messages'],
+ 'MAX_CHAT_SCROLLBACK': config['memory']['chat_scrollback'],
+ 'CHECKUP_PERIOD_USER': config['ratelimits']['user_absence'],
+ 'CHECKUP_PERIOD_CAPTCHA': config['ratelimits']['captcha_expiry'],
'THRESHOLD_IDLE': config['thresholds']['idle'],
'THRESHOLD_ABSENT': config['thresholds']['absent'],
+ 'CHAT_COMMENT_MAX_LENGTH': config['chat']['max_name_length'],
+ 'CHAT_NAME_MAX_LENGTH': config['chat']['max_name_length'],
+ 'CHAT_NAME_MIN_CONTRAST': config['chat']['min_name_contrast'],
+ 'CHAT_BACKGROUND_COLOUR': color_to_colour(config['chat']['background_color']),
})
+ assert app.config['MAX_CHAT_MESSAGES'] >= app.config['MAX_CHAT_SCROLLBACK']
assert app.config['THRESHOLD_ABSENT'] >= app.config['THRESHOLD_IDLE']
app.chat = {'messages': OrderedDict(), 'nonce_hashes': set()}
diff --git a/anonstream/helpers/tripcode.py b/anonstream/helpers/tripcode.py
new file mode 100644
index 0000000..0b0489a
--- /dev/null
+++ b/anonstream/helpers/tripcode.py
@@ -0,0 +1,40 @@
+import base64
+import hashlib
+
+import werkzeug.security
+from quart import current_app
+
+from anonstream.utils.colour import generate_colour, generate_maximum_contrast_colour, colour_to_color
+
+CONFIG = current_app.config
+
+def _generate_tripcode_digest_legacy(password):
+ hexdigest, _ = werkzeug.security._hash_internal(
+ 'pbkdf2:sha256:150000',
+ CONFIG['SECRET_KEY'],
+ password,
+ )
+ digest = bytes.fromhex(hexdigest)
+ return base64.b64encode(digest)[:8].decode()
+
+def generate_tripcode_digest(password):
+ parts = CONFIG['SECRET_KEY'] + b'tripcode\0' + password.encode()
+ digest = hashlib.sha256(parts).digest()
+ return base64.b64encode(digest)[:8].decode()
+
+def generate_tripcode(password, generate_digest=generate_tripcode_digest):
+ digest = generate_digest(password)
+ background_colour = generate_colour(
+ seed='tripcode-background\0' + digest,
+ bg=CONFIG['CHAT_BACKGROUND_COLOUR'],
+ contrast=5.0,
+ )
+ foreground_colour = generate_maximum_contrast_colour(
+ seed='tripcode-foreground\0' + digest,
+ bg=background_colour,
+ )
+ return {
+ 'digest': digest,
+ 'background_color': colour_to_color(background_colour),
+ 'foreground_color': colour_to_color(foreground_colour),
+ }
diff --git a/anonstream/helpers/user.py b/anonstream/helpers/user.py
index ea6944c..ce67a10 100644
--- a/anonstream/helpers/user.py
+++ b/anonstream/helpers/user.py
@@ -5,6 +5,8 @@ from math import inf
from quart import current_app
+from anonstream.utils.colour import generate_colour, colour_to_color
+
CONFIG = current_app.config
def generate_token_hash(token):
@@ -12,13 +14,18 @@ def generate_token_hash(token):
digest = hashlib.sha256(parts).digest()
return base64.b32encode(digest)[:26].lower().decode()
-def generate_user(secret, token, broadcaster, timestamp):
+def generate_user(token, broadcaster, timestamp):
+ colour = generate_colour(
+ seed='name\0' + token,
+ bg=CONFIG['CHAT_BACKGROUND_COLOUR'],
+ contrast=4.53,
+ )
return {
'token': token,
'token_hash': generate_token_hash(token),
'broadcaster': broadcaster,
'name': None,
- 'color': '#c7007f',
+ 'color': colour_to_color(colour),
'tripcode': None,
'notices': OrderedDict(),
'seen': {
diff --git a/anonstream/routes/nojs.py b/anonstream/routes/nojs.py
index 9dd6f5d..022fde9 100644
--- a/anonstream/routes/nojs.py
+++ b/anonstream/routes/nojs.py
@@ -1,11 +1,13 @@
-from quart import current_app, request, render_template, redirect, url_for
+from quart import current_app, request, render_template, redirect, url_for, escape, Markup
from anonstream.stream import get_stream_title
-from anonstream.user import add_notice, pop_notice
+from anonstream.user import add_notice, pop_notice, change_name, change_color, change_tripcode, delete_tripcode, BadAppearance
from anonstream.chat import add_chat_message, Rejected
from anonstream.routes.wrappers import with_user_from
+from anonstream.wrappers import try_except_log
from anonstream.helpers.user import get_default_name
from anonstream.utils.chat import generate_nonce
+from anonstream.utils.user import concatenate_for_notice
@current_app.route('/info.html')
@with_user_from(request)
@@ -31,11 +33,13 @@ async def nojs_chat(user):
@with_user_from(request)
async def nojs_form(user):
notice_id = request.args.get('notice', type=int)
+ notice, verbose = pop_notice(user, notice_id)
prefer_chat_form = request.args.get('landing') != 'appearance'
return await render_template(
'nojs_form.html',
user=user,
- notice=pop_notice(user, notice_id),
+ notice=notice,
+ verbose=verbose,
prefer_chat_form=prefer_chat_form,
nonce=generate_nonce(),
default_name=get_default_name(user),
@@ -63,9 +67,47 @@ async def nojs_submit_message(user):
else:
notice_id = None
- return redirect(url_for('nojs_form', token=user['token'], notice=notice_id))
+ return redirect(url_for('nojs_form', token=user['token'], landing='chat', notice=notice_id))
@current_app.post('/chat/appearance')
@with_user_from(request)
async def nojs_submit_appearance(user):
- pass
+ form = await request.form
+ name = form.get('name', '') or None
+ color = form.get('color', '')
+ password = form.get('password', '')
+ want_delete_tripcode = form.get('clear-tripcode', type=bool)
+ want_change_tripcode = form.get('set-tripcode', type=bool)
+
+ errors = []
+ def try_(f, *args, **kwargs):
+ return try_except_log(errors, BadAppearance)(f)(*args, **kwargs)
+
+ try_(change_name, user, name, dry_run=True)
+ try_(change_color, user, color, dry_run=True)
+ if want_delete_tripcode:
+ pass
+ elif want_change_tripcode:
+ try_(change_tripcode, user, password, dry_run=True)
+
+ if errors:
+ notice = Markup('
').join(
+ concatenate_for_notice(*error.args) for error in errors
+ )
+ else:
+ change_name(user, name)
+ change_color(user, color)
+ if want_delete_tripcode:
+ delete_tripcode(user)
+ elif want_change_tripcode:
+ change_tripcode(user, password)
+
+ notice = 'Changed appearance'
+
+ notice_id = add_notice(user, notice, verbose=len(errors) > 1)
+ return redirect(url_for(
+ 'nojs_form',
+ token=user['token'],
+ landing='appearance' if errors else 'chat',
+ notice=notice_id,
+ ))
diff --git a/anonstream/templates/nojs_form.html b/anonstream/templates/nojs_form.html
index 8c03725..f9615ea 100644
--- a/anonstream/templates/nojs_form.html
+++ b/anonstream/templates/nojs_form.html
@@ -30,7 +30,7 @@
text-decoration: underline;
}
.tripcode {
- padding: 0 4px;
+ padding: 0 5px;
border-radius: 7px;
font-family: monospace;
cursor: default;
@@ -40,7 +40,6 @@
}
#tripcode {
cursor: pointer;
- margin-right: 4px;
}
.x {
font-size: 14pt;
@@ -61,6 +60,9 @@
font-size: 18pt;
line-height: 1.25;
}
+ #notice.verbose h1 {
+ font-size: 14pt;
+ }
#chat-form, #appearance-form {
padding: 0 var(--padding-size) var(--padding-size) var(--padding-size);
@@ -110,7 +112,7 @@
#password-column {
display: grid;
grid-template-columns: auto auto 1fr;
- grid-gap: 0.25rem;
+ grid-gap: 0.375rem;
align-items: center;
}
#appearance-form label:not(.tripcode):not(.x) {
@@ -181,20 +183,20 @@