commit 1774372c3a85de00b6af4495321fc5f8d9aed401
parent b9bbb6eeb17a6a150b0af930822d864438500da0
Author: MikoĊaj Lenczewski <mblenczewski@gmail.com>
Date: Mon, 6 May 2024 20:33:27 +0000
Null-op bot that tries to connect to twitch over irc
Diffstat:
10 files changed, 598 insertions(+), 19 deletions(-)
diff --git a/build.sh b/build.sh
@@ -1,8 +1,6 @@
#!/bin/sh
CC="${CC:-cc}"
-AR="${AR:-ar}"
-RANLIB="${RANLIB:-ranlib}"
WARNINGS="-Wall -Wextra -Wpedantic -Werror"
@@ -14,11 +12,6 @@ LIBS="$(pkg-config --libs liburing)"
set -ex
-mkdir -p bin obj
+mkdir -p bin
-$CC -o obj/libtwitch.o -c libtwitch/libtwitch.c $CFLAGS $CPPFLAGS
-$AR -rcs bin/libtwitch.a obj/libtwitch.o
-$RANLIB bin/libtwitch.a
-
-$CC -o bin/twitchbot twitchbot/twitchbot.c bin/libtwitch.a \
- $CFLAGS $CPPFLAGS -Ilibtwitch $INCS $LIBS
+$CC -o bin/twitchbot twitchbot/twitchbot.c $CFLAGS $CPPFLAGS $INCS $LIBS
diff --git a/clean.sh b/clean.sh
@@ -2,4 +2,4 @@
set -ex
-rm -rf bin obj
+rm -rf bin
diff --git a/libtwitch/libtwitch.c b/libtwitch/libtwitch.c
@@ -1 +0,0 @@
-#include "libtwitch.h"
diff --git a/libtwitch/libtwitch.h b/libtwitch/libtwitch.h
@@ -1,4 +0,0 @@
-#ifndef LIBTWITCH_H
-#define LIBTWITCH_H
-
-#endif /* LIBTWITCH_H */
diff --git a/twitchbot/parse.c b/twitchbot/parse.c
@@ -0,0 +1,10 @@
+#include "twitchbot.h"
+
+struct msg
+msgparse(char *restrict begin, char *restrict end)
+{
+ (void) begin;
+ (void) end;
+
+ return (struct msg) {0};
+}
diff --git a/twitchbot/pools.c b/twitchbot/pools.c
@@ -0,0 +1,23 @@
+#include "twitchbot.h"
+
+bool
+io_pool_init(struct io_pool *pool, uint64_t capacity)
+{
+ if (!(pool->buf = malloc(capacity * sizeof *pool->buf)))
+ return false;
+
+ pool->freelist.head = pool->freelist.tail = NULL;
+ for (uint64_t i = 0; i < capacity; i++) {
+ struct io_request *req = pool->buf + i;
+
+ list_push(&pool->freelist, &req->freelist);
+ }
+
+ return true;
+}
+
+extern inline struct io_request *
+io_pool_alloc(struct io_pool *pool);
+
+extern inline void
+io_pool_free(struct io_pool *pool, struct io_request *req);
diff --git a/twitchbot/proto.c b/twitchbot/proto.c
@@ -0,0 +1,39 @@
+#include "twitchbot.h"
+
+void
+twitch_on_recv(struct state *state, struct io_request *req)
+{
+ (void) state;
+ (void) req;
+
+ switch (opts.protocol) {
+ case TWITCH_IRC:
+ break;
+ }
+}
+
+void
+twitch_send(struct state *state, char *buf, size_t len)
+{
+ struct io_request *req = io_pool_alloc(state->io_pool);
+
+ req->type = IO_REQUEST_SEND;
+ req->send.fd = state->sock;
+ req->send.buf = buf;
+ req->send.len = len;
+ req->send.flags = 0;
+
+ submit_io(state, req);
+}
+
+void
+twitch_on_send(struct state *state, struct io_request *req)
+{
+ (void) state;
+ (void) req;
+
+ switch (opts.protocol) {
+ case TWITCH_IRC:
+ break;
+ }
+}
diff --git a/twitchbot/twitchbot.c b/twitchbot/twitchbot.c
@@ -1,10 +1,302 @@
#include "twitchbot.h"
+struct opts opts = {
+ .addressfamily = AF_UNSPEC,
+ .protocol = TWITCH_IRC,
+ .mem_cap = 64 * MiB,
+};
+
+static char const *optstr = "hv46p:m:";
+
+static void
+usage(char **argv)
+{
+ fprintf(stderr, "Usage: %s [-hv46] [-p irc] [-m <mem-mib>] <channels...>\n", argv[0]);
+ fprintf(stderr, "\n");
+ fprintf(stderr, "\t-h :\n\t\tdisplays this help message\n");
+ fprintf(stderr, "\t-v :\n\t\tenables verbose logging\n");
+ fprintf(stderr, "\t-4, -6 :\n\t\tforce the use of IPv4 or IPv6, respectively\n");
+ fprintf(stderr, "\t-p :\n\t\tsets the chosen protocol (default: irc)\n");
+ fprintf(stderr, "\t-m :\n\t\tsets the maximum memory in MiB (default: 64 MiB)\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, "\tchannels :\n\t\tthe list of channels to attempt to join\n");
+}
+
+#include <getopt.h>
+
+static bool
+parse_args(int argc, char **argv)
+{
+ int opt;
+ while ((opt = getopt(argc, argv, optstr)) != -1) {
+ switch (opt) {
+ case 'v':
+ opts.verbose = true;
+ break;
+
+ case '4':
+ opts.addressfamily = AF_INET;
+ break;
+
+ case '6':
+ opts.addressfamily = AF_INET6;
+ break;
+
+ case 'p':
+ if (strcmp(optarg, "irc") == 0) {
+ opts.protocol = TWITCH_IRC;
+ } else {
+ return false;
+ }
+ break;
+
+ case 'm':
+ if (!(opts.mem_cap = strtoull(optarg, NULL, 0) * MiB))
+ return false;
+ break;
+
+ default:
+ return false;
+ }
+ }
+
+ opts.channels.ptr = argv + optind;
+ opts.channels.len = argc - optind;
+
+ if (!opts.channels.len)
+ return false;
+
+ return true;
+}
+
int
main(int argc, char **argv)
{
- (void) argc;
- (void) argv;
+ if (!parse_args(argc, argv)) {
+ usage(argv);
+ exit(EXIT_FAILURE);
+ }
+
+ struct io_uring uring;
+ unsigned entries = 1024, flags = 0;
+ if (io_uring_queue_init(entries, &uring, flags)) {
+ fprintf(stderr, "Failed to initialise io_uring\n");
+ return false;
+ }
+
+ struct arena arena = {
+ .ptr = malloc(opts.mem_cap),
+ .cap = opts.mem_cap,
+ .len = 0,
+ };
+
+ if (!arena.ptr) {
+ fprintf(stderr, "Failed to allocate arena memory\n");
+ return false;
+ }
+
+ struct io_pool io_pool;
+ if (!io_pool_init(&io_pool, entries)) {
+ fprintf(stderr, "Failed to initialise io request pool\n");
+ return false;
+ }
- return 0;
+ struct state state = {
+ .arena = &arena,
+ .io_pool = &io_pool,
+ .uring = &uring,
+ };
+
+ exit(run(&state));
+}
+
+static bool
+submit_io(struct state *state, struct io_request *req)
+{
+ struct io_uring_sqe *sqe = io_uring_get_sqe(state->uring);
+ if (!sqe)
+ return false;
+
+ switch (req->type) {
+ case IO_REQUEST_CONN:
+ io_uring_prep_connect(sqe, req->conn.fd, (struct sockaddr *) &req->conn.addr, req->conn.addrlen);
+
+ case IO_REQUEST_RECV:
+ io_uring_prep_recv(sqe, req->recv.fd, req->recv.buf, req->recv.len, req->recv.flags);
+ break;
+
+ case IO_REQUEST_SEND:
+ io_uring_prep_send(sqe, req->recv.fd, req->recv.buf, req->recv.len, req->recv.flags);
+ break;
+ }
+
+ io_uring_sqe_set_data(sqe, req);
+
+ io_uring_submit(state->uring);
+
+ return true;
}
+
+static bool
+setup_connect(struct state *state, struct io_request *req)
+{
+ struct addrinfo hints = {
+ .ai_flags = AI_NUMERICSERV,
+ .ai_family = opts.addressfamily,
+ .ai_socktype = SOCK_STREAM,
+ }, *addrinfo;
+
+ char *host = NULL, *serv = NULL;
+ switch (opts.protocol) {
+ case TWITCH_IRC:
+ host = "irc.chat.twitch.tv"; serv = "6667"; break;
+
+ /* TODO: websocket + ssl irc + ssl websocket */
+ }
+
+ if (getaddrinfo(host, serv, &hints, &addrinfo)) {
+ fprintf(stderr, "Could not get address information for %s:%s\n", host, serv);
+ return false;
+ }
+
+ char hoststr[NI_MAXHOST], servstr[NI_MAXSERV];
+ int nameinfo_flags = NI_NUMERICHOST | NI_NUMERICSERV;
+ if (opts.verbose && getnameinfo(addrinfo->ai_addr, addrinfo->ai_addrlen,
+ hoststr, NI_MAXHOST, servstr, NI_MAXSERV,
+ nameinfo_flags) == 0) {
+ fprintf(stderr, "Attempting to connect to %s:%s\n", hoststr, servstr);
+ }
+
+ req->type = IO_REQUEST_CONN;
+
+ req->conn.fd = socket(addrinfo->ai_family,
+ addrinfo->ai_socktype | SOCK_CLOEXEC,
+ addrinfo->ai_protocol);
+
+ memcpy(&req->conn.addr, addrinfo->ai_addr, addrinfo->ai_addrlen);
+ req->conn.addrlen = addrinfo->ai_addrlen;
+
+ freeaddrinfo(addrinfo);
+
+ return true;
+}
+
+int
+run(struct state *state)
+{
+ unsigned int timeout = 0;
+
+ struct io_request connreq;
+ if (!setup_connect(state, &connreq))
+ return EXIT_FAILURE;
+
+ submit_io(state, &connreq);
+
+ while (true) {
+ struct io_uring_cqe *cqe;
+ if (io_uring_wait_cqe(state->uring, &cqe) < 0)
+ continue;
+
+ int res = cqe->res;
+
+ struct io_request *req = io_uring_cqe_get_data(cqe);
+ switch (req->type) {
+ case IO_REQUEST_CONN: {
+ assert(req == &connreq);
+
+ if (res < 0) {
+ if (opts.verbose)
+ fprintf(stderr, "Failed to connect: %d %s\n",
+ -res, strerror(-res));
+
+ timeout = timeout ? (timeout * 2) : 1;
+
+ /* go on exponential cooldown */
+ unsigned int remaining = timeout;
+ fprintf(stderr, "Waiting for %u seconds\n", remaining);
+ while ((remaining = sleep(remaining)));
+
+ goto reconnect;
+ }
+
+ timeout = 0;
+
+ on_connect(state);
+ } break;
+
+ case IO_REQUEST_RECV: {
+ if (res < 0) goto reconnect;
+
+ twitch_on_recv(state, req);
+ } break;
+
+ case IO_REQUEST_SEND: {
+ if (res < 0) goto reconnect;
+
+ twitch_on_send(state, req);
+ } break;
+ }
+
+ assert(req != &connreq);
+
+ io_pool_free(state->io_pool, req);
+
+next_cqe:
+ io_uring_cqe_seen(state->uring, cqe);
+ continue;
+
+reconnect:
+ close(state->sock);
+
+ if (!setup_connect(state, req))
+ return EXIT_FAILURE;
+
+ submit_io(state, req);
+ goto next_cqe;
+ }
+
+ return EXIT_SUCCESS;
+}
+
+/* possible messages (and their direction)
+ * ---------------------------------------------------------------------------
+ * PASS oauth:<token> - sent - sent to authenticate as a bot
+ * NICK <nick> - sent - sent to set the bots username
+ * NOTICE
+ *
+ * PING :tmi.twitch.tv - received - keepalive request
+ * PONG :tmi.twitch.tv - sent - keepalive response
+ *
+ * JOIN #channel{,#channel} - sent - sent to join a channel
+ * PART - sent/received - sent to leave a channel, received when banned from a channel
+ * PRIVMSG #channel :msg - sent/received - messages
+ */
+
+void
+on_connect(struct state *state)
+{
+ (void) state;
+
+ fprintf(stderr, "Connected to twitch!\n");
+
+ // TODO: request capabilities
+ // TODO: authenticate
+ // TODO: send nick
+ // TODO: join all requested channels
+
+ close(state->sock);
+}
+
+void
+on_msg(struct state *state, struct msg msg)
+{
+ (void) state;
+ (void) msg;
+
+ // TODO: handle message
+}
+
+#include "proto.c"
+#include "parse.c"
+#include "pools.c"
+#include "utils.c"
diff --git a/twitchbot/twitchbot.h b/twitchbot/twitchbot.h
@@ -1,6 +1,220 @@
#ifndef TWITCHBOT_H
#define TWITCHBOT_H
-#include "libtwitch.h"
+#define _XOPEN_SOURCE 700
+#define _GNU_SOURCE
+#define _DEFAULT_SOURCE
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+
+#include <fcntl.h>
+
+#include <liburing.h>
+
+#define KiB 1024
+#define MiB (1024 * KiB)
+#define GiB (1024 * MiB)
+
+#define ARRLEN(arr) (sizeof (arr) / sizeof (arr)[0])
+
+#define ALIGN_PREV(v, align) ((v) & ~((align) - 1))
+#define ALIGN_NEXT(v, align) ALIGN_PREV((v) + (align) - 1, (align))
+
+struct arena {
+ void *ptr;
+ uint64_t cap, len;
+};
+
+inline void *
+arena_alloc(struct arena *arena, uint64_t size, uint64_t alignment)
+{
+ uint64_t aligned_len = ALIGN_NEXT(arena->len, alignment);
+ if (arena->cap < aligned_len + size)
+ return false;
+
+ void *ptr = (uint8_t *) arena->ptr + aligned_len;
+ arena->len = aligned_len + size;
+
+ return ptr;
+}
+
+#define PUSH_SIZED(arena, T) \
+ arena_alloc(arena, sizeof(T), alignof(T))
+
+#define PUSH_ARRAY(arena, T, n) \
+ arena_alloc(arena, sizeof(T) * (n), alignof(T))
+
+inline void
+arena_reset(struct arena *arena)
+{
+ arena->len = 0;
+}
+
+struct list_node;
+struct list_node {
+ struct list_node *prev, *next;
+};
+
+#define FROM_LIST_NODE(node, T, member) \
+ ((T *) (((uintptr_t) node) - offsetof(T, member)))
+
+struct list {
+ struct list_node *head, *tail;
+};
+
+inline void
+list_push(struct list *list, struct list_node *node)
+{
+ if (!list->head)
+ list->head = node;
+
+ if (list->tail)
+ list->tail->next = node;
+
+ node->next = NULL;
+ node->prev = list->tail;
+ list->tail = node;
+}
+
+inline struct list_node *
+list_pop(struct list *list)
+{
+ if (!list->head)
+ return NULL;
+
+ if (list->head == list->tail)
+ list->tail = NULL;
+
+ struct list_node *node = list->head;
+ list->head = node->next;
+
+ return node;
+}
+
+/* twitchbot definitions */
+enum twitch_proto {
+ TWITCH_IRC,
+};
+
+struct opts {
+ bool verbose;
+
+ int addressfamily;
+
+ enum twitch_proto protocol;
+ uint64_t mem_cap;
+
+ struct {
+ char **ptr;
+ size_t len;
+ } channels;
+};
+
+struct io_request {
+ enum {
+ IO_REQUEST_CONN,
+ IO_REQUEST_RECV,
+ IO_REQUEST_SEND,
+ } type;
+
+ union {
+ struct {
+ int fd;
+ struct sockaddr_storage addr;
+ socklen_t addrlen;
+ } conn;
+
+ struct {
+ int fd;
+ void *buf;
+ size_t len;
+ int flags;
+ } recv, send;
+ };
+
+ struct list_node freelist;
+};
+
+struct io_pool {
+ struct io_request *buf;
+ struct list freelist;
+};
+
+bool
+io_pool_init(struct io_pool *pool, uint64_t capacity);
+
+inline struct io_request *
+io_pool_alloc(struct io_pool *pool)
+{
+ struct list_node *node = list_pop(&pool->freelist);
+ if (!node)
+ return NULL;
+
+ return FROM_LIST_NODE(node, struct io_request, freelist);
+}
+
+inline void
+io_pool_free(struct io_pool *pool, struct io_request *req)
+{
+ list_push(&pool->freelist, &req->freelist);
+}
+
+struct state {
+ struct arena *arena;
+ struct io_pool *io_pool;
+ struct io_uring *uring;
+
+ int sock;
+
+ struct {
+ char *begin, *end;
+ size_t len;
+ } buffer;
+};
+
+int
+run(struct state *state);
+
+/* protocol handlers */
+
+void
+twitch_on_recv(struct state *state, struct io_request *req);
+
+void
+twitch_send(struct state *state, char *buf, size_t len);
+
+void
+twitch_on_send(struct state *state, struct io_request *req);
+
+/* user-defined handlers */
+
+void
+on_connect(struct state *state);
+
+struct msgfrag {
+ char *ptr;
+ int len;
+};
+
+struct msg {
+ struct msgfrag prefix, command, params;
+};
+
+struct msg
+msgparse(char *restrict begin, char *restrict end);
+
+void
+on_msg(struct state *state, struct msg msg);
#endif /* TWITCHBOT_H */
diff --git a/twitchbot/utils.c b/twitchbot/utils.c
@@ -0,0 +1,13 @@
+#include "twitchbot.h"
+
+extern inline void *
+arena_alloc(struct arena *arena, uint64_t size, uint64_t alignment);
+
+extern inline void
+arena_reset(struct arena *arena);
+
+extern inline void
+list_push(struct list *list, struct list_node *node);
+
+extern inline struct list_node *
+list_pop(struct list *list);