diff --git a/README.md b/README.md index 356f4f8..44f0447 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ ESC - Return to NORMAL mode **Special messages (INSERT mode)** ``` /me - Send action (e.g. /me waves) +@username - Mention user (bell + highlight) ``` ### Security Configuration @@ -127,6 +128,9 @@ TNT_MAX_CONN_RATE_PER_IP=30 tnt # Disable connection-rate and auth-failure blocking (testing only) TNT_RATE_LIMIT=0 tnt + +# Idle timeout in seconds (default 1800 = 30min, 0 to disable) +TNT_IDLE_TIMEOUT=3600 tnt ``` **SSH logging:** diff --git a/include/common.h b/include/common.h index 627c306..36d8769 100644 --- a/include/common.h +++ b/include/common.h @@ -25,6 +25,7 @@ #define MAX_LOG_SIZE (10 * 1024 * 1024) /* 10 MiB */ #define HOST_KEY_FILE "host_key" #define TNT_DEFAULT_STATE_DIR "." +#define DEFAULT_IDLE_TIMEOUT 1800 /* 30 minutes */ /* ANSI color codes */ #define ANSI_RESET "\033[0m" diff --git a/include/ssh_server.h b/include/ssh_server.h index 8464d26..fb999c3 100644 --- a/include/ssh_server.h +++ b/include/ssh_server.h @@ -27,6 +27,8 @@ typedef struct client { char command_output[2048]; char exec_command[MAX_EXEC_COMMAND_LEN]; char ssh_login[MAX_USERNAME_LEN]; + time_t connect_time; + time_t last_active; atomic_bool redraw_pending; pthread_t thread; atomic_bool connected; diff --git a/src/main.c b/src/main.c index 34841d8..175ee42 100644 --- a/src/main.c +++ b/src/main.c @@ -65,6 +65,7 @@ int main(int argc, char **argv) { printf(" TNT_ACCESS_TOKEN Require this password for SSH auth\n"); printf(" TNT_MAX_CONNECTIONS Global connection limit (default: 64)\n"); printf(" TNT_RATE_LIMIT Set to 0 to disable rate limiting\n"); + printf(" TNT_IDLE_TIMEOUT Idle disconnect timeout in seconds (default: 1800)\n"); return 0; } else { fprintf(stderr, "Unknown option: %s\n", argv[i]); diff --git a/src/ssh_server.c b/src/ssh_server.c index 44699be..488dfb3 100644 --- a/src/ssh_server.c +++ b/src/ssh_server.c @@ -66,6 +66,7 @@ static int g_max_conn_per_ip = 5; static int g_max_conn_rate_per_ip = 10; static int g_rate_limit_enabled = 1; static char g_access_token[256] = ""; +static int g_idle_timeout = DEFAULT_IDLE_TIMEOUT; static void buffer_appendf(char *buffer, size_t buf_size, size_t *pos, const char *fmt, ...) { @@ -142,6 +143,8 @@ static void init_rate_limit_config(void) { g_max_conn_rate_per_ip = env_int("TNT_MAX_CONN_RATE_PER_IP", 10, 1, 1024); g_rate_limit_enabled = env_int("TNT_RATE_LIMIT", 1, 0, 1); + g_idle_timeout = env_int("TNT_IDLE_TIMEOUT", DEFAULT_IDLE_TIMEOUT, 0, 86400); + if ((env = getenv("TNT_ACCESS_TOKEN")) != NULL) { strncpy(g_access_token, env, sizeof(g_access_token) - 1); g_access_token[sizeof(g_access_token) - 1] = '\0'; @@ -979,6 +982,31 @@ static int exec_command_tail(client_t *client, const char *args) { return rc; } +static void notify_mentions(const char *content, const client_t *sender) { + pthread_rwlock_rdlock(&g_room->lock); + int count = g_room->client_count; + client_t *targets[MAX_CLIENTS]; + int target_count = 0; + + for (int i = 0; i < count; i++) { + client_t *c = g_room->clients[i]; + if (c == sender) continue; + char mention[MAX_USERNAME_LEN + 2]; + snprintf(mention, sizeof(mention), "@%s", c->username); + if (strstr(content, mention) != NULL) { + client_addref(c); + targets[target_count++] = c; + } + } + pthread_rwlock_unlock(&g_room->lock); + + for (int i = 0; i < target_count; i++) { + client_send(targets[i], "\a", 1); + targets[i]->redraw_pending = true; + client_release(targets[i]); + } +} + static int exec_command_post(client_t *client, const char *args) { char content[MAX_MESSAGE_LEN]; char username[MAX_USERNAME_LEN]; @@ -1022,6 +1050,7 @@ static int exec_command_post(client_t *client, const char *args) { } room_broadcast(g_room, &msg); + notify_mentions(msg.content, client); if (message_save(&msg) < 0) { client_printf(client, "post: failed to persist message\n"); return 1; @@ -1138,11 +1167,21 @@ static void execute_command(client_t *client) { "----------------------------------------\n", g_room->client_count); + time_t now = time(NULL); for (int i = 0; i < g_room->client_count; i++) { char marker = (g_room->clients[i] == client) ? '*' : ' '; + int dur = (int)(now - g_room->clients[i]->connect_time); + char dur_str[32]; + if (dur < 60) { + snprintf(dur_str, sizeof(dur_str), "%ds", dur); + } else if (dur < 3600) { + snprintf(dur_str, sizeof(dur_str), "%dm", dur / 60); + } else { + snprintf(dur_str, sizeof(dur_str), "%dh%dm", dur / 3600, (dur % 3600) / 60); + } buffer_appendf(output, sizeof(output), &pos, - "%c %d. %s\n", marker, i + 1, - g_room->clients[i]->username); + "%c %d. %s (%s)\n", marker, i + 1, + g_room->clients[i]->username, dur_str); } pthread_rwlock_unlock(&g_room->lock); @@ -1166,6 +1205,7 @@ static void execute_command(client_t *client) { "========================================\n" "In INSERT mode:\n" " /me - Send action message\n" + " @username - Mention (bell notify)\n" "========================================\n"); } else if (strncmp(cmd, "msg ", 4) == 0 || strncmp(cmd, "w ", 2) == 0) { @@ -1357,6 +1397,7 @@ static bool handle_key(client_t *client, unsigned char key, char *input) { snprintf(msg.content, sizeof(msg.content), "%s", input); } room_broadcast(g_room, &msg); + notify_mentions(msg.content, client); message_save(&msg); input[0] = '\0'; } @@ -1534,6 +1575,8 @@ void* client_handle_session(void *arg) { client->connected = true; client->command_history_count = 0; client->command_history_pos = 0; + client->connect_time = time(NULL); + client->last_active = time(NULL); /* Check for exec command */ if (client->exec_command[0] != '\0') { @@ -1610,6 +1653,13 @@ void* client_handle_session(void *arg) { } last_keepalive = time(NULL); } + + if (g_idle_timeout > 0 && joined_room && + time(NULL) - client->last_active >= g_idle_timeout) { + client_printf(client, "\r\n\033[33mDisconnected: idle timeout (%d min)\033[0m\r\n", + g_idle_timeout / 60); + break; + } continue; } @@ -1621,6 +1671,7 @@ void* client_handle_session(void *arg) { } last_keepalive = time(NULL); + client->last_active = last_keepalive; unsigned char b = buf[0]; diff --git a/src/tui.c b/src/tui.c index 5e9c808..af6ff60 100644 --- a/src/tui.c +++ b/src/tui.c @@ -17,12 +17,25 @@ static const char *username_color(const char *name) { } static void format_message_colored(const message_t *msg, char *buffer, - size_t buf_size, int width) { + size_t buf_size, int width, + const char *my_username) { struct tm tm_info; localtime_r(&msg->timestamp, &tm_info); char time_str[32]; strftime(time_str, sizeof(time_str), "%H:%M", &tm_info); + bool mentioned = false; + if (my_username && my_username[0] != '\0' && + strcmp(msg->username, "系统") != 0) { + char mention[MAX_USERNAME_LEN + 2]; + snprintf(mention, sizeof(mention), "@%s", my_username); + if (strstr(msg->content, mention) != NULL) { + mentioned = true; + } + } + const char *hl_start = mentioned ? "\033[1;33m" : ""; + const char *hl_end = mentioned ? "\033[0m" : ""; + if (strcmp(msg->username, "系统") == 0) { snprintf(buffer, buf_size, "\033[90m--> %s\033[0m", msg->content); @@ -32,9 +45,9 @@ static void format_message_colored(const message_t *msg, char *buffer, time_str, msg->content); } else { snprintf(buffer, buf_size, - "\033[90m%s\033[0m %s%s\033[0m: %s", + "\033[90m%s\033[0m %s%s\033[0m: %s%s%s", time_str, username_color(msg->username), - msg->username, msg->content); + msg->username, hl_start, msg->content, hl_end); } /* Plain-text version for width calculation */ @@ -85,9 +98,9 @@ static void format_message_colored(const message_t *msg, char *buffer, time_str, truncated_content); } else { snprintf(buffer, buf_size, - "\033[90m%s\033[0m %s%s\033[0m: %s", + "\033[90m%s\033[0m %s%s\033[0m: %s%s%s", time_str, username_color(msg->username), - msg->username, truncated_content); + msg->username, hl_start, truncated_content, hl_end); } } } @@ -236,7 +249,8 @@ void tui_render_screen(client_t *client) { if (msg_snapshot) { for (int i = 0; i < snapshot_count; i++) { char msg_line[2048]; - format_message_colored(&msg_snapshot[i], msg_line, sizeof(msg_line), render_width); + format_message_colored(&msg_snapshot[i], msg_line, sizeof(msg_line), + render_width, client->username); buffer_appendf(buffer, buf_size, &pos, "%s\033[K\r\n", msg_line); } free(msg_snapshot); @@ -411,6 +425,7 @@ const char* tui_get_help_text(help_lang_t lang) { "\n" "SPECIAL MESSAGES:\n" " /me - Send action (e.g. /me waves)\n" + " @username - Mention user (bell + highlight)\n" "\n" "HELP SCREEN KEYS:\n" " q, ESC - Close help\n" @@ -454,6 +469,7 @@ const char* tui_get_help_text(help_lang_t lang) { "\n" "特殊消息:\n" " /me <动作> - 发送动作 (如 /me 挥手)\n" + " @用户名 - 提及用户 (响铃+高亮)\n" "\n" "帮助界面按键:\n" " q, ESC - 关闭帮助\n" diff --git a/tnt.1 b/tnt.1 index b782b76..f4c53f2 100644 --- a/tnt.1 +++ b/tnt.1 @@ -85,6 +85,7 @@ Ctrl+W Delete last word Ctrl+U Clear input line Ctrl+C Switch to NORMAL /me \fIaction\fR Send action message (e.g. /me waves) +@\fIusername\fR Mention user (bell notification + highlight) .TE .SS NORMAL mode .TS @@ -152,6 +153,10 @@ Max new connections per IP per 60\-second window (default: 10). .B TNT_RATE_LIMIT Set to 0 to disable rate\-based blocking and auth\-failure IP blocking. Explicit capacity limits still apply (default: 1). +.TP +.B TNT_IDLE_TIMEOUT +Disconnect clients after this many seconds of inactivity. +Set to 0 to disable (default: 1800, i.e. 30 minutes). .SH FILES .TP .I messages.log