diff --git a/README.md b/README.md
index f1e554d..29be680 100644
--- a/README.md
+++ b/README.md
@@ -9,3 +9,7 @@ The canonical location of this repo is https://git.076.ne.jp/ninya9k/anonstream.
These mirrors also exist:
* https://gitlab.com/ninya9k/anonstream
* https://github.com/ninya9k/anonstream
+
+## Credits
+
+* [/anonstream/static/settings.svg](https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/anonstream/static/settings.svg): [setting](https://thenounproject.com/icon/setting-685325/) by [ulimicon](https://thenounproject.com/unlimicon/) is licensed under [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/).
diff --git a/anonstream/routes/nojs.py b/anonstream/routes/nojs.py
index 8cfcc22..3391b0a 100644
--- a/anonstream/routes/nojs.py
+++ b/anonstream/routes/nojs.py
@@ -141,7 +141,7 @@ async def nojs_submit_appearance(user):
# Collect form data
name = form.get('name', '').strip()
- if len(name) == 0 or name == get_default_name(user):
+ if len(name) == 0:
name = None
color = form.get('color', '')
diff --git a/anonstream/static/anonstream.js b/anonstream/static/anonstream.js
index 474ecf1..bd1e1fd 100644
--- a/anonstream/static/anonstream.js
+++ b/anonstream/static/anonstream.js
@@ -28,11 +28,15 @@ const jsmarkup_chat_form = `\
- Not connected to chat
+
+ Not connected to chat
+ ×
+
+
+
+`;
const insert_jsmarkup = () => {jsmarkup_info_float_viewership
@@ -115,6 +130,30 @@ const show_notice = (text) => {
chat_form.dataset.notice = "";
}
+/* override chat form settings input */
+const chat_appearance_form = document.getElementById("appearance-form_js");
+const chat_appearance_form_result = document.getElementById("appearance-form_js__row__result");
+const chat_form_settings = document.getElementById("chat-form_js__settings");
+chat_form_settings.addEventListener("click", (event) => {
+ event.preventDefault();
+ if (chat_appearance_form.dataset.hidden === undefined) {
+ chat_appearance_form.dataset.hidden = "";
+ chat_form_settings.style.backgroundColor = "";
+ chat_appearance_form_result.innerText = "";
+ if (!chat_appearance_form_submit.disabled) {
+ chat_appearance_form.reset();
+ }
+ } else {
+ chat_appearance_form.removeAttribute("data-hidden");
+ chat_form_settings.style.backgroundColor = "#4f4f53";
+ }
+});
+
+/* appearance form */
+const chat_appearance_form_name = document.getElementById("appearance-form_js__name");
+const chat_appearance_form_color = document.getElementById("appearance-form_js__color");
+const chat_appearance_form_password = document.getElementById("appearance-form_js__password");
+
/* create websocket */
const info_title = document.getElementById("info_js__title");
const info_viewership = document.getElementById("info_js__float__viewership");
@@ -523,6 +562,7 @@ const on_websocket_message = (event) => {
case "error":
console.log("ws error", receipt);
chat_form_submit.disabled = false;
+ chat_appearance_form_submit.disabled = false;
break;
case "init":
@@ -576,6 +616,14 @@ const on_websocket_message = (event) => {
update_user_tripcodes();
update_users_list()
+ // appearance form default values
+ const user = users[TOKEN_HASH];
+ if (user.name !== null) {
+ chat_appearance_form_name.setAttribute("value", user.name);
+ }
+ chat_appearance_form_name.setAttribute("placeholder", default_name[user.broadcaster]);
+ chat_appearance_form_color.setAttribute("value", user.color);
+
// insert new messages
const last = chat_messages.children.length == 0 ? null : chat_messages.children[chat_messages.children.length - 1];
const last_seq = last === null ? null : parseInt(last.dataset.seq);
@@ -666,6 +714,41 @@ const on_websocket_message = (event) => {
receipt.digest === null ? disable_captcha() : enable_captcha(receipt.digest);
break;
+ case "appearance":
+ console.log("ws appearance", receipt);
+
+ if (receipt.errors === undefined) {
+ if (receipt.name !== null) {
+ chat_appearance_form_name.setAttribute("value", receipt.name);
+ }
+ chat_appearance_form_color.setAttribute("value", receipt.color);
+ chat_appearance_form_result.innerHTML = receipt.result;
+ } else {
+ const ul = document.createElement("ul");
+ for (const error of receipt.errors) {
+ const li = document.createElement("li");
+ li.innerText = error[0];
+ for (const tuple of error.slice(1)) {
+ const mark = document.createElement("mark");
+ mark.innerText = tuple[0];
+ li.insertAdjacentText("beforeend", " ");
+ li.insertAdjacentElement("beforeend", mark);
+ li.insertAdjacentText("beforeend", tuple[1]);
+ }
+ ul.insertAdjacentElement("beforeend", li);
+ }
+ const result = document.createElement("div");
+ result.innerText = "Errors:";
+ result.insertAdjacentElement("beforeend", ul);
+ chat_appearance_form_result.innerHTML = result.innerHTML;
+ }
+
+ chat_appearance_form_submit.disabled = false;
+ chat_appearance_form.removeAttribute("data-hidden");
+ chat_form_settings.style.backgroundColor = "#4f4f53";
+
+ break;
+
default:
console.log("incomprehensible websocket message", receipt);
}
@@ -680,13 +763,13 @@ const connect_websocket = () => {
return;
}
chat_live_ball.style.borderColor = "gold";
- chat_live_status.innerHTML = "Waiting... Connecting to chat...";
+ chat_live_status.innerHTML = "Connecting to chat...···";
ws = new WebSocket(`ws://${document.domain}:${location.port}/live?token=${encodeURIComponent(TOKEN)}`);
ws.addEventListener("open", (event) => {
console.log("websocket open", event);
chat_form_submit.disabled = false;
chat_live_ball.style.borderColor = "green";
- chat_live_status.innerHTML = "Connected to chat";
+ chat_live_status.innerHTML = "Connected to chat✓";
// When the server is offline, a newly opened websocket can take a second
// to close. This timeout tries to ensure the backoff doesn't instantly
// (erroneously) reset to 2 seconds in that case.
@@ -702,7 +785,7 @@ const connect_websocket = () => {
console.log("websocket close", event);
chat_form_submit.disabled = true;
chat_live_ball.style.borderColor = "maroon";
- chat_live_status.innerHTML = "Failed to connect Disconnected from chat";
+ chat_live_status.innerHTML = "Disconnected from chat×";
if (!ws.successor) {
ws.successor = true;
setTimeout(connect_websocket, websocket_backoff);
@@ -745,6 +828,18 @@ chat_form.addEventListener("submit", (event) => {
ws.send(JSON.stringify(payload));
});
+/* override js-only appearance form */
+const chat_appearance_form_submit = document.getElementById("appearance-form_js__row__submit");
+chat_appearance_form.addEventListener("submit", (event) => {
+ event.preventDefault();
+ const form = Object.fromEntries(new FormData(chat_appearance_form));
+ const payload = {type: "appearance", form: form};
+ chat_appearance_form_submit.disabled = true;
+ chat_appearance_form_password.value = "";
+ chat_appearance_form_result.innerText = "";
+ ws.send(JSON.stringify(payload));
+});
+
/* when chat is being resized, peg its bottom in place (instead of its top) */
const track_scroll = (element) => {
chat_messages.dataset.scrollTop = chat_messages.scrollTop;
diff --git a/anonstream/static/settings.svg b/anonstream/static/settings.svg
new file mode 100644
index 0000000..ace59e8
--- /dev/null
+++ b/anonstream/static/settings.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/anonstream/static/style.css b/anonstream/static/style.css
index 27187c3..e9a3561 100644
--- a/anonstream/static/style.css
+++ b/anonstream/static/style.css
@@ -258,19 +258,19 @@ noscript {
#chat-users_nojs {
height: 100%;
}
+#chat__form {
+ position: relative;
+}
#chat-form_js {
display: grid;
- grid-template-columns: 1fr min-content min-content 5rem;
+ grid-template-columns: 1fr min-content min-content min-content 5rem;
grid-template-rows: auto var(--button-height);
grid-gap: 0.375rem;
padding: 0 0.5rem 0.5rem 0.5rem;
position: relative;
}
-#chat-form_js__submit {
- grid-column: 2 / span 1;
-}
#chat-form_js__comment {
- grid-column: 1 / span 4;
+ grid-column: 1 / span 5;
background-color: #434347;
border-radius: 4px;
border: 2px solid transparent;
@@ -299,9 +299,20 @@ noscript {
#chat-form_js__captcha-answer {
width: 8ch;
}
-#chat-form_js__submit {
+#chat-form_js__settings {
+ align-self: center;
+ padding: 5px;
+ box-sizing: border-box;
+ border-radius: 3px;
+ color: var(--text-color);
grid-column: 4;
}
+#chat-form_js__settings:hover {
+ background-color: #434347;
+}
+#chat-form_js__submit {
+ grid-column: 5;
+}
#chat-form_js:not([data-captcha]) > #chat-form_js__captcha-image,
#chat-form_js:not([data-captcha]) > #chat-form_js__captcha-answer {
display: none;
@@ -312,9 +323,10 @@ noscript {
#chat-form_js__notice {
position: absolute;
width: 100%;
- background: linear-gradient(#2323277f 25%, #232327);
+ background: linear-gradient(#23232700, #2323277f 8%, #232327);
height: 100%;
display: grid;
+ z-index: 1;
}
#chat-form_js__notice__button {
color: inherit;
@@ -336,6 +348,51 @@ noscript {
#chat-form_nojs {
height: 13ch;
}
+#appearance-form_js {
+ position: absolute;
+ bottom: 3rem;
+ padding: 0.5rem;
+ margin: 0 1rem;
+ width: calc(100% - 2rem);
+ box-sizing: border-box;
+ background: #343437df;
+ border: 2px outset #434347;
+ border-radius: 4px;
+ display: grid;
+ grid-template-columns: min-content 1fr min-content;
+ grid-template-rows: 1fr 1fr auto;
+ grid-gap: 0.375rem;
+}
+#appearance-form_js[data-hidden] {
+ display: none;
+}
+#appearance-form_js__label-name,
+#appearance-form_js__label-tripcode {
+ align-self: center;
+}
+#appearance-form_js__name,
+#appearance-form_js__password {
+ min-width: 12ch;
+}
+#appearance-form_js__row {
+ grid-column: 1 / span 3;
+ grid-row: 3;
+ display: grid;
+ grid-template-columns: auto 4rem;
+ align-items: end;
+}
+#appearance-form_js__row__result {
+ font-weight: bold;
+ font-size: 11pt;
+}
+#appearance-form_js__row__result > ul {
+ margin: 0;
+ padding-left: 1.125rem;
+ font-size: 10pt;
+}
+#appearance-form_js__row__submit {
+ min-height: 1.75rem;
+}
#chat-live {
position: relative;
font-size: 9pt;
diff --git a/anonstream/user.py b/anonstream/user.py
index 0be76d9..180a9b7 100644
--- a/anonstream/user.py
+++ b/anonstream/user.py
@@ -4,7 +4,7 @@ from math import inf
from quart import current_app
from anonstream.wrappers import try_except_log, with_timestamp
-from anonstream.helpers.user import get_presence, Presence
+from anonstream.helpers.user import get_default_name, get_presence, Presence
from anonstream.helpers.captcha import check_captcha_digest, Answer
from anonstream.helpers.tripcode import generate_tripcode
from anonstream.utils.colour import color_to_colour, get_contrast, NotAColor
@@ -69,6 +69,8 @@ def try_change_appearance(user, name, color, password, want_tripcode):
def change_name(user, name, dry_run=False):
if dry_run:
+ if name == get_default_name(user):
+ name = None
if name is not None:
if len(name) == 0:
raise BadAppearance('Name was empty')
@@ -91,7 +93,7 @@ def change_color(user, color, dry_run=False):
if contrast < min_contrast:
raise BadAppearance(
'Colour had insufficient contrast:',
- (f'{contrast:.2f}', f'/{min_contrast}'),
+ (f'{contrast:.2f}', f'/{min_contrast:.2f}'),
)
else:
user['color'] = color
diff --git a/anonstream/utils/websocket.py b/anonstream/utils/websocket.py
index 1dcaec0..25b01b4 100644
--- a/anonstream/utils/websocket.py
+++ b/anonstream/utils/websocket.py
@@ -1,3 +1,7 @@
+from enum import Enum
+
+WS = Enum('WS', names=('MESSAGE, CAPTCHA, APPEARANCE'))
+
class Malformed(Exception):
pass
@@ -19,13 +23,27 @@ def parse_websocket_data(receipt):
comment = get(str, form, 'comment')
digest = get(str, form, 'captcha-digest', '')
answer = get(str, form, 'captcha-answer', '')
- return nonce, comment, digest, answer
+ return WS.MESSAGE, (nonce, comment, digest, answer)
case 'appearance':
- raise NotImplemented
+ form = get(dict, receipt, 'form')
+ name = get(str, form, 'name').strip()
+ if len(name) == 0:
+ name = None
+ color = get(str, form, 'color')
+ password = get(str, form, 'password')
+ #match get(str | None, form, 'want-tripcode'):
+ # case '0':
+ # want_tripcode = False
+ # case '1':
+ # want_tripcode = True
+ # case _:
+ # want_tripcode = None
+ want_tripcode = bool(password)
+ return WS.APPEARANCE, (name, color, password, want_tripcode)
case 'captcha':
- return None
+ return WS.CAPTCHA, ()
case _:
raise Malformed('malformed type')
diff --git a/anonstream/websocket.py b/anonstream/websocket.py
index 75e2d07..41e53d6 100644
--- a/anonstream/websocket.py
+++ b/anonstream/websocket.py
@@ -6,9 +6,9 @@ from quart import current_app, websocket
from anonstream.stream import get_stream_title, get_stream_uptime_and_viewership
from anonstream.captcha import get_random_captcha_digest_for
from anonstream.chat import get_all_messages_for_websocket, add_chat_message, Rejected
-from anonstream.user import get_all_users_for_websocket, see, verify, deverify, BadCaptcha
+from anonstream.user import get_all_users_for_websocket, see, verify, deverify, BadCaptcha, try_change_appearance
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, WS
CONFIG = current_app.config
@@ -41,7 +41,7 @@ async def websocket_inbound(queue, user):
finally:
see(user)
try:
- parsed = parse_websocket_data(receipt)
+ receipt_type, parsed = parse_websocket_data(receipt)
except Malformed as e:
error , *_ = e.args
payload = {
@@ -49,12 +49,14 @@ async def websocket_inbound(queue, user):
'because': error,
}
else:
- match parsed:
- case [nonce, comment, digest, answer]:
- payload = handle_inbound_message(user, *parsed)
-
- case None:
- payload = handle_inbound_captcha(user)
+ match receipt_type:
+ case WS.MESSAGE:
+ handle = handle_inbound_message
+ case WS.APPEARANCE:
+ handle = handle_inbound_appearance
+ case WS.CAPTCHA:
+ handle = handle_inbound_captcha
+ payload = handle(user, *parsed)
queue.put_nowait(payload)
@@ -64,6 +66,22 @@ def handle_inbound_captcha(user):
'digest': get_random_captcha_digest_for(user),
}
+def handle_inbound_appearance(user, name, color, password, want_tripcode):
+ 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'],
+ }
+
def handle_inbound_message(user, nonce, comment, digest, answer):
try:
verification_happened = verify(user, digest, answer)