mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-05-10 19:00:57 +08:00
feat: add :last, :search, :mute-joins commands and MOTD support
- :last [N]: show last N messages from log (max 50, default 10) - :search <keyword>: 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
This commit is contained in:
parent
ff6df3ab16
commit
ed5fc43cbd
5 changed files with 201 additions and 16 deletions
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1198,6 +1198,9 @@ static void execute_command(client_t *client) {
|
|||
"list, users, who - Show online users\n"
|
||||
"nick/name <name> - Change nickname\n"
|
||||
"msg/w <user> <text> - Whisper to user\n"
|
||||
"last [N] - Show last N messages\n"
|
||||
"search <keyword> - 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 <keyword>\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);
|
||||
|
|
|
|||
28
src/tui.c
28
src/tui.c
|
|
@ -5,6 +5,12 @@
|
|||
#include <stdarg.h>
|
||||
#include <unistd.h>
|
||||
|
||||
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;
|
||||
|
|
@ -419,6 +437,9 @@ const char* tui_get_help_text(help_lang_t lang) {
|
|||
" :nick <name> - Change nickname\n"
|
||||
" :msg <user> <text> - Whisper to user\n"
|
||||
" :w <user> <text> - Short alias for :msg\n"
|
||||
" :last [N] - Show last N messages (max 50)\n"
|
||||
" :search <keyword> - 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"
|
||||
|
|
@ -463,6 +484,9 @@ const char* tui_get_help_text(help_lang_t lang) {
|
|||
" :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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue