#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 bool prompt_yn(struct readbuffer *rb, const char *text) { printf("%s [y/n] ", text); fflush(stdout); while (true) { struct string_view line = extract_line(rb); if (line.s == NULL) { read_more_data(STDIN_FILENO, rb); continue; } splice_scanned(rb); if (line.len <= 3) { char buf[4]; memcpy(buf, line.s, line.len); buf[line.len] = '\0'; if (strcmp(buf, "y") == 0 || strcmp(buf, "yes") == 0) { return true; } if (strcmp(buf, "n") == 0 || strcmp(buf, "no") == 0) { return false; } } printf("Please answer with 'y', 'n', 'yes' or 'no'. [y/n] "); fflush(stdout); } } 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; char *focus_room; }; 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 ) { if (line.len == 0) return false; if (line.s[0] != '/' || (line.len >= 2 && line.s[0] == '/' && line.s[1] == '/')) { if (state->focus_room != NULL) { char *message = NULL; sv_copy(line, &message); enum tomsg_retval ret = tomsg_send(client, state->focus_room, message, -1, NULL); free(message); if (ret != TOMSG_OK) return true; return false; } else { printf("Can't send directly, no room is /focus'ed\n"); return false; } } // drop the '/' sv_skip(&line, 1); 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, "leave") || sv_equals(command, "leave_room")) { if (parse_args(line, args, num_args = 1, false)) { autocomplete_roomname(state, &args[0]); ret = tomsg_leave_room(client, args[0]); } } 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], -1, NULL); } } else if (sv_equals(command, "r") || sv_equals(command, "reply")) { int64_t replyid; if (parse_args(line, args, num_args = 3, true) && parse_i64(args[1], &replyid)) { autocomplete_roomname(state, &args[0]); ret = tomsg_send(client, args[0], args[2], replyid, 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, "get") || sv_equals(command, "get_message")) { int64_t msgid; if (parse_args(line, args, num_args = 1, false) && parse_i64(args[0], &msgid)) { ret = tomsg_get_message(client, 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, "focus")) { if (parse_args(line, args, num_args = 1, false)) { autocomplete_roomname(state, &args[0]); if (state->focus_room != NULL) free(state->focus_room); state->focus_room = args[0]; args[0] = NULL; printf("Focused room set to %s.\n", state->focus_room); ret = TOMSG_OK; } } 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" " leave/leave_room \n" " invite \n" " s/send \n" " r/reply \n" " ping\n" " hist/history \n" " histb/history_before \n" " get/get_message \n" " on/is_online \n" " act/user_active \n" " quit/exit\n" " focus \n" " help\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_LEAVE_ROOM: return "leave_room"; case TOMSG_EV_INVITE: return "invite"; case TOMSG_EV_SEND: return "send"; case TOMSG_EV_HISTORY: return "history"; case TOMSG_EV_GET_MESSAGE: return "get_message"; 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"; case TOMSG_EV_PUSH_LEAVE: return "push_leave"; } return "?unknown type?"; } static void print_history_message(const struct history_message msg) { printf("%" PRIi64 " \x1B[90m%" PRIi64 "\x1B[0m <%s> ", msg.msgid, msg.timestamp, msg.username); if (msg.replyid != -1) { printf("\x1B[32m[%" PRIi64 "<-]\x1B[0m ", msg.replyid); } printf("%s\n", msg.message); } static void handle_event(struct state *state, const struct tomsg_event event) { printf("Event(%s):", 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: printf("\n"); 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_LEAVE_ROOM: printf(" Left room %s\n", event.leave_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_GET_MESSAGE: printf(" %" PRIi64 " is in room %s:\n ", event.get_message.message.msgid, event.get_message.room_name); print_history_message(event.get_message.message); break; case TOMSG_EV_PING: printf(" Pong\n"); break; case TOMSG_EV_IS_ONLINE: case TOMSG_EV_PUSH_ONLINE: { const char *once_strings[4] = {NULL, "once", "twice", "thrice"}; printf(" %s is ", event.is_online.username); if (event.type == TOMSG_EV_PUSH_ONLINE) printf("now "); const int64_t count = event.is_online.online_count; if (count <= 0) { printf("offline\n"); } else if (count <= 3) { printf("online %s\n", once_strings[count]); } else { printf("online %" PRIi64 " times\n", count); } break; } case TOMSG_EV_USER_ACTIVE: printf(" Marked %s\n", event.user_active.active ? "active" : "inactive"); break; case TOMSG_EV_PUSH_MESSAGE: printf("\a [%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; case TOMSG_EV_PUSH_LEAVE: printf(" %s has left room %s\n", event.push_leave.username, event.push_leave.room_name); break; } } static bool hostkey_checker(const unsigned char *hash, size_t length, struct readbuffer *stdinbuf) { const char *fingerprint = tomsg_print_hash(hash, length); printf("Server host key fingerprint: %s\n", fingerprint); return prompt_yn(stdinbuf, "Does this hash match the one given to you by the server administrator, or by a\n" "member that you trust and is already connected to the server?"); } static enum tomsg_retval connect_server( const char *hostname, int port, struct readbuffer *stdinbuf, struct tomsg_client **clientp ) { struct tomsg_async_connect *async; enum tomsg_retval ret = tomsg_async_connect(hostname, port, &async); if (ret != TOMSG_OK) return ret; const int fd = tomsg_async_connect_poll_fd(async); struct pollfd pfd; pfd.fd = fd; pfd.events = POLLIN; while (true) { pfd.revents = 0; int pollret = poll(&pfd, 1, -1); if (pollret < 0) { perror("poll"); // Can't really close the async connector? exit(1); } if (pfd.revents & (POLLIN | POLLHUP | POLLERR)) { struct tomsg_async_connect_event event; ret = tomsg_async_connect_next_event(async, &event); if (ret == TOMSG_ERR_AGAIN) continue; if (ret != TOMSG_OK) return ret; switch (event.type) { case TOMSG_AC_HOSTKEY: ret = tomsg_async_connect_accept(async, hostkey_checker(event.key.hostkey, event.key.length, stdinbuf)); tomsg_async_connect_event_nullify(event); if (ret != TOMSG_OK) return ret; break; case TOMSG_AC_SUCCESS: *clientp = event.client; return TOMSG_OK; } } } } 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; } struct readbuffer stdinbuf = readbuffer_new(); printf("Connecting...\n"); struct tomsg_client *client; enum tomsg_retval ret = connect_server(hostname, port, &stdinbuf, &client); if (ret != TOMSG_OK) { printf("Could not connect: %s\n", tomsg_strerror(ret)); return 1; } printf("Connected.\n"); struct state state = (struct state){ .rooms = NULL, .num_rooms = 0, .focus_room = NULL, }; 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)); goto error_cleanup; } } } } error_cleanup: tomsg_close(client); }