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 */
|
/* Format a message for display */
|
||||||
void message_format(const message_t *msg, char *buffer, size_t buf_size, int width);
|
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 */
|
#endif /* MESSAGE_H */
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ typedef struct client {
|
||||||
time_t connect_time;
|
time_t connect_time;
|
||||||
time_t last_active;
|
time_t last_active;
|
||||||
atomic_bool redraw_pending;
|
atomic_bool redraw_pending;
|
||||||
|
bool mute_joins;
|
||||||
pthread_t thread;
|
pthread_t thread;
|
||||||
atomic_bool connected;
|
atomic_bool connected;
|
||||||
int ref_count; /* Reference count for safe cleanup */
|
int ref_count; /* Reference count for safe cleanup */
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,78 @@ int message_save(const message_t *msg) {
|
||||||
return rc;
|
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 */
|
/* Format a message for display */
|
||||||
void message_format(const message_t *msg, char *buffer, size_t buf_size, int width) {
|
void message_format(const message_t *msg, char *buffer, size_t buf_size, int width) {
|
||||||
struct tm tm_info;
|
struct tm tm_info;
|
||||||
|
|
|
||||||
|
|
@ -1198,6 +1198,9 @@ static void execute_command(client_t *client) {
|
||||||
"list, users, who - Show online users\n"
|
"list, users, who - Show online users\n"
|
||||||
"nick/name <name> - Change nickname\n"
|
"nick/name <name> - Change nickname\n"
|
||||||
"msg/w <user> <text> - Whisper to user\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"
|
"help, commands - Show this help\n"
|
||||||
"clear, cls - Clear command output\n"
|
"clear, cls - Clear command output\n"
|
||||||
"q, quit, exit - Disconnect\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);
|
"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 ||
|
} else if (strcmp(cmd, "q") == 0 || strcmp(cmd, "quit") == 0 ||
|
||||||
strcmp(cmd, "exit") == 0) {
|
strcmp(cmd, "exit") == 0) {
|
||||||
client->connected = false;
|
client->connected = false;
|
||||||
|
|
@ -1310,6 +1370,7 @@ static void execute_command(client_t *client) {
|
||||||
"Type 'help' for available commands\n", cmd);
|
"Type 'help' for available commands\n", cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd_done:
|
||||||
buffer_appendf(output, sizeof(output), &pos,
|
buffer_appendf(output, sizeof(output), &pos,
|
||||||
"\nPress any key to continue...");
|
"\nPress any key to continue...");
|
||||||
|
|
||||||
|
|
@ -1607,10 +1668,33 @@ void* client_handle_session(void *arg) {
|
||||||
room_broadcast(g_room, &join_msg);
|
room_broadcast(g_room, &join_msg);
|
||||||
message_save(&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 */
|
/* Render initial screen */
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
seen_update_seq = room_get_update_seq(g_room);
|
seen_update_seq = room_get_update_seq(g_room);
|
||||||
|
|
||||||
|
main_loop:
|
||||||
|
|
||||||
/* Main input loop */
|
/* Main input loop */
|
||||||
while (client->connected && ssh_channel_is_open(client->channel)) {
|
while (client->connected && ssh_channel_is_open(client->channel)) {
|
||||||
int ready = ssh_channel_poll_timeout(client->channel, 1000, 0);
|
int ready = ssh_channel_poll_timeout(client->channel, 1000, 0);
|
||||||
|
|
|
||||||
56
src/tui.c
56
src/tui.c
|
|
@ -5,6 +5,12 @@
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <unistd.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 *username_color(const char *name) {
|
||||||
static const char *colors[] = {
|
static const char *colors[] = {
|
||||||
"\033[31m", "\033[32m", "\033[33m",
|
"\033[31m", "\033[32m", "\033[33m",
|
||||||
|
|
@ -222,6 +228,17 @@ void tui_render_screen(client_t *client) {
|
||||||
|
|
||||||
/* Now render using snapshot (no lock held) */
|
/* 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 */
|
/* Move to top (Home) - Do NOT clear screen to prevent flicker */
|
||||||
buffer_appendf(buffer, buf_size, &pos, ANSI_HOME);
|
buffer_appendf(buffer, buf_size, &pos, ANSI_HOME);
|
||||||
|
|
||||||
|
|
@ -232,8 +249,9 @@ void tui_render_screen(client_t *client) {
|
||||||
|
|
||||||
char title[256];
|
char title[256];
|
||||||
snprintf(title, sizeof(title),
|
snprintf(title, sizeof(title),
|
||||||
" %s | 在线: %d | 模式: %s | ? 帮助 ",
|
" %s | 在线: %d | 模式: %s%s | ? 帮助 ",
|
||||||
client->username, online, mode_str);
|
client->username, online, mode_str,
|
||||||
|
client->mute_joins ? " [静音]" : "");
|
||||||
|
|
||||||
int title_width = utf8_string_width(title);
|
int title_width = utf8_string_width(title);
|
||||||
int padding = render_width - title_width;
|
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"
|
" Ctrl+C - Exit chat\n"
|
||||||
"\n"
|
"\n"
|
||||||
"AVAILABLE COMMANDS:\n"
|
"AVAILABLE COMMANDS:\n"
|
||||||
" :list, :users - Show online users\n"
|
" :list, :users - Show online users\n"
|
||||||
" :nick <name> - Change nickname\n"
|
" :nick <name> - Change nickname\n"
|
||||||
" :msg <user> <text> - Whisper to user\n"
|
" :msg <user> <text> - Whisper to user\n"
|
||||||
" :w <user> <text> - Short alias for :msg\n"
|
" :w <user> <text> - Short alias for :msg\n"
|
||||||
" :help - Show available commands\n"
|
" :last [N] - Show last N messages (max 50)\n"
|
||||||
" :clear - Clear command output\n"
|
" :search <keyword> - Search message history\n"
|
||||||
" :q, :quit, :exit - Disconnect\n"
|
" :mute-joins - Toggle join/leave notices\n"
|
||||||
|
" :help - Show available commands\n"
|
||||||
|
" :clear - Clear command output\n"
|
||||||
|
" :q, :quit, :exit - Disconnect\n"
|
||||||
"\n"
|
"\n"
|
||||||
"SPECIAL MESSAGES:\n"
|
"SPECIAL MESSAGES:\n"
|
||||||
" /me <action> - Send action (e.g. /me waves)\n"
|
" /me <action> - Send action (e.g. /me waves)\n"
|
||||||
|
|
@ -459,13 +480,16 @@ const char* tui_get_help_text(help_lang_t lang) {
|
||||||
" Ctrl+C - 退出聊天\n"
|
" Ctrl+C - 退出聊天\n"
|
||||||
"\n"
|
"\n"
|
||||||
"可用命令:\n"
|
"可用命令:\n"
|
||||||
" :list, :users - 显示在线用户\n"
|
" :list, :users - 显示在线用户\n"
|
||||||
" :nick <名字> - 更改昵称\n"
|
" :nick <名字> - 更改昵称\n"
|
||||||
" :msg <用户> <文本> - 私聊\n"
|
" :msg <用户> <文本> - 私聊\n"
|
||||||
" :w <用户> <文本> - :msg 的简写\n"
|
" :w <用户> <文本> - :msg 的简写\n"
|
||||||
" :help - 显示可用命令\n"
|
" :last [N] - 显示最后 N 条消息(最多50)\n"
|
||||||
" :clear - 清空命令输出\n"
|
" :search <关键词> - 搜索消息历史\n"
|
||||||
" :q, :quit, :exit - 断开连接\n"
|
" :mute-joins - 切换加入/离开提示\n"
|
||||||
|
" :help - 显示可用命令\n"
|
||||||
|
" :clear - 清空命令输出\n"
|
||||||
|
" :q, :quit, :exit - 断开连接\n"
|
||||||
"\n"
|
"\n"
|
||||||
"特殊消息:\n"
|
"特殊消息:\n"
|
||||||
" /me <动作> - 发送动作 (如 /me 挥手)\n"
|
" /me <动作> - 发送动作 (如 /me 挥手)\n"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue