summary refs log tree commit diff
path: root/bin/typer.c
diff options
context:
space:
mode:
Diffstat (limited to 'bin/typer.c')
-rw-r--r--bin/typer.c197
1 files changed, 197 insertions, 0 deletions
diff --git a/bin/typer.c b/bin/typer.c
new file mode 100644
index 00000000..76eab784
--- /dev/null
+++ b/bin/typer.c
@@ -0,0 +1,197 @@
+/* Copyright (C) 2021  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this Program, or any covered work, by linking or
+ * combining it with LibreSSL (or a modified version of that library),
+ * containing parts covered by the terms of the OpenSSL License and the
+ * original SSLeay license, the licensors of this Program grant you
+ * additional permission to convey the resulting work. Corresponding
+ * Source for a non-source form of such a combination shall include the
+ * source code for the parts of LibreSSL used as well as that of the
+ * covered work.
+ */
+
+#include <err.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/time.h>
+#include <sysexits.h>
+#include <tls.h>
+#include <unistd.h>
+
+static bool verbose;
+static struct tls *client;
+static const char *chan;
+
+static void clientWrite(const char *ptr, size_t len) {
+	if (verbose) printf("%.*s", (int)len, ptr);
+	while (len) {
+		ssize_t ret = tls_write(client, ptr, len);
+		if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue;
+		if (ret < 0) errx(EX_IOERR, "tls_write: %s", tls_error(client));
+		ptr += ret;
+		len -= ret;
+	}
+}
+
+static void format(const char *format, ...) {
+	char buf[1024];
+	va_list ap;
+	va_start(ap, format);
+	int len = vsnprintf(buf, sizeof(buf), format, ap);
+	va_end(ap);
+	if ((size_t)len > sizeof(buf) - 1) errx(EX_DATAERR, "message too large");
+	clientWrite(buf, len);
+}
+
+static bool joined;
+static bool reverse;
+static bool copy;
+
+static void handle(char *line) {
+	char *tags = NULL;
+	char *origin = NULL;
+	if (line && line[0] == '@') tags = 1 + strsep(&line, " ");
+	if (line && line[0] == ':') origin = 1 + strsep(&line, " ");
+	char *cmd = strsep(&line, " ");
+	if (!cmd) return;
+	if (!strcmp(cmd, "CAP")) {
+		char *param = strsep(&cmd, " ");
+		if (!param) errx(EX_PROTOCOL, "CAP missing parameter");
+		if (!strcmp(param, "NAK")) {
+			errx(EX_CONFIG, "server does not support message-tags");
+		}
+		format("CAP END\r\n");
+	} else if (!strcmp(cmd, "001")) {
+		format("JOIN %s\r\n", chan);
+		joined = true;
+	} else if (!strcmp(cmd, "PING")) {
+		format("PONG %s\r\n", line);
+	} else if (copy && !strcmp(cmd, "TAGMSG") && tags) {
+		if (strstr(tags, "typing=")) {
+			format("@%s TAGMSG %s\r\n", tags, chan);
+		}
+	} else if (reverse && !strcmp(cmd, "TAGMSG") && tags && origin) {
+		char *nick = strsep(&origin, "!");
+		if (strstr(tags, "typing=active")) {
+			format("PRIVMSG %s :\u2328\uFE0F %s is typing!\r\n", chan, nick);
+		} else if (strstr(tags, "typing=paused")) {
+			format("PRIVMSG %s :\U0001F914 %s is thinking!\r\n", chan, nick);
+		} else if (strstr(tags, "typing=done")) {
+			format("PRIVMSG %s :\u270B %s stopped typing!\r\n", chan, nick);
+		}
+	}
+}
+
+static void timer(int sig) {
+	(void)sig;
+	if (!joined) return;
+	const char *status = (arc4random_uniform(4) ? "active" : "done");
+	format(
+		"@+typing=%s;+draft/typing=%s TAGMSG %s\r\n",
+		status, status, chan
+	);
+}
+
+int main(int argc, char *argv[]) {
+	const char *host = NULL;
+	const char *port = "6697";
+	const char *cert = NULL;
+	const char *nick = "typer";
+	const char *user = "typer";
+	bool passive = false;
+
+	for (int opt; 0 < (opt = getopt(argc, argv, "CPRc:n:p:u:v"));) {
+		switch (opt) {
+			break; case 'C': copy = true;
+			break; case 'P': passive = true;
+			break; case 'R': reverse = true;
+			break; case 'c': cert = optarg;
+			break; case 'n': nick = optarg;
+			break; case 'p': port = optarg;
+			break; case 'u': user = optarg;
+			break; case 'v': verbose = true;
+			break; default:  return EX_USAGE;
+		}
+	}
+	if (argc - optind < 2) errx(EX_USAGE, "host and chan required");
+	host = argv[optind];
+	chan = argv[optind + 1];
+
+	client = tls_client();
+	if (!client) errx(EX_SOFTWARE, "tls_client");
+
+	struct tls_config *config = tls_config_new();
+	if (!config) errx(EX_SOFTWARE, "tls_config_new");
+
+	if (cert) {
+		int error = tls_config_set_keypair_file(config, cert, cert);
+		if (error) errx(EX_CONFIG, "%s: %s", cert, tls_config_error(config));
+	}
+
+	int error = tls_configure(client, config);
+	if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client));
+	tls_config_free(config);
+
+	error = tls_connect(client, host, port);
+	if (error) errx(EX_UNAVAILABLE, "tls_connect: %s", tls_error(client));
+
+	format(
+		"CAP REQ :message-tags%s\r\n"
+		"NICK %s\r\n"
+		"USER %s 0 * :typer\r\n",
+		(passive ? " causal.agency/passive" : ""),
+		nick, user
+	);
+
+	if (!copy && !reverse) {
+		signal(SIGALRM, timer);
+		struct itimerval itimer = {
+			.it_interval.tv_sec = 5,
+			.it_value.tv_sec = 5
+		};
+		error = setitimer(ITIMER_REAL, &itimer, NULL);
+		if (error) err(EX_OSERR, "setitimer");
+	}
+
+	size_t len = 0;
+	char buf[4096];
+	for (;;) {
+		ssize_t read = tls_read(client, &buf[len], sizeof(buf) - len);
+		if (read == TLS_WANT_POLLIN || read == TLS_WANT_POLLOUT) continue;
+		if (read < 0) errx(EX_IOERR, "tls_read: %s", tls_error(client));
+		if (!read) errx(EX_UNAVAILABLE, "server disconnected");
+		len += read;
+
+		char *crlf;
+		char *line = buf;
+		for (;;) {
+			crlf = memmem(line, &buf[len] - line, "\r\n", 2);
+			if (!crlf) break;
+			crlf[0] = '\0';
+			if (verbose) printf("%s\n", line);
+			handle(line);
+			line = &crlf[2];
+		}
+		len -= line - buf;
+		memmove(buf, line, len);
+	}
+}