diff options
author | Raymaekers Luca <luca@spacehb.net> | 2025-04-27 12:52:06 +0200 |
---|---|---|
committer | Raymaekers Luca <luca@spacehb.net> | 2025-04-27 13:05:34 +0200 |
commit | f87f7b4f0aaccc65d03ccee5bb11915ead6fb0e1 (patch) | |
tree | d54df0bfde3dbffa02b1f138af4f12456f261e54 /chatty.c | |
parent | 0574f5a7c5159a2ae1d7d2182cec982509947db9 (diff) |
First pass at preparing for Github
Diffstat (limited to 'chatty.c')
-rw-r--r-- | chatty.c | 831 |
1 files changed, 0 insertions, 831 deletions
diff --git a/chatty.c b/chatty.c deleted file mode 100644 index 3300ce0..0000000 --- a/chatty.c +++ /dev/null @@ -1,831 +0,0 @@ -#define TB_IMPL -#include "external/termbox2.h" -#undef TB_IMPL - -#include <arpa/inet.h> -#include <locale.h> -#include <poll.h> -#include <pthread.h> -#include <sys/socket.h> -#include <sys/wait.h> - -#define TIMEOUT_POLL 60 * 1000 -// time to reconnect in seconds -#define TIMEOUT_RECONNECT 1 -#define MAX_INPUT_LEN 512 -// Filepath where user ID is stored -#define ID_FILE "_id" -// Filepath where logged -#define LOGFILE "chatty.log" -// enable logging -#define LOGGING -// Number of spaces inserted when pressing Tab/Ctrl+I -#define TAB_WIDTH 4 - -#ifndef Assert -#ifdef DEBUG -#define Assert(expr) if (!(expr)) \ - { \ - tb_shutdown(); \ - raise(SIGTRAP); \ - } -#else -#define Assert(expr) ; -#endif // DEBUG -#endif // Assert - -#define CHATTY_IMPL -#include "chatty.h" -#include "protocol.h" - -#define TEXTBOX_MAX_INPUT MAX_INPUT_LEN -#include "ui.h" - -#define ARENA_IMPL -#include "arena.h" - -enum { FDS_BI = 0, // for one-way communication with the server (eg. TextMessage) - FDS_UNI, // For two-way communication with the server (eg. IDMessage) - FDS_TTY, - FDS_RESIZE, - FDS_MAX }; - -typedef struct { - u8 Author[AUTHOR_LEN]; - ID ID; -} User; -#define USER_FMT "[%s](%lu)" -#define USER_ARG(client) client.Author, client.ID - -typedef struct { - s32 NumRead; - u32 Error; -} command_output; - -// User used by chatty -global_variable User user = {0}; -// Address of chatty server -global_variable struct sockaddr_in address; - -// fill str array with char -void -fillstr(u32* Str, u32 ch, u32 Len) -{ - for (u32 i = 0; i < Len; i++) - Str[i] = ch; -} - -// Centered popup displaying message in the appropriate cololrs -void -popup(u32 fg, u32 bg, u8* text) -{ - u32 len = strlen((char*)text); - Assert(len > 0); - tb_print(global.width / 2 - len / 2, global.height / 2, fg, bg, (char*)text); -} - -// Returns client in clientsArena matching id -// Returns user if the id was the user's ID -// Returns 0 if nothing was found -User* -get_user_by_id(Arena* clientsArena, ID id) -{ - // User is not in the clientsArena - if (id == user.ID) return &user; - - User* clients = clientsArena->addr; - for (u64 i = 0; i < (clientsArena->pos / sizeof(*clients)); i++) - { - if (clients[i].ID == id) - return clients + i; - } - return 0; -} - -// Request information of client from fd byd id and add it to clientsArena -// Returns pointer to added client -User* -add_user_info(Arena* clientsArena, s32 fd, u64 id) -{ - // Request information about ID - HeaderMessage header = HEADER_INIT(HEADER_TYPE_ID); - header.id = user.ID; - IDMessage message = {id}; - s32 nsend = sendAnyMessage(fd, header, &message); - Assert(nsend != -1); - - // Wait for response - IntroductionMessage introduction_message; - recvAnyMessageType(fd, &header, &introduction_message, HEADER_TYPE_INTRODUCTION); - - // Add the information - User* client = ArenaPush(clientsArena, sizeof(*client)); - memcpy(client->Author, introduction_message.author, AUTHOR_LEN); - client->ID = id; - - LoggingF("Got " USER_FMT "\n", USER_ARG((*client))); - return client; -} - -// Tries to connect to address and populates resulting file descriptors in ConnectionResult. -s32 -get_connection(struct sockaddr_in* address) -{ - s32 fd = socket(AF_INET, SOCK_STREAM, 0); - if (fd == -1) return -1; - - s32 err = connect(fd, (struct sockaddr*)address, sizeof(*address)); - if (err) return -1; - - return fd; -} - -// Authenticates a file descriptor with either the user's id if non-zero or -// it's information if id is zero. -// Returns 0 if an error occurred. Non-zero on success. -u32 -authenticate(User* user, s32 fd) -{ - /* Scenario 1: Already have an ID */ - if (user->ID) - { - HeaderMessage header = HEADER_INIT(HEADER_TYPE_ID); - IDMessage message = {user->ID}; - s32 nsend = sendAnyMessage(fd, header, &message); - Assert(nsend != -1); - - ErrorMessage error_message; - s32 nrecv = recvAnyMessageType(fd, &header, &error_message, HEADER_TYPE_ERROR); - Assert(nrecv != -1); - // TODO: handle not found - if (nrecv == 0) - return 0; - - if (error_message.type == ERROR_TYPE_SUCCESS) - return 1; - else - return 0; - } - /* Scenario 2: No ID, request one from server */ - else - { - HeaderMessage header = HEADER_INIT(HEADER_TYPE_INTRODUCTION); - IntroductionMessage message; - memcpy(message.author, user->Author, AUTHOR_LEN); - s32 nsend = sendAnyMessage(fd, header, &message); - Assert(nsend != -1); - - IDMessage id_message; - s32 nrecv = recvAnyMessageType(fd, &header, &id_message, HEADER_TYPE_ID); - Assert(nrecv != -1); - user->ID = id_message.id; - return 1; - } -} - -// Connect to *address_ptr of type `struct sockaddr_in*`. If it failed wait for TIMEOUT_RECONNECT -// seconds. -// This function is meant to be run by a thread. -// An offline server means fds[FDS_SERVER] is set to -1. When online -// it is set to with the appropriate file descriptor. -// Returns 0. -#define Miliseconds(s) (s*1000*1000) -void* -thread_reconnect(void* fds_ptr) -{ - s32 unifd, bifd; - struct pollfd* fds = fds_ptr; - struct timespec t = { 0, Miliseconds(300) }; // 300 miliseconds - LoggingF("Trying to reconnect\n"); - while (1) - { - // timeout - nanosleep(&t, &t); - - bifd = get_connection(&address); - if (bifd == -1) - { - LoggingF("errno: %d\n", errno); - continue; - } - unifd = get_connection(&address); - if (unifd == -1) - { - LoggingF("errno: %d\n", errno); - close(bifd); - continue; - } - - LoggingF("Reconnect succeeded (%d, %d), authenticating\n", unifd, bifd); - - if (authenticate(&user, bifd) && - authenticate(&user, unifd)) - { - break; - } - - close(bifd); - close(unifd); - - LoggingF("Failed, retrying...\n"); - } - - fds[FDS_BI].fd = bifd; - fds[FDS_UNI].fd = unifd; - - // Redraw screen - raise(SIGWINCH); - - return 0; -} - -command_output -run_command_get_output(char *Command, char *Argv[], u8 *OutputBuffer, int Len) -{ - command_output Result = {0}; - - int CommandPipe[2]; - int Error = pipe(CommandPipe); - Assert(Error != -1); - - int Pid = fork(); - Assert(Pid != -1); - - // Run command in child - if (!Pid) - { - dup2(CommandPipe[1], STDOUT_FILENO); //redirect stdout to Pipe - close(CommandPipe[0]); - close(CommandPipe[1]); - - int fd = open("/dev/null", O_WRONLY); - dup2(fd, STDERR_FILENO); - - execvp(Command, Argv); - } - - // Wait for child - int statval; - waitpid(Pid, &statval, 0); - - if(WIFEXITED(statval)) - { - int ExitCode = WEXITSTATUS(statval); - if (ExitCode) - { - Result.Error = ExitCode; - } - } - else - { - Result.Error = 1; - return Result; - } - - close(CommandPipe[1]); - - Result.NumRead = read(CommandPipe[0], OutputBuffer, Len); - Assert(Result.NumRead != -1); - - return Result; -} - -// home screen, the first screen the user sees -// it displays a prompt with the user input of input_len wide characters -// and the received messages from msgsArena -void -DisplayChat(Arena* ScratchArena, - Arena* MessagesArena, u32 MessagesNum, - Arena* ClientsArena, struct pollfd* fds, - wchar_t Input[], u32 InputLen) -{ - rect TextBox = { - 1, 0, global.width - 2, 3, - }; - u32 FreeHeight = global.height - TextBox.H; - TextBox.Y = FreeHeight; - -#define MIN_TEXT_WIDTH_FOR_WRAPPING 20 - s32 MinBoxWidth = TEXTBOX_MIN_WIDTH; - s32 InputBoxTextWidth = TextBox.W - MinBoxWidth + 2; - bool ShouldIncreaseSize = ( - (s32)InputLen >= InputBoxTextWidth && - InputBoxTextWidth > MIN_TEXT_WIDTH_FOR_WRAPPING - ); - if (ShouldIncreaseSize) - { - TextBox.H++; - } -#undef MIN_TEXT_WIDTH_FOR_WRAPPING - - rect TextR = { - TextBox.X + 2, TextBox.Y + 1, - TextBox.W - 2*TEXTBOX_PADDING_X - 2*TEXTBOX_BORDER_WIDTH, - TextBox.H - 2*TEXTBOX_BORDER_WIDTH - }; - - if (global.height < TextBox.H || global.width < TextBox.W) - { - tb_hide_cursor(); - return; - } - - bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]); - global.cursor_x = TextR.X; - global.cursor_y = TextR.Y; - DrawBox(TextBox, 0); - - // InputBox(TextBox, Input, InputLen, True); - - // Print vertical bar - s32 VerticalBarOffset = TIMESTAMP_LEN + AUTHOR_LEN + 2; - for (u32 Y = 0; Y < FreeHeight; Y++) - tb_print(VerticalBarOffset, Y, 0, 0, "│"); - - // show error popup if server disconnected - if (fds[FDS_UNI].fd == -1 || fds[FDS_BI].fd == -1) - { - popup(TB_RED, TB_BLACK, (u8*)"Server disconnected."); - } - - // Print messages in msgsArena, if there are too many to display, start printing from an offset. - // Looks like this: - // 03:24:29 [1234567890ab] hello homes how are - // you doing? - // 03:24:33 [TlasT] │ I am fine - // 03:24:33 [Fin] │ I am too - { - - // If there is not enough space to draw, do not draw - if (FreeHeight <= 0) return; - - // Used to go to the next message in MessagesArena by incrementing with the messages' size. - u8* MessageAddress = MessagesArena->addr; - Assert(MessageAddress != 0); - - // Skip messages if there is not enough space to display them all - u32 MessagesOffset = (MessagesNum > FreeHeight) ? MessagesNum - FreeHeight : 0; - for (u32 MessageIndex = 0; MessageIndex < MessagesOffset; MessageIndex++) - { - HeaderMessage* header = (HeaderMessage*)MessageAddress; - MessageAddress += sizeof(*header); - - switch (header->type) - { - case HEADER_TYPE_TEXT: - { - TextMessage* message = (TextMessage*)MessageAddress; - MessageAddress += TEXTMESSAGE_SIZE; - MessageAddress += message->len * sizeof(*message->text); - break; - } - case HEADER_TYPE_PRESENCE: - MessageAddress += sizeof(PresenceMessage); - break; - case HEADER_TYPE_HISTORY: - MessageAddress += sizeof(HistoryMessage); - break; - default: - // unhandled message type - Assert(0); - } - } - - u32 MessageY = 0; - - for (u32 i = MessagesOffset; - i < MessagesNum; - i++) - { - if (MessageY >= FreeHeight) break; - - HeaderMessage* header = (HeaderMessage*)MessageAddress; - MessageAddress += sizeof(*header); - - User* client = get_user_by_id(ClientsArena, header->id); - if (!client) - { - LoggingF("User not known, requesting from server\n"); - client = add_user_info(ClientsArena, fds[FDS_BI].fd, header->id); - } - Assert(client); - - switch (header->type) - { - case HEADER_TYPE_TEXT: - { - TextMessage* message = (TextMessage*)MessageAddress; - - - // Color own messages - u32 fg = 0; - if (user.ID == header->id) - { - fg = TB_CYAN; - } - else - { - fg = TB_MAGENTA; - } - - // prefix is of format "HH:MM:SS [<author>] ", create it - u8 timestamp[TIMESTAMP_LEN]; - formatTimestamp(timestamp, message->timestamp); - - tb_printf(0, MessageY, TB_WHITE, 0, "%s", timestamp); - tb_printf(TIMESTAMP_LEN, MessageY, fg, 0, "[%s]", client->Author); - - // Only display when there is enough space - if (global.width > VerticalBarOffset + 2) - { - raw_result RawText = markdown_to_raw(ScratchArena, (wchar_t*)&message->text, message->len); - markdown_formatoptions MDFormat = preprocess_markdown(ScratchArena, - (wchar_t*)&message->text, - message->len); - - u32 timesWrapped = tb_print_wrapped_with_markdown(VerticalBarOffset + 2, MessageY, fg, 0, - RawText.Text, RawText.Len, - global.width, global.height, MDFormat); - - // Free the memory - ScratchArena->pos = 0; - - MessageY += timesWrapped; - } - else - { - // We still displayed the timestamp so we need to increment the Y. - MessageY++; - } - - u32 message_size = TEXTMESSAGE_SIZE + message->len * sizeof(*message->text); - MessageAddress += message_size; - } break; - case HEADER_TYPE_PRESENCE: - { - PresenceMessage* message = (PresenceMessage*)MessageAddress; - tb_printf(TIMESTAMP_LEN, MessageY, TB_MAGENTA, 0, "[%s]", client->Author); - - // Wrap Text in '*' - u8 *Text = presenceTypeString(message->type); - u32 Len = 0; - while(Text[Len]) Len++; - u32 FormattedText[Len+2]; - FormattedText[0] = '*'; - FormattedText[Len+1] = '*'; - for (u32 i = 1; i < Len + 1; i++) FormattedText[i] = Text[i-1]; - - tb_print_markdown(VerticalBarOffset + 2, MessageY, 0, 0, FormattedText, Len + 2); - - MessageY++; - MessageAddress += sizeof(*message); - } break; - case HEADER_TYPE_HISTORY: - { - HistoryMessage* message = (HistoryMessage*)MessageAddress; - MessageAddress += sizeof(*message); - // TODO: implement - } break; - default: - tb_printf(0, MessageY, 0, 0, "%s", headerTypeString(header->type)); - MessageY++; - break; - } - } - - } -} - -int -main(int argc, char** argv) -{ - if (argc < 2) - { - fprintf(stderr, "usage: chatty <username>\n"); - return 1; - } - - u32 arg_len = strlen(argv[1]); - Assert(arg_len <= AUTHOR_LEN - 1); - memcpy(user.Author, argv[1], arg_len); - user.Author[arg_len] = '\0'; - - s32 err = 0; // error code for functions - - u32 MessagesNum = 0; // Number of messages in msgsArena - s32 nrecv = 0; // number of bytes received - - wchar_t Input[MAX_INPUT_LEN] = {0}; // input buffer - u32 InputIndex = 0; // number of characters in input - - Arena ScratchArena; - Arena MessagesArena; - Arena ClientsArena; - ArenaAlloc(&MessagesArena, Megabytes(64)); // Messages received & sent - ArenaAlloc(&ClientsArena, Megabytes(1)); // Arena for storing clients - ArenaAlloc(&ScratchArena, Megabytes(1)); // Arena for storing clients - - struct tb_event ev; // event fork keypress & resize - u8 quit = 0; // boolean to indicate if we want to quit the main loop - u8* quitmsg = 0; // this string will be printed before returning from main - - pthread_t thr_rec; // thread for reconnecting to server when disconnected - -#ifdef LOGGING - LogFD = open(LOGFILE, O_RDWR | O_CREAT | O_TRUNC, 0600); - Assert(LogFD != -1); -#else - logfd = 2; // stderr -#endif - - // poopoo C cannot infer type - struct pollfd fds[FDS_MAX] = { - {-1, POLLIN, 0}, // FDS_BI - {-1, POLLIN, 0}, // FDS_UNI - {-1, POLLIN, 0}, // FDS_TTY - {-1, POLLIN, 0}, // FDS_RESIZE - }; - - address = (struct sockaddr_in){ - AF_INET, - htons(PORT), - {0}, - {0}, - }; - -#ifdef IMPORT_ID - // File for storing the user's ID. - u32 idfile = open(ID_FILE, O_RDWR | O_CREAT, 0600); - s32 nread = read(idfile, &user.id, sizeof(user.id)); - Assert(nread != -1); -#endif - /* Authentication */ - { - s32 unifd, bifd; - bifd = get_connection(&address); - if (bifd == -1) - { - LoggingF("errno: %d\n", errno); - return 1; - } - unifd = get_connection(&address); - if (unifd == -1) - { - LoggingF("errno: %d\n", errno); - return 1; - } - LoggingF("(%d,%d)\n", bifd, unifd); - if (!authenticate(&user, bifd) || - !authenticate(&user, unifd)) - { - LoggingF("errno: %d\n", errno); - return 1; - } - else - { - LoggingF("Authenticated (%d,%d)\n", bifd, unifd); - } - fds[FDS_BI].fd = bifd; - fds[FDS_UNI].fd = unifd; - } - -#ifdef IMPORT_ID - // Save id - write(idfile, &user.id, sizeof(user.id)); -#endif - - LoggingF("Got ID: %lu\n", user.ID); - - // for wide character printing - Assert(setlocale(LC_ALL, "")); - - // init - tb_init(); - tb_get_fds(&fds[FDS_TTY].fd, &fds[FDS_RESIZE].fd); - - DisplayChat(&ScratchArena, - &MessagesArena, MessagesNum, - &ClientsArena, fds, - Input, InputIndex); - tb_present(); - - // main loop - while (!quit) - { - err = poll(fds, FDS_MAX, TIMEOUT_POLL); - // ignore resize events and use them to redraw the screen - Assert(err != -1 || errno == EINTR); - - tb_clear(); - - if (fds[FDS_UNI].revents & POLLIN) - { - // got data from server - HeaderMessage header; - nrecv = recv(fds[FDS_UNI].fd, &header, sizeof(header), 0); - Assert(nrecv != -1); - - // Server disconnects - if (nrecv == 0) - { - // close diconnected server's socket - err = close(fds[FDS_UNI].fd); - Assert(err == 0); - fds[FDS_UNI].fd = -1; // ignore - // start trying to reconnect in a thread - err = pthread_create(&thr_rec, 0, &thread_reconnect, (void*)fds); - Assert(err == 0); - } - else - { - if (header.version != PROTOCOL_VERSION) - { - LoggingF("Header received does not match version\n"); - continue; - } - - void* addr = ArenaPush(&MessagesArena, sizeof(header)); - memcpy(addr, &header, sizeof(header)); - - // Messages handled from server - switch (header.type) - { - case HEADER_TYPE_TEXT: - recvTextMessage(&MessagesArena, fds[FDS_UNI].fd); - MessagesNum++; - break; - case HEADER_TYPE_PRESENCE:; - PresenceMessage* message = ArenaPush(&MessagesArena, sizeof(*message)); - nrecv = recv(fds[FDS_UNI].fd, message, sizeof(*message), 0); - Assert(nrecv != -1); - Assert(nrecv == sizeof(*message)); - MessagesNum++; - break; - default: - LoggingF("Got unhandled message: %s\n", headerTypeString(header.type)); - break; - } - } - } - - if (fds[FDS_TTY].revents & POLLIN) - { - // got a key event - tb_poll_event(&ev); - - switch (ev.key) - { - case TB_KEY_CTRL_W: - // delete consecutive whitespace - while (InputIndex) - { - if (Input[InputIndex - 1] == L' ') - { - Input[InputIndex - 1] = 0; - InputIndex--; - continue; - } - break; - } - // delete until whitespace - while (InputIndex) - { - if (Input[InputIndex - 1] == L' ') - break; - // erase - Input[InputIndex - 1] = 0; - InputIndex--; - } - break; - case TB_KEY_CTRL_Z: - { - pid_t pid = getpid(); - tb_shutdown(); - kill(pid, SIGSTOP); - tb_init(); - } break; - case TB_KEY_CTRL_Y: // Paste clipboard contents to input - { - u32 OutputBufferLen = MAX_INPUT_LEN - InputIndex; - if (OutputBufferLen <= 0) break; - - u8 OutputBuffer[OutputBufferLen]; - - char *PathName = "xclip"; - char *Argv[] = {PathName, "-o", "-sel", "c", 0}; - - command_output Output = run_command_get_output(PathName, Argv, OutputBuffer, OutputBufferLen - 1); - if (Output.Error) break; - - // Remove trailing whitespace - int BufferIndex = Output.NumRead - 1; - while (BufferIndex > 0 && - (OutputBuffer[BufferIndex] == '\n' || - OutputBuffer[BufferIndex] == '\t')) - { - OutputBuffer[BufferIndex] = 0; - BufferIndex--; - } - - // Append to output - for (s32 BufferIndex = 0; BufferIndex < Output.NumRead; BufferIndex++) - { - // convert u8 to u32 - u32 ch = OutputBuffer[BufferIndex]; - Input[InputIndex] = ch; - InputIndex++; - } - - } break; - case TB_KEY_CTRL_I: - { - for (u32 i = 0; - i < TAB_WIDTH && InputIndex < MAX_INPUT_LEN - 1; - i++) - { - Input[InputIndex] = L' '; - InputIndex++; - } - } break; - case TB_KEY_BACKSPACE2: - if (InputIndex) InputIndex--; - Input[InputIndex] = 0; - break; - case TB_KEY_CTRL_D: - case TB_KEY_CTRL_C: - quit = 1; - break; - case TB_KEY_CTRL_M: // send message - { - raw_result RawText = markdown_to_raw(0, Input, InputIndex); - - if (RawText.Len == 0) - // do not send empty message - break; - if (fds[FDS_UNI].fd == -1) - // do not send message to disconnected server - break; - - // null terminate - Input[InputIndex] = 0; - InputIndex++; - - // Save header - HeaderMessage* header = ArenaPush(&MessagesArena, sizeof(*header)); - header->version = PROTOCOL_VERSION; - header->type = HEADER_TYPE_TEXT; - header->id = user.ID; - - // Save message - TextMessage* sendmsg = ArenaPush(&MessagesArena, TEXTMESSAGE_SIZE); - sendmsg->timestamp = time(0); - sendmsg->len = InputIndex; - - u32 text_size = InputIndex * sizeof(*Input); - ArenaPush(&MessagesArena, text_size); - memcpy(&sendmsg->text, Input, text_size); - - sendAnyMessage(fds[FDS_UNI].fd, *header, sendmsg); - - MessagesNum++; - // also clear input - } // fallthrough - case TB_KEY_CTRL_U: // clear input - bzero(Input, InputIndex * sizeof(*Input)); - InputIndex = 0; - break; - default: - if (ev.ch == 0) - break; - - // TODO: show error - if (InputIndex == MAX_INPUT_LEN - 1) // last byte reserved for \0 - break; - - // append key to input buffer - Input[InputIndex] = ev.ch; - InputIndex++; - } - if (quit) - break; - } - - // These are used to redraw the screen from threads - if (fds[FDS_RESIZE].revents & POLLIN) - { - // ignore - tb_poll_event(&ev); - } - - DisplayChat(&ScratchArena, &MessagesArena, MessagesNum, &ClientsArena, fds, Input, InputIndex); - - tb_present(); - } - - tb_shutdown(); - - if (quitmsg != 0) - printf("%s\n", quitmsg); - - return 0; -} |