#include #include #include #include #include #include #include #include #include #include "tomsg_clientlib.h" #include "string_view.h" static bool parse_address(const char *arg, char **hostp, int *portp) { const char *const p = strchr(arg, ':'); if (p == NULL) return false; const size_t hostlen = p - arg; char *endp; *portp = strtol(p + 1, &endp, 10); if (!p[1] || *endp) return false; *hostp = malloc(hostlen + 1); memcpy(*hostp, arg, hostlen); (*hostp)[hostlen] = '\0'; return true; } struct readbuffer { char *buffer; size_t len, cap; size_t newline_cursor; }; static struct readbuffer readbuffer_new(void) { return (struct readbuffer){ .buffer = malloc(1024), .len = 0, .cap = 1024, .newline_cursor = 0, }; } // Returns whether fd was closed static bool read_more_data(int fd, struct readbuffer *rb) { if (rb->cap - rb->len < 256) { rb->cap *= 2; rb->buffer = realloc(rb->buffer, rb->cap); } const ssize_t nr = read(fd, rb->buffer + rb->len, rb->cap - rb->len); if (nr < 0) { if (errno == EINTR) return false; perror("read"); return true; } if (nr == 0) return true; rb->len += nr; return false; } static struct string_view extract_line(struct readbuffer *rb) { for (size_t i = rb->newline_cursor; i < rb->len; i++) { if (rb->buffer[i] == '\n') { const struct string_view sv = string_view(rb->buffer, i); rb->newline_cursor = i; return sv; } } rb->newline_cursor = rb->len; return string_view(NULL, 0); } static void splice_scanned(struct readbuffer *rb) { size_t start = rb->newline_cursor + 1; if (start > rb->len) start = rb->len; const size_t restlen = rb->len - start; memmove(rb->buffer, rb->buffer + start, restlen); rb->len = restlen; rb->newline_cursor = 0; } static struct string_view tokenise_greedy(struct string_view *line) { sv_skip_whitespace(line); return sv_tokenise_word(line); } static bool parse_args( struct string_view line, char **args, int num, bool longlast) { memset(args, 0, num * sizeof(char*)); for (int i = 0; i < num - longlast; i++) { struct string_view word = tokenise_greedy(&line); if (!word.s) { printf("Not enough arguments given (%d expected)\n", num); for (int j = 0; j < i; j++) free(args[j]); memset(args, 0, num * sizeof(char*)); return false; } sv_copy(word, &args[i]); } if (longlast) sv_copy(line, &args[num - 1]); else if (!sv_is_empty(line)) { printf("Too many arguments given (%d expected)\n", num); for (int i = 0; i < num; i++) free(args[i]); memset(args, 0, num * sizeof(char*)); return false; } return true; } struct state { char **rooms; size_t num_rooms; }; static void autocomplete_roomname(const struct state *state, char **namep) { const char *const name = *namep; const size_t namelen = strlen(name); if (strlen(name) == 0) return; size_t found_index = (size_t)-1; for (size_t i = 0; i < state->num_rooms; i++) { const size_t roomnamelen = strlen(state->rooms[i]); for (size_t j = 0; j < roomnamelen && j < namelen; j++) { if (tolower(state->rooms[i][j]) != tolower(name[j])) goto nope_next; } if (found_index == (size_t)-1) found_index = i; else return; // ambiguous completion nope_next: ; } if (found_index == (size_t)-1) return; // no completion found // apply completion free(*namep); *namep = strdup(state->rooms[found_index]); } static bool parse_i64(const char *str, int64_t *output) { const size_t len = strlen(str); if (len > 20) return false; // strlen(itoa(INT64_MIN)) == 20 char buf[21]; memcpy(buf, str, len); buf[len] = '\0'; char *endp; *output = strtoll(buf, &endp, 10); return endp == buf + len; } static bool handle_line( struct state *state, struct tomsg_client *client, struct string_view line ) { const struct string_view command = tokenise_greedy(&line); char *args[10]; int num_args = 0; enum tomsg_retval ret = TOMSG_OK; bool quit_requested = false; if (sv_equals(command, "register")) { if (parse_args(line, args, num_args = 2, false)) { ret = tomsg_register(client, args[0], args[1]); } } else if (sv_equals(command, "login")) { if (parse_args(line, args, num_args = 2, false)) { ret = tomsg_login(client, args[0], args[1]); } } else if (sv_equals(command, "logout")) { ret = tomsg_logout(client); } else if (sv_equals(command, "ls") || sv_equals(command, "list_rooms")) { ret = tomsg_list_rooms(client); } else if (sv_equals(command, "lsm") || sv_equals(command, "list_members")) { if (parse_args(line, args, num_args = 1, false)) { autocomplete_roomname(state, &args[0]); ret = tomsg_list_members(client, args[0]); } } else if (sv_equals(command, "create_room")) { ret = tomsg_create_room(client); } else if (sv_equals(command, "invite")) { if (parse_args(line, args, num_args = 2, false)) { autocomplete_roomname(state, &args[0]); ret = tomsg_invite(client, args[0], args[1]); } } else if (sv_equals(command, "s") || sv_equals(command, "send")) { if (parse_args(line, args, num_args = 2, true)) { autocomplete_roomname(state, &args[0]); ret = tomsg_send(client, args[0], args[1], NULL); } } else if (sv_equals(command, "ping")) { ret = tomsg_ping(client); } else if (sv_equals(command, "hist") || sv_equals(command, "history")) { int64_t count; if (parse_args(line, args, num_args = 2, false) && parse_i64(args[1], &count)) { autocomplete_roomname(state, &args[0]); ret = tomsg_history(client, args[0], count, -1); } } else if (sv_equals(command, "histb") || sv_equals(command, "history_before")) { int64_t count, msgid; if (parse_args(line, args, num_args = 3, false) && parse_i64(args[1], &count) && parse_i64(args[2], &msgid)) { autocomplete_roomname(state, &args[0]); ret = tomsg_history(client, args[0], count, msgid); } } else if (sv_equals(command, "on") || sv_equals(command, "is_online")) { if (parse_args(line, args, num_args = 1, false)) { ret = tomsg_is_online(client, args[0]); } } else if (sv_equals(command, "act") || sv_equals(command, "user_active")) { if (parse_args(line, args, num_args = 1, false)) { const bool active = strchr("1yY", args[0][0]); ret = tomsg_user_active(client, active); } } else if (sv_equals(command, "quit") || sv_equals(command, "exit")) { quit_requested = true; } else if (sv_equals(command, "help")) { printf( "Commands:\n" " register \n" " login \n" " logout\n" " ls/list_rooms\n" " lsm/list_members \n" " create_room\n" " invite \n" " s/send \n" " ping\n" " hist/history \n" " histb/history_before \n" " on/is_online \n" " act/user_active \n" " help\n" " quit/exit\n" "Room names are autocompleted.\n" ); } else { char *str; sv_copy(command, &str); printf("Error: unknown command '%s'\n", str); free(str); } for (int i = 0; i < num_args; i++) free(args[i]); if (ret != TOMSG_OK) { printf("Error: %s\n", tomsg_strerror(ret)); return true; } else { return quit_requested; } } static const char* event_type_descr(enum tomsg_event_type type) { switch (type) { case TOMSG_EV_REGISTER: return "register"; case TOMSG_EV_LOGIN: return "login"; case TOMSG_EV_LOGOUT: return "logout"; case TOMSG_EV_LIST_ROOMS: return "list_rooms"; case TOMSG_EV_LIST_MEMBERS: return "list_members"; case TOMSG_EV_CREATE_ROOM: return "create_room"; case TOMSG_EV_INVITE: return "invite"; case TOMSG_EV_SEND: return "send"; case TOMSG_EV_HISTORY: return "history"; case TOMSG_EV_PING: return "ping"; case TOMSG_EV_IS_ONLINE: return "is_online"; case TOMSG_EV_USER_ACTIVE: return "user_active"; case TOMSG_EV_PUSH_ONLINE: return "push_online"; case TOMSG_EV_PUSH_MESSAGE: return "push_message"; case TOMSG_EV_PUSH_INVITE: return "push_invite"; case TOMSG_EV_PUSH_JOIN: return "push_join"; } return "?unknown type?"; } static void print_history_message(const struct history_message msg) { printf("%" PRIi64 " \x1B[90m%" PRIi64 "\x1B[0m <%s> %s\n", msg.msgid, msg.timestamp, msg.username, msg.message); } static void handle_event(struct state *state, const struct tomsg_event event) { printf("Event(%s):\n", event_type_descr(event.type)); if (event.error) { printf(" Error: %s\n", event.error); return; } switch (event.type) { case TOMSG_EV_REGISTER: printf(" Successfully registered as %s\n", event.login.username); break; case TOMSG_EV_LOGIN: printf(" Successfully logged in as %s\n", event.login.username); break; case TOMSG_EV_LOGOUT: printf(" Successfully logged out\n"); break; case TOMSG_EV_LIST_ROOMS: for (int64_t i = 0; i < event.list_rooms.count; i++) { printf(" - %s\n", event.list_rooms.rooms[i]); } for (size_t i = 0; i < state->num_rooms; i++) free(state->rooms[i]); if (event.list_rooms.count != (int64_t)state->num_rooms) { state->num_rooms = event.list_rooms.count; state->rooms = realloc( state->rooms, state->num_rooms * sizeof(char*)); } // Move the strings to state->rooms for (size_t i = 0; i < state->num_rooms; i++) { state->rooms[i] = event.list_rooms.rooms[i]; event.list_rooms.rooms[i] = NULL; } break; case TOMSG_EV_LIST_MEMBERS: printf(" In room %s:\n", event.list_members.room_name); for (int64_t i = 0; i < event.list_members.count; i++) { printf(" - %s\n", event.list_members.members[i]); } break; case TOMSG_EV_CREATE_ROOM: printf(" Created room %s\n", event.create_room.room_name); break; case TOMSG_EV_INVITE: printf(" Invited %s to %s\n", event.join.username, event.join.room_name); break; case TOMSG_EV_SEND: printf(" Sent message %" PRIi64 "\n", event.send.msgid); break; case TOMSG_EV_HISTORY: printf(" Some history for %s:\n", event.history.room_name); for (int64_t i = 0; i < event.history.count; i++) { printf(" "); print_history_message(event.history.messages[i]); } break; case TOMSG_EV_PING: printf(" Pong\n"); break; case TOMSG_EV_IS_ONLINE: case TOMSG_EV_PUSH_ONLINE: printf(" %s is ", event.is_online.username); if (event.type == TOMSG_EV_PUSH_ONLINE) printf("now "); if (event.is_online.online_count == 1) { printf("online once\n"); } else if (event.is_online.online_count > 1) { printf("online %" PRIi64 " times\n", event.is_online.online_count); } else { printf("offline\n"); } break; case TOMSG_EV_USER_ACTIVE: printf(" Marked %s\n", event.user_active.active ? "active" : "inactive"); break; case TOMSG_EV_PUSH_MESSAGE: printf("[%s] ", event.push_message.room_name); print_history_message(event.push_message.message); break; case TOMSG_EV_PUSH_INVITE: printf(" You've been invited to %s by %s\n", event.push_invite.room_name, event.push_invite.inviter); break; case TOMSG_EV_PUSH_JOIN: printf(" %s has joined %s\n", event.join.username, event.join.room_name); break; } } int main(int argc, char **argv) { if (argc != 2) { printf("Usage: %s \n", argv[0]); return 1; } char *hostname = NULL; int port; if (!parse_address(argv[1], &hostname, &port)) { printf("Cannot parse hostname:port from '%s'\n", argv[1]); return 1; } printf("Connecting...\n"); struct tomsg_client *client; enum tomsg_retval ret = tomsg_connect(hostname, port, &client); if (ret != TOMSG_OK) { printf("Could not connect: %s\n", tomsg_strerror(ret)); return 1; } printf("Connected.\n"); struct readbuffer stdinbuf = readbuffer_new(); struct state state = (struct state){ .rooms = NULL, .num_rooms = 0, }; struct pollfd pollfds[2]; pollfds[0].fd = STDIN_FILENO; pollfds[0].events = POLLIN; pollfds[1].fd = tomsg_poll_fd(client); pollfds[1].events = POLLIN; while (true) { int ret = poll(pollfds, 2, -1); if (ret < 0) { perror("poll"); tomsg_close(client); return 1; } if (ret == 0) continue; if (pollfds[0].revents & (POLLIN | POLLHUP | POLLERR)) { if (read_more_data(STDIN_FILENO, &stdinbuf)) break; const struct string_view sv = extract_line(&stdinbuf); if (sv.s != NULL) { if (handle_line(&state, client, sv)) break; splice_scanned(&stdinbuf); } } if (pollfds[1].revents & (POLLIN | POLLHUP | POLLERR)) { struct tomsg_event event; // Get all events currently available while (true) { ret = tomsg_next_event(client, &event); if (ret == TOMSG_ERR_AGAIN) break; if (ret == TOMSG_OK) { handle_event(&state, event); tomsg_event_nullify(event); } else { printf("Tomsg error: %s\n", tomsg_strerror(ret)); break; } } } } tomsg_close(client); }