From ed5fc43cbdf8e0394b81b4ae775bf1104cccedc3 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Thu, 23 Apr 2026 12:03:27 +0800 Subject: [PATCH] feat: add :last, :search, :mute-joins commands and MOTD support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - :last [N]: show last N messages from log (max 50, default 10) - :search : case-insensitive full-text search across log history - :mute-joins: per-client toggle to silence join/leave system messages, indicated by [静音] in the title bar - MOTD: display motd.txt from state directory on connect before entering chat - Add message_search() to message.c/h for log file scanning - Update :help and tui help screen (EN/ZH) with new commands --- include/message.h | 4 +++ include/ssh_server.h | 1 + src/message.c | 72 +++++++++++++++++++++++++++++++++++++ src/ssh_server.c | 84 ++++++++++++++++++++++++++++++++++++++++++++ src/tui.c | 56 ++++++++++++++++++++--------- 5 files changed, 201 insertions(+), 16 deletions(-) diff --git a/include/message.h b/include/message.h index 44891b0..3da4800 100644 --- a/include/message.h +++ b/include/message.h @@ -22,4 +22,8 @@ int message_save(const message_t *msg); /* Format a message for display */ void message_format(const message_t *msg, char *buffer, size_t buf_size, int width); +/* Search log file for messages matching query (case-insensitive, username or content). + * Returns the last max_results matches in chronological order; caller must free *results. */ +int message_search(const char *query, message_t **results, int max_results); + #endif /* MESSAGE_H */ diff --git a/include/ssh_server.h b/include/ssh_server.h index fb999c3..bc06e9c 100644 --- a/include/ssh_server.h +++ b/include/ssh_server.h @@ -30,6 +30,7 @@ typedef struct client { time_t connect_time; time_t last_active; atomic_bool redraw_pending; + bool mute_joins; pthread_t thread; atomic_bool connected; int ref_count; /* Reference count for safe cleanup */ diff --git a/src/message.c b/src/message.c index ab8161d..123cbbb 100644 --- a/src/message.c +++ b/src/message.c @@ -244,6 +244,78 @@ int message_save(const message_t *msg) { return rc; } +/* Search log file for messages whose username or content contains query. + * Case-insensitive. Returns the last max_results matches (most recent); caller frees *results. */ +int message_search(const char *query, message_t **results, int max_results) { + char log_path[PATH_MAX]; + + message_t *res = calloc(max_results, sizeof(message_t)); + if (!res) return 0; + + if (!query || query[0] == '\0' || + tnt_state_path(log_path, sizeof(log_path), LOG_FILE) < 0) { + *results = res; + return 0; + } + + pthread_mutex_lock(&g_message_file_lock); + FILE *fp = fopen(log_path, "r"); + if (!fp) { + pthread_mutex_unlock(&g_message_file_lock); + *results = res; + return 0; + } + + char line[2048]; + int count = 0; + + while (fgets(line, sizeof(line), fp)) { + size_t line_len = strlen(line); + if (line_len >= sizeof(line) - 1) { + int c; + while ((c = fgetc(fp)) != '\n' && c != EOF); + continue; + } + + char line_copy[2048]; + strncpy(line_copy, line, sizeof(line_copy) - 1); + line_copy[sizeof(line_copy) - 1] = '\0'; + + char *timestamp_str = strtok(line_copy, "|"); + char *username = strtok(NULL, "|"); + char *content = strtok(NULL, "\n"); + + if (!timestamp_str || !username || !content || username[0] == '\0') continue; + if (strlen(username) >= MAX_USERNAME_LEN || strlen(content) >= MAX_MESSAGE_LEN) continue; + if (!utf8_is_valid_string(username) || !utf8_is_valid_string(content)) continue; + + if (strcasestr(username, query) == NULL && strcasestr(content, query) == NULL) continue; + + time_t msg_time = parse_rfc3339_utc(timestamp_str); + if (msg_time == (time_t)-1) continue; + + message_t m; + m.timestamp = msg_time; + strncpy(m.username, username, MAX_USERNAME_LEN - 1); + m.username[MAX_USERNAME_LEN - 1] = '\0'; + strncpy(m.content, content, MAX_MESSAGE_LEN - 1); + m.content[MAX_MESSAGE_LEN - 1] = '\0'; + + if (count < max_results) { + res[count++] = m; + } else { + memmove(&res[0], &res[1], (max_results - 1) * sizeof(message_t)); + res[max_results - 1] = m; + /* count stays at max_results */ + } + } + + fclose(fp); + pthread_mutex_unlock(&g_message_file_lock); + *results = res; + return (count < max_results) ? count : max_results; +} + /* Format a message for display */ void message_format(const message_t *msg, char *buffer, size_t buf_size, int width) { struct tm tm_info; diff --git a/src/ssh_server.c b/src/ssh_server.c index 488dfb3..7ce775d 100644 --- a/src/ssh_server.c +++ b/src/ssh_server.c @@ -1198,6 +1198,9 @@ static void execute_command(client_t *client) { "list, users, who - Show online users\n" "nick/name - Change nickname\n" "msg/w - Whisper to user\n" + "last [N] - Show last N messages\n" + "search - Search message history\n" + "mute-joins - Toggle join/leave notices\n" "help, commands - Show this help\n" "clear, cls - Clear command output\n" "q, quit, exit - Disconnect\n" @@ -1289,6 +1292,63 @@ static void execute_command(client_t *client) { "Nickname changed: %s -> %s\n", old_name, client->username); } + } else if (strncmp(cmd, "last", 4) == 0 && (cmd[4] == ' ' || cmd[4] == '\0')) { + char *arg = cmd + 4; + while (*arg == ' ') arg++; + int n = 10; + if (*arg != '\0') { + char *endp; + long val = strtol(arg, &endp, 10); + if (*endp != '\0' || val < 1 || val > 50) { + buffer_appendf(output, sizeof(output), &pos, + "Usage: last [N] (N: 1-50, default 10)\n"); + goto cmd_done; + } + n = (int)val; + } + + message_t *last_msgs = NULL; + int last_count = message_load(&last_msgs, n); + buffer_appendf(output, sizeof(output), &pos, + "--- Last %d message(s) ---\n", last_count); + for (int i = 0; i < last_count; i++) { + char ts[20]; + struct tm tmi; + localtime_r(&last_msgs[i].timestamp, &tmi); + strftime(ts, sizeof(ts), "%m-%d %H:%M", &tmi); + buffer_appendf(output, sizeof(output), &pos, + "[%s] %s: %s\n", ts, last_msgs[i].username, last_msgs[i].content); + } + free(last_msgs); + + } else if (strncmp(cmd, "search ", 7) == 0) { + char *query = cmd + 7; + while (*query == ' ') query++; + if (*query == '\0') { + buffer_appendf(output, sizeof(output), &pos, + "Usage: search \n"); + } else { + message_t *found = NULL; + int found_count = message_search(query, &found, 15); + buffer_appendf(output, sizeof(output), &pos, + "--- Search: \"%s\" (%d match(es)) ---\n", query, found_count); + for (int i = 0; i < found_count; i++) { + char ts[20]; + struct tm tmi; + localtime_r(&found[i].timestamp, &tmi); + strftime(ts, sizeof(ts), "%m-%d %H:%M", &tmi); + buffer_appendf(output, sizeof(output), &pos, + "[%s] %s: %s\n", ts, found[i].username, found[i].content); + } + free(found); + } + + } else if (strcmp(cmd, "mute-joins") == 0 || strcmp(cmd, "mute") == 0) { + client->mute_joins = !client->mute_joins; + buffer_appendf(output, sizeof(output), &pos, + "Join/leave notifications: %s\n", + client->mute_joins ? "muted" : "unmuted"); + } else if (strcmp(cmd, "q") == 0 || strcmp(cmd, "quit") == 0 || strcmp(cmd, "exit") == 0) { client->connected = false; @@ -1310,6 +1370,7 @@ static void execute_command(client_t *client) { "Type 'help' for available commands\n", cmd); } +cmd_done: buffer_appendf(output, sizeof(output), &pos, "\nPress any key to continue..."); @@ -1607,10 +1668,33 @@ void* client_handle_session(void *arg) { room_broadcast(g_room, &join_msg); message_save(&join_msg); + /* Show MOTD if motd.txt exists in state directory */ + { + char motd_path[PATH_MAX]; + if (tnt_state_path(motd_path, sizeof(motd_path), "motd.txt") == 0) { + FILE *motd_fp = fopen(motd_path, "r"); + if (motd_fp) { + char motd_buf[sizeof(client->command_output) - 64]; + size_t motd_len = fread(motd_buf, 1, sizeof(motd_buf) - 1, motd_fp); + fclose(motd_fp); + if (motd_len > 0) { + motd_buf[motd_len] = '\0'; + snprintf(client->command_output, sizeof(client->command_output), + "=== 公告 / MOTD ===\n%s", motd_buf); + tui_render_command_output(client); + seen_update_seq = room_get_update_seq(g_room); + goto main_loop; + } + } + } + } + /* Render initial screen */ tui_render_screen(client); seen_update_seq = room_get_update_seq(g_room); +main_loop: + /* Main input loop */ while (client->connected && ssh_channel_is_open(client->channel)) { int ready = ssh_channel_poll_timeout(client->channel, 1000, 0); diff --git a/src/tui.c b/src/tui.c index af6ff60..38a0eaa 100644 --- a/src/tui.c +++ b/src/tui.c @@ -5,6 +5,12 @@ #include #include +static bool is_join_leave_msg(const message_t *msg) { + if (strcmp(msg->username, "系统") != 0) return false; + return strstr(msg->content, "加入了聊天室") != NULL || + strstr(msg->content, "离开了聊天室") != NULL; +} + static const char *username_color(const char *name) { static const char *colors[] = { "\033[31m", "\033[32m", "\033[33m", @@ -222,6 +228,17 @@ void tui_render_screen(client_t *client) { /* Now render using snapshot (no lock held) */ + /* If mute_joins is set, remove join/leave messages from snapshot in place */ + if (client->mute_joins && msg_snapshot) { + int filtered = 0; + for (int i = 0; i < snapshot_count; i++) { + if (!is_join_leave_msg(&msg_snapshot[i])) { + msg_snapshot[filtered++] = msg_snapshot[i]; + } + } + snapshot_count = filtered; + } + /* Move to top (Home) - Do NOT clear screen to prevent flicker */ buffer_appendf(buffer, buf_size, &pos, ANSI_HOME); @@ -232,8 +249,9 @@ void tui_render_screen(client_t *client) { char title[256]; snprintf(title, sizeof(title), - " %s | 在线: %d | 模式: %s | ? 帮助 ", - client->username, online, mode_str); + " %s | 在线: %d | 模式: %s%s | ? 帮助 ", + client->username, online, mode_str, + client->mute_joins ? " [静音]" : ""); int title_width = utf8_string_width(title); int padding = render_width - title_width; @@ -415,13 +433,16 @@ const char* tui_get_help_text(help_lang_t lang) { " Ctrl+C - Exit chat\n" "\n" "AVAILABLE COMMANDS:\n" - " :list, :users - Show online users\n" - " :nick - Change nickname\n" - " :msg - Whisper to user\n" - " :w - Short alias for :msg\n" - " :help - Show available commands\n" - " :clear - Clear command output\n" - " :q, :quit, :exit - Disconnect\n" + " :list, :users - Show online users\n" + " :nick - Change nickname\n" + " :msg - Whisper to user\n" + " :w - Short alias for :msg\n" + " :last [N] - Show last N messages (max 50)\n" + " :search - Search message history\n" + " :mute-joins - Toggle join/leave notices\n" + " :help - Show available commands\n" + " :clear - Clear command output\n" + " :q, :quit, :exit - Disconnect\n" "\n" "SPECIAL MESSAGES:\n" " /me - Send action (e.g. /me waves)\n" @@ -459,13 +480,16 @@ const char* tui_get_help_text(help_lang_t lang) { " Ctrl+C - 退出聊天\n" "\n" "可用命令:\n" - " :list, :users - 显示在线用户\n" - " :nick <名字> - 更改昵称\n" - " :msg <用户> <文本> - 私聊\n" - " :w <用户> <文本> - :msg 的简写\n" - " :help - 显示可用命令\n" - " :clear - 清空命令输出\n" - " :q, :quit, :exit - 断开连接\n" + " :list, :users - 显示在线用户\n" + " :nick <名字> - 更改昵称\n" + " :msg <用户> <文本> - 私聊\n" + " :w <用户> <文本> - :msg 的简写\n" + " :last [N] - 显示最后 N 条消息(最多50)\n" + " :search <关键词> - 搜索消息历史\n" + " :mute-joins - 切换加入/离开提示\n" + " :help - 显示可用命令\n" + " :clear - 清空命令输出\n" + " :q, :quit, :exit - 断开连接\n" "\n" "特殊消息:\n" " /me <动作> - 发送动作 (如 /me 挥手)\n"