changeset 0:aa723e3948a4 default tip

*: initial commit awesome
author Paper <paper@tflc.us>
date Tue, 09 Sep 2025 00:29:57 -0400
parents
children
files .hgignore LICENSE Makefile README flashii.c flashii.h wcc.c
diffstat 6 files changed, 996 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Tue Sep 09 00:29:57 2025 -0400
@@ -0,0 +1,4 @@
+syntax: glob
+
+token.txt
+wcc
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Tue Sep 09 00:29:57 2025 -0400
@@ -0,0 +1,5 @@
+wcc: wcc.c flashii.c
+	$(CC) -o $@ $^ $(CFLAGS) `pkg-config --cflags --libs libwebsockets` `pkg-config --cflags --libs ncursesw` -fsanitize=address
+
+clean:
+	$(RM) -f main
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Tue Sep 09 00:29:57 2025 -0400
@@ -0,0 +1,16 @@
+wcc is a shitty little chat program I wrote for flashii's chat protocol
+(sockchat).
+
+It depends on libwebsockets to function correctly. All of the sockchat
+specific stuff is located in flashii.c, while the curses frontend is
+located in main.c.
+
+To build it you'll have to provide a Misuzu session token in the source
+directory named token.txt. This file is conveniently put in .hgignore
+so you don't have to worry about accidentally pushing it or whatever.
+
+You'll also need a C23 compiler, since I'm using #embed. If anyone
+actually needs it to compile with C99 or C11, feel free to give me
+a patch.
+
+contact: paper <paper@tflc.us>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/flashii.c	Tue Sep 09 00:29:57 2025 -0400
@@ -0,0 +1,704 @@
+/*
+ * wcc -- a shitty sockchat client
+ *
+ * Copyright (c) 2025 Paper
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include "flashii.h"
+
+#include <libwebsockets.h>
+#include <assert.h>
+
+#define HEARTBEAT_SECONDS (30)
+
+/* internal linked list of messages to send */
+struct flashii_internal_msgbuf {
+	struct flashii_internal_msgbuf *next;
+
+	char msg[]; /* meat */
+};
+
+struct flashii {
+	struct lws_context *ctx;
+	struct lws *lws;
+
+	struct flashii_internal_msgbuf *msgbuf;
+
+	flashii_msg_recv_spec msg_recv;
+	flashii_user_recv_spec user_recv;
+
+	/* user ID, retrieved after auth success */
+	char *id;
+	/* username */
+	char *username;
+	/* color */
+	/* char *color;  -- don't care */
+
+	/* permissions
+	 * odd, they can be separated by \f or ' ',
+	 * so if we want to save them we'll have to write mempbrk */
+
+	/* maybe save channel name too? */
+
+	/* maximum message length in unicode characters */
+	int32_t max_msg_length;
+
+	time_t last_heartbeat;
+	int need_heartbeat;
+};
+
+static char *memduptostr(const char *in, size_t len)
+{
+	char *q = malloc(len + 1);
+	if (!q)
+		return NULL;
+
+	memcpy(q, in, len);
+	q[len] = 0;
+
+	return q;
+}
+
+/* converts a string-like thing to i64 */
+static int memtoi64(const char *mem, size_t len, int64_t *pr)
+{
+	size_t i;
+	int64_t r;
+	int neg;
+
+	if (len <= 0)
+		return -1;
+
+	if (!(mem[0] == '-' || (mem[0] >= '0' && mem[0] <= '9')))
+		return -1;
+
+	r = 0;
+
+	neg = (mem[0] == '-');
+
+	for (i = (neg ? 1 : 0); i < len; i++) {
+		/* FIXME this does absolutely no checking for overflow etc */
+		if (!(mem[i] >= '0' && mem[i] <= '9'))
+			return -1;
+
+		r *= 10;
+		r += (mem[i] - '0');
+	}
+
+	if (mem[0] == '-')
+		r = -r;
+
+	*pr = r;
+
+	return 0;
+}
+
+/* converts a string-like thing to i32 */
+static int memtoi32(const char *mem, size_t len, int32_t *pr)
+{
+	int64_t r;
+	if (memtoi64(mem, len, &r) < 0)
+		return -1;
+
+	if (r > INT32_MAX || r < INT32_MIN)
+		return -1;
+
+	*pr = r;
+	return 0;
+}
+
+/* converts a binary string to u32
+ *
+ * e.x.:
+ *  '10010' -> 0x09 */
+static int memtou32flags(const char *mem, size_t len, uint32_t *pflags)
+{
+	size_t i;
+	uint32_t u;
+
+	if (len >= 32)
+		return -1; /* totally funtastic! */
+
+	u = 0;
+
+	for (i = 0; i < len; i++) {
+		switch (mem[i]) {
+		case '0':
+			break;
+		case '1':
+			u |= (UINT32_C(1) << i);
+			break;
+		default:
+			/* invalid input */
+			return -1;
+		}
+	}
+
+	*pflags = u;
+
+	return 0;
+}
+
+/* ------------------------------------------------------------------------ */
+/* packet receiving and parsing.
+ *
+ * packets are generally received as
+ *  [type] (optional subtype) (data)
+ * hence I've put together a simple filter thing to be able to handle
+ * types and subtypes without duplicating a shit ton of code */
+
+struct flashii_receive_packet_filter {
+	const char *prefix;
+	size_t len;
+	int (*func)(struct flashii *fls, const char *in, size_t len);
+};
+
+#define FLASHII_RECEIVE_PACKET_FILTER(prefix, func) \
+	{ (prefix "\t"), sizeof(prefix), (func) }
+
+static int flashii_receive_packet_filter(struct flashii *fls, const char *in,
+	size_t len, struct flashii_receive_packet_filter *filters)
+{
+	struct flashii_receive_packet_filter *f;
+
+	for (f = filters; f->prefix; f++) {
+		if (f->len > len || memcmp(in, f->prefix, f->len))
+			continue; /* nope */
+
+		return f->func(fls, in + f->len, len - f->len);
+	}
+
+	return -1;
+}
+
+/* ------------------------------------------------------------------------ */
+/* enumerate over parts of a packet
+ * note that whatever returned to the callback is not guaranteed to be
+ * NUL terminated; you should treat it simply as a memory buffer */
+
+/* XXX needs to take in delim */
+static int flashii_receive_packet_enum(struct flashii *fls, const char *in,
+	size_t len,
+	int (*cb)(struct flashii *fls,
+			const char *in, size_t len, size_t index, void *userdata),
+	void *userdata)
+{
+	size_t index;
+
+	for (index = 0; /* none */; index++) {
+		size_t ilen;
+
+		{
+			const char *x;
+			x = memchr(in, '\t', len);
+			ilen = x ? (x - in) : len;
+		}
+
+		//printf("%d: %.*s\n", (int)index, (int)ilen, in);
+
+		if (cb(fls, in, ilen, index, userdata) < 0)
+			return -1;
+
+		/* hopefully this is right */
+		if (ilen + 1 >= len)
+			break;
+
+		in = in + ilen + 1;
+		len -= ilen + 1;
+	}
+
+	return index;
+}
+
+/* ------------------------------------------------------------------------ */
+/* 0: pong */
+
+static int flashii_receive_packet_pong(struct flashii *fls, const char *in,
+	size_t len)
+{
+	/* nothing to do */
+	return 0;
+}
+
+/* ------------------------------------------------------------------------ */
+/* 1: auth */
+
+static int flashii_receive_auth_cb(struct flashii *fls, const char *in,
+	size_t len, size_t index, void *userdata)
+{
+	switch (index) {
+	case 1:
+	case 2:
+	case 3:
+	case 4:
+		return 0; /* nothing */
+	case 0:
+		fls->id = memduptostr(in, len);
+		/* user ID */
+		return 0;
+	case 5:
+		if (memtoi32(in, len, &fls->max_msg_length) < 0)
+			return -1;
+
+		return 0;
+	}
+
+	return -1;
+
+	(void)userdata;
+}
+
+static int flashii_receive_packet_auth_success(struct flashii *fls,
+	const char *in, size_t len)
+{
+	return (flashii_receive_packet_enum(fls, in, len, flashii_receive_auth_cb, NULL) != 5) ? -1 : 0;
+}
+
+static int flashii_receive_packet_auth(struct flashii *fls, const char *in,
+	size_t len)
+{
+	struct flashii_receive_packet_filter f[] = {
+		FLASHII_RECEIVE_PACKET_FILTER("y", flashii_receive_packet_auth_success),
+		/* FLASHII_RECEIVE_PACKET_FILTER("n", flashii_receive_packet_auth_failure), */
+		/* FLASHII_RECEIVE_PACKET_FILTER("#", flashii_receive_packet_user_add), */
+		/* terminate */
+		{ 0 }
+	};
+
+	return flashii_receive_packet_filter(fls, in, len, f);
+}
+
+/* ------------------------------------------------------------------------ */
+/* 2: message add */
+
+static int flashii_receive_packet_msg_cb(struct flashii *fls, const char *in,
+	size_t len, size_t index, void *userdata)
+{
+	struct flashii_msg *msg = userdata;
+
+	switch (index) {
+	case 0:
+		/* UNIX timestamp */
+		if (memtoi64(in, len, &msg->timestamp) < 0)
+			return -1;
+		return 0;
+	case 1:
+		msg->id = memduptostr(in, len);
+		return 0;
+	case 2:
+		msg->body = memduptostr(in, len);
+		return 0;
+	case 3:
+		msg->msg_id = memduptostr(in, len);
+		return 0;
+	case 4:
+		if (memtou32flags(in, len, &msg->flags) < 0)
+			return -1;
+		return 0;
+	}
+
+	return -1;
+}
+
+static int flashii_receive_packet_msg(struct flashii *fls, const char *in,
+	size_t len)
+{
+	struct flashii_msg msg;
+
+	if (flashii_receive_packet_enum(fls, in, len, flashii_receive_packet_msg_cb, &msg) != 4)
+		return -1;
+
+	fls->msg_recv(fls, &msg);
+	return 0;
+}
+
+/* ------------------------------------------------------------------------ */
+/* 7: context */
+
+static int flashii_receive_packet_context_user_list_cb(struct flashii *fls,
+	const char *in, size_t len, size_t index, void *userdata)
+{
+	struct flashii_user *user = userdata;
+
+	if (!index)
+		return 0; /* ignored */
+
+	switch ((index - 1) % 5) {
+	case 0:
+		/* user ID */
+		user->id = memduptostr(in, len);
+		return 0;
+	case 1:
+		/* user name */
+		user->name = memduptostr(in, len);
+		return 0;
+	case 2:
+		/* color */
+		return 0;
+	case 3:
+		/* perms */
+		return 0;
+	case 4:
+		/* visiblility */
+		if (memtoi32(in, len, &user->visible) < 0)
+			return -1;
+		fls->user_recv(fls, user);
+		return 0;
+	}
+
+	return -1;
+}
+
+static int flashii_receive_packet_context_user_list(struct flashii *fls,
+	const char *in, size_t len)
+{
+	struct flashii_user user;
+
+	if (flashii_receive_packet_enum(fls, in, len, flashii_receive_packet_context_user_list_cb, &user) < 0)
+		return -1;
+
+	return 0;
+}
+
+static int flashii_receive_packet_context_msg_add_cb(struct flashii *fls,
+	const char *in, size_t len, size_t index, void *userdata)
+{
+	struct flashii_msg *msg = userdata;
+
+	switch (index) {
+	case 0:
+		/* UNIX timestamp */
+		if (memtoi64(in, len, &msg->timestamp) < 0)
+			return -1;
+		return 0;
+	case 1:
+		msg->id = memduptostr(in, len);
+		return 0;
+	case 2:
+		/* "user name" wtf */
+		return 0;
+	case 3:
+		/* color */
+		return 0;
+	case 4:
+		/* user permissions */
+		return 0;
+	case 5:
+		msg->body = memduptostr(in, len);
+		return 0;
+	case 6:
+		msg->msg_id = memduptostr(in, len);
+		return 0;
+	case 7:
+		/* notifications */
+		return 0;
+	case 8:
+#if 0
+		if (memtou32flags(in, len, &msg->flags) < 0)
+			return -1;
+#endif
+		return 0;
+	}
+
+	return -1;
+}
+
+static int flashii_receive_packet_context_message_add(struct flashii *fls,
+	const char *in, size_t len)
+{
+	struct flashii_msg msg;
+
+	if (flashii_receive_packet_enum(fls, in, len, flashii_receive_packet_context_msg_add_cb, &msg) < 0)
+		return -1;
+
+	fls->msg_recv(fls, &msg);
+	return 0;
+}
+
+static int flashii_receive_packet_context(struct flashii *fls, const char *in,
+	size_t len)
+{
+	struct flashii_receive_packet_filter f[] = {
+		FLASHII_RECEIVE_PACKET_FILTER("0", flashii_receive_packet_context_user_list),
+		FLASHII_RECEIVE_PACKET_FILTER("1", flashii_receive_packet_context_message_add),
+		/* FLASHII_RECEIVE_PACKET_FILTER("2", ...), */
+		/* terminate */
+		{ 0 }
+	};
+
+	return flashii_receive_packet_filter(fls, in, len, f);
+}
+
+/* ------------------------------------------------------------------------ */
+
+static int flashii_receive_packet(struct flashii *fls, const char *in,
+	size_t len)
+{
+	struct flashii_receive_packet_filter f[] = {
+		FLASHII_RECEIVE_PACKET_FILTER("0", flashii_receive_packet_pong),
+		FLASHII_RECEIVE_PACKET_FILTER("1", flashii_receive_packet_auth),
+		FLASHII_RECEIVE_PACKET_FILTER("2", flashii_receive_packet_msg),
+		FLASHII_RECEIVE_PACKET_FILTER("7", flashii_receive_packet_context),
+		/* terminate */
+		{ 0 }
+	};
+
+	return flashii_receive_packet_filter(fls, in, len, f);
+}
+
+/* ------------------------------------------------------------------------ */
+
+static int flashii_send_vpacket(struct flashii *fls, const char *fmt, va_list ap)
+{
+	/* this uses vsnprintf to assemble the packet and sends it off */
+	int r;
+	char *s;
+	va_list ap2;
+
+	va_copy(ap2, ap);
+
+	r = vsnprintf(NULL, 0, fmt, ap);
+
+	if (r < 0) {
+		va_end(ap2);
+		return -1;
+	}
+
+	s = malloc(LWS_PRE + r + 1);
+	if (!s) {
+		va_end(ap2);
+		return -1;
+	}
+
+	r = vsnprintf(s + LWS_PRE, r + 1, fmt, ap2);
+
+	va_end(ap2);
+
+	if (r < 0)
+		return -1;
+
+	lws_write(fls->lws, s + LWS_PRE, r, LWS_WRITE_TEXT);
+	free(s);
+
+	return 0;
+}
+
+static int flashii_send_packet(struct flashii *fls, const char *fmt, ...)
+{
+	va_list ap;
+	int r;
+
+	va_start(ap, fmt);
+	r = flashii_send_vpacket(fls, fmt, ap);
+	va_end(ap);
+
+	return r;
+}
+
+static int flashii_send_ping(struct flashii *fls, const char *id)
+{
+	assert(id);
+	return flashii_send_packet(fls, "0\t%s", id);
+}
+
+static int flashii_send_auth(struct flashii *fls, const char *scheme, const char *args)
+{
+	assert(scheme);
+	assert(args);
+	return flashii_send_packet(fls, "1\t%s\t%s", scheme, args);
+}
+
+static int flashii_send_msg(struct flashii *fls, const char *id, const char *msg)
+{
+	assert(id);
+	assert(msg);
+	return flashii_send_packet(fls, "2\t%s\t%s", id, msg);
+}
+
+static int flashii_process_msgbuf(struct flashii *fls)
+{
+	while (fls->msgbuf) {
+		struct flashii_internal_msgbuf *tmp;
+
+		tmp = fls->msgbuf;
+		flashii_send_msg(fls, fls->id, tmp->msg);
+		fls->msgbuf = tmp->next;
+		free(tmp);
+	}
+}
+
+/* libwebsockets calls us back here */
+static int flashii_cb(struct lws *wsi, enum lws_callback_reasons reason,
+	void *userdata, void *in, size_t len)
+{
+	struct flashii *fls = (struct flashii *)userdata;
+	static const unsigned char token[] = {
+#embed "token.txt"
+		, 0
+	};
+
+	switch (reason) {
+	case LWS_CALLBACK_CLIENT_ESTABLISHED:
+		lws_callback_on_writable(wsi);
+		break;
+	case LWS_CALLBACK_CLIENT_RECEIVE:
+		/* incoming message -- print it out
+		 * we should handle it based on the type */
+		flashii_receive_packet(fls, in, len);
+		break;
+	case LWS_CALLBACK_CLIENT_WRITEABLE:
+		if (fls->need_heartbeat) {
+			flashii_send_ping(fls, fls->id);
+			fls->need_heartbeat = 0;
+		}
+
+		/* process the buffer of messages, if any */
+		flashii_process_msgbuf(fls);
+
+		flashii_send_auth(fls, "Misuzu", token);
+		/* send message here using lws_write */
+		break;
+	case LWS_CALLBACK_CLIENT_CLOSED:
+	case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
+		break;
+#if 0
+	default:
+		printf("UNKNOWN REASON: %d\n", reason);
+		break;
+#endif
+	}
+
+	return lws_callback_http_dummy(wsi, reason, userdata, in, len);
+}
+
+static const struct lws_protocols protocols[] = {
+	{
+		"sockchat",
+		flashii_cb,
+		sizeof(struct flashii),
+		0,
+	},
+	{ 0 } /* terminator */
+};
+
+/* ------------------------------------------------------------------------ */
+
+struct flashii *flashii_init(const char *protocol, const char *address,
+	uint16_t port, flashii_msg_recv_spec msg_recv,
+	flashii_user_recv_spec user_recv)
+{
+	struct lws_context_creation_info info;
+	struct lws_client_connect_info ccinfo;
+	struct flashii *fls;
+
+	fls = calloc(1, sizeof(*fls));
+	if (!fls)
+		return NULL;
+
+	fls->msg_recv = msg_recv;
+	fls->user_recv = user_recv;
+
+	lws_context_info_defaults(&info, NULL);
+	info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
+	info.port = CONTEXT_PORT_NO_LISTEN; /* we do not run any server */
+	info.protocols = protocols;
+
+	fls->ctx = lws_create_context(&info);
+	if (!fls->ctx) {
+		lwsl_err("lws init failed\n");
+		free(fls);
+		return NULL;
+	}
+
+	/* konekuto */
+	memset(&ccinfo, 0, sizeof(ccinfo));
+	
+	ccinfo.context = fls->ctx;
+	ccinfo.address = address;
+	ccinfo.port = port;
+	ccinfo.path = "/";
+	ccinfo.host = address;
+	ccinfo.origin = address;
+	ccinfo.protocol = "sockchat";
+	ccinfo.ssl_connection = LCCSCF_USE_SSL;
+	ccinfo.userdata = fls;
+
+	fls->lws = lws_client_connect_via_info(&ccinfo);
+	if (!fls->lws) {
+		lwsl_cx_err(fls->ctx, "lws connect failed\n");
+		lws_context_destroy(fls->ctx);
+		free(fls);
+		return NULL;
+	}
+
+	return fls;
+}
+
+/* worker
+ *
+ * this should be called in an event loop, on the same thread that
+ * initialized the structure.
+ *
+ * `noblock` is a boolean saying... whether we should block :) */
+void flashii_work(struct flashii *fls, int noblock)
+{
+	time_t thetime;
+
+	time(&thetime);
+
+	if (fls->id) {
+		if (difftime(thetime, fls->last_heartbeat) >= 30.0) {
+			fls->need_heartbeat = 1;
+			fls->last_heartbeat = thetime;
+			lws_callback_on_writable(fls->lws);
+		}
+	}
+
+	lws_service(fls->ctx, (noblock) ? -1 : 0);
+}
+
+int flashii_send_message(struct flashii *fls, const char *msg)
+{
+	/* append to the internal linked list of messages */
+	size_t len = strlen(msg);
+
+	struct flashii_internal_msgbuf *msgbuf = malloc(sizeof(*msgbuf) + len + 1);
+	if (!msgbuf)
+		return -1; /* WHAT */
+
+	msgbuf->next = NULL;
+
+	/* copy it allll in (includes NUL terminator) */
+	memcpy(msgbuf->msg, msg, len + 1);
+
+	if (fls->msgbuf) {
+		struct flashii_internal_msgbuf *tail = fls->msgbuf;
+		while (tail->next)
+			tail = tail->next;
+		tail->next = msgbuf;
+	} else {
+		fls->msgbuf = msgbuf;
+	}
+
+	lws_callback_on_writable(fls->lws);
+
+	return 0; /* yay */
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/flashii.h	Tue Sep 09 00:29:57 2025 -0400
@@ -0,0 +1,66 @@
+/*
+ * wcc -- a shitty sockchat client
+ *
+ * Copyright (c) 2025 Paper
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef WCC_FLASHII_H_
+#define WCC_FLASHII_H_
+
+#include <stdint.h>
+
+/* opaque type */
+struct flashii;
+
+struct flashii_msg {
+	int64_t timestamp; /* UNIX timestamp */
+	char *id; /* user ID of the author */
+	char *body; /* message body; sanitized. this is HTML/BBcode */
+	char *msg_id; /* message ID (lol) */
+
+#define FLASHII_MSG_FLAG_BOLD (0x01)
+#define FLASHII_MSG_FLAG_CURSIVE (0x02) /* AKA italics */
+#define FLASHII_MSG_FLAG_UNDERLINE (0x04)
+#define FLASHII_MSG_FLAG_COLON (0x08)
+#define FLASHII_MSG_FLAG_PRIVATE (0x10) /* private message */
+	uint32_t flags;
+};
+
+typedef void (*flashii_msg_recv_spec)(struct flashii *fls, struct flashii_msg *msg);
+
+struct flashii_user {
+	char *id;
+	char *name;
+	/* char *color; -- dont care */
+	/* (?) perms; -- dont care */
+	int32_t visible;
+};
+
+typedef void (*flashii_user_recv_spec)(struct flashii *fls, struct flashii_user *user);
+
+struct flashii *flashii_init(const char *protocol, const char *address,
+	uint16_t port, flashii_msg_recv_spec msg_recv,
+	flashii_user_recv_spec user_recv);
+void flashii_work(struct flashii *fls, int noblock);
+int flashii_send_message(struct flashii *fls, const char *msg);
+/* TODO flashii_quit */
+
+#endif /* WCC_FLASHII_H_ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wcc.c	Tue Sep 09 00:29:57 2025 -0400
@@ -0,0 +1,201 @@
+/*
+ * wcc -- a shitty sockchat client
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <time.h>
+#include <curses.h>
+#include <locale.h>
+
+#include "flashii.h"
+
+/* ------------------------------------------------------------------------ */
+
+/* creates a set (only unique IDs allowed)
+ *
+ * the ID must be a string! */
+#define DYNAMIC_SET(type, name, uid) \
+	struct name { \
+		type *arr; \
+		size_t size; \
+		size_t alloc; \
+	}; \
+\
+	static type *name##_lookup(struct name *arr, const char *id) \
+	{ \
+		size_t i; \
+	\
+		for (i = 0; i < arr->size; i++) \
+			if (!strcmp(arr->arr[i].uid, id)) \
+				return arr->arr + i; \
+	\
+		return NULL; \
+	} \
+\
+	static int name##_insert(struct name *x, type *u) \
+	{ \
+		type *uu; \
+\
+		uu = name##_lookup(x, u->uid); \
+		if (uu) { \
+			/* overwrite it with our own data */ \
+			memcpy(uu, u, sizeof(*u)); \
+			return 0; \
+		} /* else... */ \
+\
+		if (x->size + 1 >= x->alloc) { \
+			x->alloc = (x->alloc) ? (x->alloc * 2) : 8; \
+			x->arr = realloc(x->arr, x->alloc * sizeof(*u)); \
+			if (!x->arr) \
+				return -1; \
+		} \
+\
+		memcpy(x->arr + x->size, u, sizeof(*u)); \
+		x->size++; \
+\
+		return 1; \
+	} \
+\
+	static int name##_remove(struct name *s, type *u) \
+	{ \
+		/* TODO */ \
+		return -1;	 \
+	}
+
+DYNAMIC_SET(struct flashii_user, flashii_user_set, id)
+DYNAMIC_SET(struct flashii_msg, flashii_msg_set, msg_id)
+
+/* ------------------------------------------------------------------------ */
+
+static struct flashii_user_set users = {0};
+static struct flashii_msg_set msgs = {0};
+
+static void msg_recv(struct flashii *fls, struct flashii_msg *msg)
+{
+	flashii_msg_set_insert(&msgs, msg);
+}
+
+static void user_recv(struct flashii *fls, struct flashii_user *user)
+{
+	flashii_user_set_insert(&users, user);
+}
+
+/* pending message ... */
+static wchar_t pending_msg[65536];
+static size_t pending_msg_len = 0;
+
+int main(void)
+{
+	int height, width;
+	struct flashii *fls;
+	WINDOW *cw, *iw;
+
+	/* stupid C shit */
+	setlocale(LC_ALL, "en_US.UTF-8");
+
+	initscr();
+	cbreak();
+	noecho();
+	curs_set(1);
+
+	getmaxyx(stdscr, height, width);
+
+#define INPUT_HEIGHT (3)
+	cw = newwin(height - INPUT_HEIGHT, width, 0, 0);
+	iw = newwin(INPUT_HEIGHT, width, height - INPUT_HEIGHT, 0);
+
+	scrollok(cw, TRUE);
+	box(iw, 0, 0);
+	nodelay(iw, TRUE); /* no blocking! */
+	keypad(iw, TRUE);
+
+	fls = flashii_init("wss", "chatsrv-neru.flashii.net", 443,
+		msg_recv, user_recv);
+	if (!fls)
+		return 1;
+
+	for (;;) {
+		size_t i;
+
+		/* IM GONNA TALK TO DA POWER */
+		flashii_work(fls, 1);
+
+		/* chat window */
+		werase(cw);
+
+		/* draw msgs to the screen */
+		for (i = 0; i < msgs.size; i++) {
+			const char *name;
+			struct flashii_user *user;
+
+			user = flashii_user_set_lookup(&users, msgs.arr[i].id);
+			name = (user && user->name) ? user->name : msgs.arr[i].id;
+
+			wprintw(cw, "<%s> %s\n", name, msgs.arr[i].body);
+		}
+
+		wrefresh(cw);
+
+		werase(iw);
+
+		/* handle input */
+		box(iw, 0, 0);
+		for (;;) {
+			wint_t c;
+			int r = wget_wch(iw, &c);
+			if (r == KEY_CODE_YES) {
+				/* keycode, KEY_DOWN, KEY_UP, etc */
+				if (c == KEY_BACKSPACE) { /* backspace */
+					pending_msg[--pending_msg_len] = '\0';
+				} else {
+					printf("UNKNOWN KEYCODE: %d\n", c);
+				}
+			} else if (r == OK) {
+				/* unicode */
+				if (pending_msg_len >= width - 7) /* or something like that */
+					continue;
+
+				if (c == '\r' || c == '\n') {
+					static char pending_msg_conv[65536];
+					wcstombs(pending_msg_conv, pending_msg, pending_msg_len);
+					flashii_send_message(fls, pending_msg_conv);
+					memset(pending_msg, 0, pending_msg_len);
+					pending_msg_len = 0;
+				} else if (c == '\b' || c == 127) {
+					pending_msg[--pending_msg_len] = '\0';
+				} else {
+					pending_msg[pending_msg_len++] = c;
+				}
+			} else/*if (r == ERR)*/ {
+				break;
+			}
+		}
+
+		mvwprintw(iw, 1, 1, "> %ls", pending_msg);
+
+		wrefresh(iw);
+
+		usleep(10000);
+	}
+
+	endwin();
+
+	return 0;
+}