twitchbot

twitchbot.git
git clone git://git.lenczewski.org/twitchbot.git
Log | Files | Refs | README | LICENSE

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:
Mbuild.sh | 11++---------
Mclean.sh | 2+-
Dlibtwitch/libtwitch.c | 1-
Dlibtwitch/libtwitch.h | 4----
Atwitchbot/parse.c | 10++++++++++
Atwitchbot/pools.c | 23+++++++++++++++++++++++
Atwitchbot/proto.c | 39+++++++++++++++++++++++++++++++++++++++
Mtwitchbot/twitchbot.c | 298++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtwitchbot/twitchbot.h | 216++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Atwitchbot/utils.c | 13+++++++++++++
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);