#define _GNU_SOURCE #include #include #include #include #include #include #include #include "tomsg_clientlib.h" #include "sshnc.h" #include "string_view.h" struct inflight { int64_t tag; // Partially filled in, depending on the event type struct tomsg_event event; }; struct tomsg_client { struct sshnc_client *conn; // Receive buffer for the ssh connection size_t buffer_len, buffer_cap; char *buffer; size_t buffer_newline_cursor; // no newlines before this index int64_t next_tag; size_t inflight_num, inflight_cap; struct inflight *inflight; }; static size_t min_size_t(size_t a, size_t b) { return a < b ? a : b; } static bool hostkey_checker(const unsigned char *hash, size_t length, void *userdata) { (void)userdata; static const char *preload = "SHA256:ppz/McaESpOQy0O3kbaIi1LPZ37/YtrdC+y9102Y0+I"; const char *fingerprint = sshnc_print_hash(hash, length); return strcmp(fingerprint, preload) == 0; } static bool hasspacelf(const char *string) { for (size_t i = 0; string[i]; i++) if (string[i] == ' ' || string[i] == '\n') return true; return false; } static bool haslf(const char *string) { for (size_t i = 0; string[i]; i++) if (string[i] == '\n') return true; return false; } __attribute__((warn_unused_result)) static enum tomsg_retval add_inflight( struct tomsg_client *client, int64_t tag, struct tomsg_event event) { if (client->inflight_num == client->inflight_cap) { client->inflight_cap *= 2; struct inflight *new_inflight = realloc( client->inflight, client->inflight_cap * sizeof(struct inflight)); if (!new_inflight) { // Just don't send the message; hope the application can handle that tomsg_event_nullify(event); return TOMSG_ERR_MEMORY; } client->inflight = new_inflight; } client->inflight[client->inflight_num++] = (struct inflight){ .tag = tag, .event = event, }; return TOMSG_OK; } static enum tomsg_retval receive_more_data(struct tomsg_client *client) { if (client->buffer_cap - client->buffer_len < 256) { client->buffer_cap *= 2; char *new_buffer = realloc(client->buffer, client->buffer_cap); if (!new_buffer) { // Cannot safely continue; we also can't "just" clear the buffer // and start parsing again, since we may detect commands in the // middle of messages, which, even if not an RCE vector, would not // be good. Thus, throw everything away and close the connection // now, before stuff goes wrong. free(client->buffer); client->buffer = NULL; sshnc_close(client->conn); client->conn = NULL; return TOMSG_ERR_MEMORY; } client->buffer = new_buffer; } size_t recvlen = 0; const enum sshnc_retval ret = sshnc_maybe_recv( client->conn, client->buffer_cap - client->buffer_len, client->buffer + client->buffer_len, &recvlen ); if (ret == SSHNC_EOF) return TOMSG_ERR_CLOSED; if (ret == SSHNC_AGAIN) return TOMSG_ERR_AGAIN; if (ret != SSHNC_OK) return TOMSG_ERR_TRANSPORT; client->buffer_len += recvlen; return TOMSG_OK; } // Returns the first line in the buffer, if any. Updates buffer_newline_cursor // to point at the newline after that line, or at the end of the buffer // otherwise. static struct string_view extract_next_line(struct tomsg_client *client) { for (size_t i = client->buffer_newline_cursor; i < client->buffer_len; i++) { if (client->buffer[i] == '\n') { struct string_view sv = string_view(client->buffer, i); client->buffer_newline_cursor = i; return sv; } } client->buffer_newline_cursor = client->buffer_len; return string_view(NULL, 0); } // Assumes buffer_newline_cursor points at a newline; removes the part ending // at that newline from the buffer. static void splice_scanned_buffer_part(struct tomsg_client *client) { const size_t start = min_size_t(client->buffer_newline_cursor + 1, client->buffer_len); const size_t restlen = client->buffer_len - start; memmove(client->buffer, client->buffer + start, restlen); client->buffer_len = restlen; client->buffer_newline_cursor = 0; } const char* tomsg_strerror(enum tomsg_retval code) { switch (code) { case TOMSG_OK: return "Success"; case TOMSG_ERR_CONNECT: return "Server refused connection"; case TOMSG_ERR_CLOSED: return "Server connection unexpectedly closed"; case TOMSG_ERR_VERSION: return "Server protocol version incompatible"; case TOMSG_ERR_TRANSPORT: return "Error in the underlying SSH transport"; case TOMSG_ERR_SPACE: return "Argument contained space or LF, while it could not"; case TOMSG_ERR_AGAIN: return "No events for now, poll(2) and try again"; case TOMSG_ERR_PARSE: return "Could not parse line from server, line ignored"; case TOMSG_ERR_MEMORY: return "Error allocating memory"; } return "?unknown error?"; } static enum tomsg_retval version_negotiation(struct tomsg_client *client) { if (!client->conn) return TOMSG_ERR_CLOSED; const enum sshnc_retval retssh = sshnc_send(client->conn, "ver version 1\n", 14); if (retssh == SSHNC_EOF) return TOMSG_ERR_CLOSED; if (retssh != SSHNC_OK) return TOMSG_ERR_TRANSPORT; struct pollfd pfd; pfd.fd = sshnc_poll_fd(client->conn); pfd.events = POLLIN; while (true) { const int retpoll = poll(&pfd, 1, -1); if (retpoll < 0) return TOMSG_ERR_TRANSPORT; if (retpoll == 0) continue; const enum tomsg_retval retmsg = receive_more_data(client); if (retmsg == TOMSG_ERR_AGAIN) continue; if (retmsg != TOMSG_OK) return retmsg; const struct string_view line = extract_next_line(client); if (line.s != NULL) { if (sv_equals(line, "ver ok")) { splice_scanned_buffer_part(client); return TOMSG_OK; } else { sshnc_close(client->conn); client->conn = NULL; return TOMSG_ERR_VERSION; } } } } enum tomsg_retval tomsg_connect( const char *hostname, int port, struct tomsg_client **clientp) { // In case we throw an error along the way *clientp = NULL; struct sshnc_client *conn; enum sshnc_retval ret = sshnc_connect( hostname, port, "tomsg", "tomsg", hostkey_checker, NULL, &conn); if (ret == SSHNC_ERR_CONNECT) return TOMSG_ERR_CONNECT; if (ret != SSHNC_OK) return TOMSG_ERR_TRANSPORT; struct tomsg_client *client = malloc(sizeof(struct tomsg_client)); if (!client) return TOMSG_ERR_MEMORY; client->conn = conn; client->buffer_len = 0; client->buffer_cap = 1024; client->buffer = malloc(client->buffer_cap); if (!client->buffer) { free(client); return TOMSG_ERR_MEMORY; } client->buffer_newline_cursor = 0; client->next_tag = 1; client->inflight_num = 0; client->inflight_cap = 2; client->inflight = malloc(client->inflight_cap * sizeof(struct inflight)); if (!client->inflight) { free(client->buffer); free(client); return TOMSG_ERR_MEMORY; } *clientp = client; return version_negotiation(client); } void tomsg_close(struct tomsg_client *client) { if (client->conn) sshnc_close(client->conn); free(client->buffer); if (client->inflight) { for (size_t i = 0; i < client->inflight_num; i++) { tomsg_event_nullify(client->inflight[i].event); } } free(client->inflight); free(client); } int tomsg_poll_fd(const struct tomsg_client *client) { if (!client->conn) return -1; return sshnc_poll_fd(client->conn); } static void history_message_nullify(struct history_message message) { free(message.username); message.username = NULL; free(message.message); message.message = NULL; } void tomsg_event_nullify(struct tomsg_event event) { if (event.error) free(event.error); event.error = NULL; switch (event.type) { case TOMSG_EV_LOGOUT: case TOMSG_EV_PING: case TOMSG_EV_USER_ACTIVE: case TOMSG_EV_SEND: break; case TOMSG_EV_REGISTER: case TOMSG_EV_LOGIN: free(event.login.username); break; case TOMSG_EV_INVITE: case TOMSG_EV_PUSH_JOIN: free(event.join.room_name); free(event.join.username); break; case TOMSG_EV_IS_ONLINE: case TOMSG_EV_PUSH_ONLINE: free(event.is_online.username); break; case TOMSG_EV_CREATE_ROOM: free(event.create_room.room_name); break; case TOMSG_EV_LIST_ROOMS: if (event.list_rooms.rooms != NULL) { for (int64_t i = 0; i < event.list_rooms.count; i++) { free(event.list_rooms.rooms[i]); } } free(event.list_rooms.rooms); break; case TOMSG_EV_LIST_MEMBERS: if (event.list_members.members != NULL) { for (int64_t i = 0; i < event.list_members.count; i++) { free(event.list_members.members[i]); } } free(event.list_members.members); break; case TOMSG_EV_HISTORY: free(event.history.room_name); if (event.history.messages != NULL) { for (int64_t i = 0; i < event.history.count; i++) { history_message_nullify(event.history.messages[i]); } } free(event.history.messages); break; case TOMSG_EV_PUSH_MESSAGE: free(event.push_message.room_name); history_message_nullify(event.push_message.message); break; case TOMSG_EV_PUSH_INVITE: free(event.push_invite.room_name); free(event.push_invite.inviter); break; } } static bool find_extract_inflight(struct tomsg_client *client, int64_t tag, struct inflight *output) { for (size_t i = 0; i < client->inflight_num; i++) { if (client->inflight[i].tag == tag) { *output = client->inflight[i]; if (i < client->inflight_num - 1) { client->inflight[i] = client->inflight[client->inflight_num - 1]; } client->inflight_num--; return true; } } return false; } // Returns TOMSG_ERR_AGAIN in case the line generated no output event. static enum tomsg_retval handle_line( struct tomsg_client *client, struct string_view line, struct tomsg_event *eventp ) { const struct string_view tagstr = sv_tokenise_word(&line); const struct string_view command = sv_tokenise_word(&line); // These parse macro's directly write to eventp. #define PARSE_I64(memb_) \ do { if (!sv_parse_i64(sv_tokenise_word(&line), &eventp -> memb_)) return TOMSG_ERR_PARSE; } while (0) #define PARSE_WORD(memb_) \ do { if (!sv_copy(sv_tokenise_word(&line), &eventp -> memb_)) return TOMSG_ERR_PARSE; } while (0) #define PARSE_RESTSTRING(memb_) \ do { if (!sv_copy(line, &eventp -> memb_)) return TOMSG_ERR_PARSE; } while (0) #define EXPECT_FINISH() \ do { if (!sv_is_empty(line)) return TOMSG_ERR_PARSE; } while (0) if (sv_equals(tagstr, "_push")) { // Ordered by rought expected occurrence probability if (sv_equals(command, "ping")) { return TOMSG_ERR_AGAIN; } else if (sv_equals(command, "message")) { eventp->type = TOMSG_EV_PUSH_MESSAGE; PARSE_WORD(push_message.room_name); PARSE_WORD(push_message.message.username); PARSE_I64(push_message.message.timestamp); PARSE_I64(push_message.message.msgid); PARSE_RESTSTRING(push_message.message.message); return TOMSG_OK; } else if (sv_equals(command, "online")) { eventp->type = TOMSG_EV_PUSH_ONLINE; PARSE_I64(is_online.online_count); PARSE_WORD(is_online.username); EXPECT_FINISH(); return TOMSG_OK; } else if (sv_equals(command, "invite")) { eventp->type = TOMSG_EV_PUSH_INVITE; PARSE_WORD(push_invite.room_name); PARSE_WORD(push_invite.inviter); EXPECT_FINISH(); return TOMSG_OK; } else if (sv_equals(command, "join")) { eventp->type = TOMSG_EV_PUSH_JOIN; PARSE_WORD(join.room_name); PARSE_WORD(join.username); EXPECT_FINISH(); return TOMSG_OK; } else { // Unknown command, let's just ignore return TOMSG_ERR_AGAIN; } } // These parse functions are now not valid anymore, since we can't directly // write to eventp anymore, and additionally need to manage the event // structure we will have around in 'inflight'. #undef PARSE_I64 #undef PARSE_WORD #undef PARSE_RESTSTRING #undef EXPECT_FINISH int64_t tag; struct inflight inflight; if (!sv_parse_i64(tagstr, &tag) || !find_extract_inflight(client, tag, &inflight)) { // Unknown tag, let's just ignore return TOMSG_ERR_AGAIN; } // These macros correctly dispose of, or persist, the event structure before returning. #define CLEANUP_RETURN(ret_) do { tomsg_event_nullify(inflight.event); return (ret_); } while (0) #define SUCCESS_RETURN() do { *eventp = inflight.event; return TOMSG_OK; } while (0) if (sv_equals(command, "error")) { if (!sv_copy(line, &inflight.event.error)) CLEANUP_RETURN(TOMSG_ERR_PARSE); SUCCESS_RETURN(); } // Since the response is not 'error', at least we're not in an error case. // Thus, we can assert the other, successful response type in each of the // event arms below. inflight.event.error = NULL; switch (inflight.event.type) { case TOMSG_EV_REGISTER: case TOMSG_EV_LOGIN: case TOMSG_EV_LOGOUT: case TOMSG_EV_INVITE: if (!sv_equals(command, "ok")) CLEANUP_RETURN(TOMSG_ERR_PARSE); SUCCESS_RETURN(); case TOMSG_EV_LIST_ROOMS: case TOMSG_EV_LIST_MEMBERS: { if (!sv_equals(command, "list")) CLEANUP_RETURN(TOMSG_ERR_PARSE); int64_t count; char **words; if (!sv_parse_i64(sv_tokenise_word(&line), &count)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (count < 0 || count > (1 << 30)) CLEANUP_RETURN(TOMSG_ERR_PARSE); words = calloc(count, sizeof(char*)); if (!words) CLEANUP_RETURN(TOMSG_ERR_MEMORY); for (int64_t i = 0; i < count; i++) { if (!sv_copy(sv_tokenise_word(&line), &words[i])) { for (int64_t j = 0; j < i; j++) free(words[j]); CLEANUP_RETURN(TOMSG_ERR_PARSE); } } // There might be words after the last one in the list, but we'll // just ignore them for simplicity's sake. if (inflight.event.type == TOMSG_EV_LIST_ROOMS) { inflight.event.list_rooms.count = count; inflight.event.list_rooms.rooms = words; } else { inflight.event.list_members.count = count; inflight.event.list_members.members = words; } SUCCESS_RETURN(); } case TOMSG_EV_CREATE_ROOM: if (!sv_equals(command, "name")) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_copy(sv_tokenise_word(&line), &inflight.event.create_room.room_name)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_is_empty(line)) CLEANUP_RETURN(TOMSG_ERR_PARSE); SUCCESS_RETURN(); case TOMSG_EV_SEND: if (!sv_equals(command, "number")) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_parse_i64(sv_tokenise_word(&line), &inflight.event.send.msgid)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_is_empty(line)) CLEANUP_RETURN(TOMSG_ERR_PARSE); SUCCESS_RETURN(); case TOMSG_EV_HISTORY: if (sv_equals(command, "history")) { int64_t count; if (!sv_parse_i64(sv_tokenise_word(&line), &count)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (count < 0 || count > (1 << 30)) CLEANUP_RETURN(TOMSG_ERR_PARSE); inflight.event.history.count = count; // Use calloc() to ensure the embedded pointers are NULL, in case we free it early inflight.event.history.messages = calloc(count, sizeof(struct history_message)); if (!inflight.event.history.messages) CLEANUP_RETURN(TOMSG_ERR_MEMORY); // Re-add the tag for the history_message events enum tomsg_retval ret = add_inflight(client, tag, inflight.event); if (ret != TOMSG_OK) return ret; return TOMSG_ERR_AGAIN; // don't cleanup, we re-submitted the event! } else if (sv_equals(command, "history_message")) { int64_t index; if (!sv_parse_i64(sv_tokenise_word(&line), &index)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (index < 0 || index >= inflight.event.history.count) CLEANUP_RETURN(TOMSG_ERR_PARSE); // Note that early-exiting in case of parse errors is valid here // (and everywhere): the messages buffer has been allocated with // calloc(), so tomsg_event_nullify() will cleanup up precisely // everything that we haven't filled in yet. struct history_message *msg = &inflight.event.history.messages[index];; if (!sv_equals(sv_tokenise_word(&line), inflight.event.history.room_name)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_copy(sv_tokenise_word(&line), &msg->username)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_parse_i64(sv_tokenise_word(&line), &msg->timestamp)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_parse_i64(sv_tokenise_word(&line), &msg->msgid)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_copy(line, &msg->message)) CLEANUP_RETURN(TOMSG_ERR_PARSE); // If there are more history_message events coming, re-add the tag for them if (index == inflight.event.history.count - 1) { SUCCESS_RETURN(); // done with the whole history listing } else { enum tomsg_retval ret = add_inflight(client, tag, inflight.event); if (ret != TOMSG_OK) return ret; return TOMSG_ERR_AGAIN; // don't cleanup, we re-submitted the event! } } else { CLEANUP_RETURN(TOMSG_ERR_PARSE); } case TOMSG_EV_PING: if (!sv_equals(command, "pong")) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_is_empty(line)) CLEANUP_RETURN(TOMSG_ERR_PARSE); SUCCESS_RETURN(); case TOMSG_EV_IS_ONLINE: if (!sv_equals(command, "number")) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_parse_i64(sv_tokenise_word(&line), &inflight.event.is_online.online_count)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_is_empty(line)) CLEANUP_RETURN(TOMSG_ERR_PARSE); SUCCESS_RETURN(); case TOMSG_EV_USER_ACTIVE: if (!sv_equals(command, "ok")) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_is_empty(line)) CLEANUP_RETURN(TOMSG_ERR_PARSE); SUCCESS_RETURN(); case TOMSG_EV_PUSH_ONLINE: case TOMSG_EV_PUSH_MESSAGE: case TOMSG_EV_PUSH_INVITE: case TOMSG_EV_PUSH_JOIN: // What? Why did we put a push event in the inflight list? That's a bug. assert(false); } #undef CLEANUP_RETURN #undef SUCCESS_RETURN // Should not be reachable; either the switch above missed a 'return' // statement somewhere, which is a bug, or the event type was not in the // tomsg_event_type enum, which is also a bug. assert(false); } // If the buffer contains a full line, handles that line, stores the received // event in eventp, and removes the line from the buffer. Otherwise, returns // TOMSG_ERR_AGAIN. static enum tomsg_retval find_handle_next_line( struct tomsg_client *client, struct tomsg_event *eventp) { // Loop while a line was handled without producing an output event while (true) { struct string_view line = extract_next_line(client); if (line.s == NULL) return TOMSG_ERR_AGAIN; // out of data const enum tomsg_retval ret = handle_line(client, line, eventp); splice_scanned_buffer_part(client); if (ret == TOMSG_ERR_AGAIN) continue; // handled but no output return ret; // handled with output, or error thrown } } enum tomsg_retval tomsg_next_event(struct tomsg_client *client, struct tomsg_event *eventp) { if (!client->conn) return TOMSG_ERR_CLOSED; // If we already have a line waiting in the buffer, handle it now. enum tomsg_retval ret = find_handle_next_line(client, eventp); if (ret != TOMSG_ERR_AGAIN) return ret; // Otherwise, try to receive more data. ret = receive_more_data(client); if (ret != TOMSG_OK) return ret; // And then handle any line that might be completed at this point. return find_handle_next_line(client, eventp); } #define SEND_FMT0(client, tag, fmt) do { \ char buffer_[128]; \ int length_ = snprintf(buffer_, sizeof buffer_, "%" PRIi64 " " fmt "\n", (tag)); \ assert(length_ < (int)sizeof buffer_); \ enum sshnc_retval ret_ = sshnc_send((client)->conn, buffer_, length_); \ if (ret_ == SSHNC_EOF) return TOMSG_ERR_CLOSED; \ if (ret_ != SSHNC_OK) return TOMSG_ERR_TRANSPORT; \ } while (0) #define SEND_FMT(client, tag, fmt, ...) do { \ char *buffer_; \ int length_ = asprintf(&buffer_, "%" PRIi64 " " fmt "\n", (tag), __VA_ARGS__); \ if (length_ < 0) return TOMSG_ERR_MEMORY; \ enum sshnc_retval ret_ = sshnc_send((client)->conn, buffer_, length_); \ free(buffer_); \ if (ret_ == SSHNC_EOF) return TOMSG_ERR_CLOSED; \ if (ret_ != SSHNC_OK) return TOMSG_ERR_TRANSPORT; \ } while (0) enum tomsg_retval tomsg_register( struct tomsg_client *client, const char *username, const char *password) { if (!client->conn) return TOMSG_ERR_CLOSED; if (hasspacelf(username)) return TOMSG_ERR_SPACE; if (haslf(password)) return TOMSG_ERR_SPACE; const int64_t tag = client->next_tag++; SEND_FMT(client, tag, "register %s %s", username, password); const struct tomsg_event event = (struct tomsg_event){ .type = TOMSG_EV_REGISTER, .login.username = strdup(username), }; return add_inflight(client, tag, event); } enum tomsg_retval tomsg_login( struct tomsg_client *client, const char *username, const char *password) { if (!client->conn) return TOMSG_ERR_CLOSED; if (hasspacelf(username)) return TOMSG_ERR_SPACE; if (haslf(password)) return TOMSG_ERR_SPACE; const int64_t tag = client->next_tag++; SEND_FMT(client, tag, "login %s %s", username, password); const struct tomsg_event event = (struct tomsg_event){ .type = TOMSG_EV_LOGIN, .login.username = strdup(username), }; return add_inflight(client, tag, event); } enum tomsg_retval tomsg_logout(struct tomsg_client *client) { if (!client->conn) return TOMSG_ERR_CLOSED; const int64_t tag = client->next_tag++; SEND_FMT0(client, tag, "logout"); const struct tomsg_event event = (struct tomsg_event){ .type = TOMSG_EV_LOGOUT, }; return add_inflight(client, tag, event); } enum tomsg_retval tomsg_list_rooms(struct tomsg_client *client) { if (!client->conn) return TOMSG_ERR_CLOSED; const int64_t tag = client->next_tag++; SEND_FMT0(client, tag, "list_rooms"); const struct tomsg_event event = (struct tomsg_event){ .type = TOMSG_EV_LIST_ROOMS, .list_rooms.count = 0, .list_rooms.rooms = NULL, }; return add_inflight(client, tag, event); } enum tomsg_retval tomsg_list_members(struct tomsg_client *client, const char *room_name) { if (!client->conn) return TOMSG_ERR_CLOSED; if (hasspacelf(room_name)) return TOMSG_ERR_SPACE; const int64_t tag = client->next_tag++; SEND_FMT(client, tag, "list_members %s", room_name); const struct tomsg_event event = (struct tomsg_event){ .type = TOMSG_EV_LIST_MEMBERS, .list_members.room_name = strdup(room_name), .list_members.count = 0, .list_members.members = NULL, }; return add_inflight(client, tag, event); } enum tomsg_retval tomsg_create_room(struct tomsg_client *client) { if (!client->conn) return TOMSG_ERR_CLOSED; const int64_t tag = client->next_tag++; SEND_FMT0(client, tag, "create_room"); const struct tomsg_event event = (struct tomsg_event){ .type = TOMSG_EV_CREATE_ROOM, .create_room.room_name = NULL, }; return add_inflight(client, tag, event); } enum tomsg_retval tomsg_invite( struct tomsg_client *client, const char *room_name, const char *username) { if (!client->conn) return TOMSG_ERR_CLOSED; if (hasspacelf(room_name)) return TOMSG_ERR_SPACE; if (hasspacelf(username)) return TOMSG_ERR_SPACE; const int64_t tag = client->next_tag++; SEND_FMT(client, tag, "invite %s %s", room_name, username); const struct tomsg_event event = (struct tomsg_event){ .type = TOMSG_EV_INVITE, .join.room_name = strdup(room_name), .join.username = strdup(username), }; return add_inflight(client, tag, event); } enum tomsg_retval tomsg_send( struct tomsg_client *client, const char *room_name, const char *message, int64_t *tagp //output ) { if (!client->conn) return TOMSG_ERR_CLOSED; if (hasspacelf(room_name)) return TOMSG_ERR_SPACE; if (haslf(message)) return TOMSG_ERR_SPACE; const int64_t tag = client->next_tag++; SEND_FMT(client, tag, "send %s %s", room_name, message); const struct tomsg_event event = (struct tomsg_event){ .type = TOMSG_EV_SEND, .send.tag = tag, }; if (tagp) *tagp = tag; return add_inflight(client, tag, event); } enum tomsg_retval tomsg_history( struct tomsg_client *client, const char *room_name, int64_t count, int64_t before_msgid) { if (!client->conn) return TOMSG_ERR_CLOSED; if (hasspacelf(room_name)) return TOMSG_ERR_SPACE; if (count < 0) count = 0; const int64_t tag = client->next_tag++; const struct tomsg_event event = (struct tomsg_event){ .type = TOMSG_EV_HISTORY, .history.room_name = strdup(room_name), .history.count = 0, // filled in when the first response is received .history.messages = NULL, }; if (before_msgid < 0) { SEND_FMT(client, tag, "history %s %" PRIi64, room_name, count); } else { SEND_FMT(client, tag, "history_before %s %" PRIi64 " %" PRIi64, room_name, count, before_msgid); } return add_inflight(client, tag, event); } enum tomsg_retval tomsg_ping(struct tomsg_client *client) { if (!client->conn) return TOMSG_ERR_CLOSED; const int64_t tag = client->next_tag++; SEND_FMT0(client, tag, "ping"); const struct tomsg_event event = (struct tomsg_event){ .type = TOMSG_EV_PING, .send.tag = tag, }; return add_inflight(client, tag, event); } enum tomsg_retval tomsg_is_online(struct tomsg_client *client, const char *username) { if (!client->conn) return TOMSG_ERR_CLOSED; if (hasspacelf(username)) return TOMSG_ERR_SPACE; const int64_t tag = client->next_tag++; SEND_FMT(client, tag, "is_online %s", username); const struct tomsg_event event = (struct tomsg_event){ .type = TOMSG_EV_IS_ONLINE, .is_online.username = strdup(username), }; return add_inflight(client, tag, event); } enum tomsg_retval tomsg_user_active(struct tomsg_client *client, bool active) { if (!client->conn) return TOMSG_ERR_CLOSED; const int64_t tag = client->next_tag++; SEND_FMT(client, tag, "user_active %d", active); const struct tomsg_event event = (struct tomsg_event){ .type = TOMSG_EV_USER_ACTIVE, .user_active.active = active, }; return add_inflight(client, tag, event); }