mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-05-10 19:00:57 +08:00
feat: add @mention notifications, idle timeout, and online duration
- @mention: typing @username in a message sends bell char to that user and highlights the message content in bold yellow for them - Idle timeout: disconnect inactive clients after TNT_IDLE_TIMEOUT seconds (default 1800 = 30min, 0 to disable) - :list now shows connection duration per user (e.g. "alice (12m)") - Document all three features in help text, manpage, and README Closes #46
This commit is contained in:
parent
b0bb18d93e
commit
bb77c77b8f
7 changed files with 88 additions and 8 deletions
|
|
@ -92,6 +92,7 @@ ESC - Return to NORMAL mode
|
||||||
**Special messages (INSERT mode)**
|
**Special messages (INSERT mode)**
|
||||||
```
|
```
|
||||||
/me <action> - Send action (e.g. /me waves)
|
/me <action> - Send action (e.g. /me waves)
|
||||||
|
@username - Mention user (bell + highlight)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Security Configuration
|
### Security Configuration
|
||||||
|
|
@ -127,6 +128,9 @@ TNT_MAX_CONN_RATE_PER_IP=30 tnt
|
||||||
|
|
||||||
# Disable connection-rate and auth-failure blocking (testing only)
|
# Disable connection-rate and auth-failure blocking (testing only)
|
||||||
TNT_RATE_LIMIT=0 tnt
|
TNT_RATE_LIMIT=0 tnt
|
||||||
|
|
||||||
|
# Idle timeout in seconds (default 1800 = 30min, 0 to disable)
|
||||||
|
TNT_IDLE_TIMEOUT=3600 tnt
|
||||||
```
|
```
|
||||||
|
|
||||||
**SSH logging:**
|
**SSH logging:**
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
#define MAX_LOG_SIZE (10 * 1024 * 1024) /* 10 MiB */
|
#define MAX_LOG_SIZE (10 * 1024 * 1024) /* 10 MiB */
|
||||||
#define HOST_KEY_FILE "host_key"
|
#define HOST_KEY_FILE "host_key"
|
||||||
#define TNT_DEFAULT_STATE_DIR "."
|
#define TNT_DEFAULT_STATE_DIR "."
|
||||||
|
#define DEFAULT_IDLE_TIMEOUT 1800 /* 30 minutes */
|
||||||
|
|
||||||
/* ANSI color codes */
|
/* ANSI color codes */
|
||||||
#define ANSI_RESET "\033[0m"
|
#define ANSI_RESET "\033[0m"
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ typedef struct client {
|
||||||
char command_output[2048];
|
char command_output[2048];
|
||||||
char exec_command[MAX_EXEC_COMMAND_LEN];
|
char exec_command[MAX_EXEC_COMMAND_LEN];
|
||||||
char ssh_login[MAX_USERNAME_LEN];
|
char ssh_login[MAX_USERNAME_LEN];
|
||||||
|
time_t connect_time;
|
||||||
|
time_t last_active;
|
||||||
atomic_bool redraw_pending;
|
atomic_bool redraw_pending;
|
||||||
pthread_t thread;
|
pthread_t thread;
|
||||||
atomic_bool connected;
|
atomic_bool connected;
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ int main(int argc, char **argv) {
|
||||||
printf(" TNT_ACCESS_TOKEN Require this password for SSH auth\n");
|
printf(" TNT_ACCESS_TOKEN Require this password for SSH auth\n");
|
||||||
printf(" TNT_MAX_CONNECTIONS Global connection limit (default: 64)\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_RATE_LIMIT Set to 0 to disable rate limiting\n");
|
||||||
|
printf(" TNT_IDLE_TIMEOUT Idle disconnect timeout in seconds (default: 1800)\n");
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ static int g_max_conn_per_ip = 5;
|
||||||
static int g_max_conn_rate_per_ip = 10;
|
static int g_max_conn_rate_per_ip = 10;
|
||||||
static int g_rate_limit_enabled = 1;
|
static int g_rate_limit_enabled = 1;
|
||||||
static char g_access_token[256] = "";
|
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,
|
static void buffer_appendf(char *buffer, size_t buf_size, size_t *pos,
|
||||||
const char *fmt, ...) {
|
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_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_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) {
|
if ((env = getenv("TNT_ACCESS_TOKEN")) != NULL) {
|
||||||
strncpy(g_access_token, env, sizeof(g_access_token) - 1);
|
strncpy(g_access_token, env, sizeof(g_access_token) - 1);
|
||||||
g_access_token[sizeof(g_access_token) - 1] = '\0';
|
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;
|
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) {
|
static int exec_command_post(client_t *client, const char *args) {
|
||||||
char content[MAX_MESSAGE_LEN];
|
char content[MAX_MESSAGE_LEN];
|
||||||
char username[MAX_USERNAME_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);
|
room_broadcast(g_room, &msg);
|
||||||
|
notify_mentions(msg.content, client);
|
||||||
if (message_save(&msg) < 0) {
|
if (message_save(&msg) < 0) {
|
||||||
client_printf(client, "post: failed to persist message\n");
|
client_printf(client, "post: failed to persist message\n");
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -1138,11 +1167,21 @@ static void execute_command(client_t *client) {
|
||||||
"----------------------------------------\n",
|
"----------------------------------------\n",
|
||||||
g_room->client_count);
|
g_room->client_count);
|
||||||
|
|
||||||
|
time_t now = time(NULL);
|
||||||
for (int i = 0; i < g_room->client_count; i++) {
|
for (int i = 0; i < g_room->client_count; i++) {
|
||||||
char marker = (g_room->clients[i] == client) ? '*' : ' ';
|
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,
|
buffer_appendf(output, sizeof(output), &pos,
|
||||||
"%c %d. %s\n", marker, i + 1,
|
"%c %d. %s (%s)\n", marker, i + 1,
|
||||||
g_room->clients[i]->username);
|
g_room->clients[i]->username, dur_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
pthread_rwlock_unlock(&g_room->lock);
|
pthread_rwlock_unlock(&g_room->lock);
|
||||||
|
|
@ -1166,6 +1205,7 @@ static void execute_command(client_t *client) {
|
||||||
"========================================\n"
|
"========================================\n"
|
||||||
"In INSERT mode:\n"
|
"In INSERT mode:\n"
|
||||||
" /me <action> - Send action message\n"
|
" /me <action> - Send action message\n"
|
||||||
|
" @username - Mention (bell notify)\n"
|
||||||
"========================================\n");
|
"========================================\n");
|
||||||
|
|
||||||
} else if (strncmp(cmd, "msg ", 4) == 0 || strncmp(cmd, "w ", 2) == 0) {
|
} 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);
|
snprintf(msg.content, sizeof(msg.content), "%s", input);
|
||||||
}
|
}
|
||||||
room_broadcast(g_room, &msg);
|
room_broadcast(g_room, &msg);
|
||||||
|
notify_mentions(msg.content, client);
|
||||||
message_save(&msg);
|
message_save(&msg);
|
||||||
input[0] = '\0';
|
input[0] = '\0';
|
||||||
}
|
}
|
||||||
|
|
@ -1534,6 +1575,8 @@ void* client_handle_session(void *arg) {
|
||||||
client->connected = true;
|
client->connected = true;
|
||||||
client->command_history_count = 0;
|
client->command_history_count = 0;
|
||||||
client->command_history_pos = 0;
|
client->command_history_pos = 0;
|
||||||
|
client->connect_time = time(NULL);
|
||||||
|
client->last_active = time(NULL);
|
||||||
|
|
||||||
/* Check for exec command */
|
/* Check for exec command */
|
||||||
if (client->exec_command[0] != '\0') {
|
if (client->exec_command[0] != '\0') {
|
||||||
|
|
@ -1610,6 +1653,13 @@ void* client_handle_session(void *arg) {
|
||||||
}
|
}
|
||||||
last_keepalive = time(NULL);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1621,6 +1671,7 @@ void* client_handle_session(void *arg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
last_keepalive = time(NULL);
|
last_keepalive = time(NULL);
|
||||||
|
client->last_active = last_keepalive;
|
||||||
|
|
||||||
unsigned char b = buf[0];
|
unsigned char b = buf[0];
|
||||||
|
|
||||||
|
|
|
||||||
28
src/tui.c
28
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,
|
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;
|
struct tm tm_info;
|
||||||
localtime_r(&msg->timestamp, &tm_info);
|
localtime_r(&msg->timestamp, &tm_info);
|
||||||
char time_str[32];
|
char time_str[32];
|
||||||
strftime(time_str, sizeof(time_str), "%H:%M", &tm_info);
|
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) {
|
if (strcmp(msg->username, "系统") == 0) {
|
||||||
snprintf(buffer, buf_size,
|
snprintf(buffer, buf_size,
|
||||||
"\033[90m--> %s\033[0m", msg->content);
|
"\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);
|
time_str, msg->content);
|
||||||
} else {
|
} else {
|
||||||
snprintf(buffer, buf_size,
|
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),
|
time_str, username_color(msg->username),
|
||||||
msg->username, msg->content);
|
msg->username, hl_start, msg->content, hl_end);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Plain-text version for width calculation */
|
/* 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);
|
time_str, truncated_content);
|
||||||
} else {
|
} else {
|
||||||
snprintf(buffer, buf_size,
|
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),
|
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) {
|
if (msg_snapshot) {
|
||||||
for (int i = 0; i < snapshot_count; i++) {
|
for (int i = 0; i < snapshot_count; i++) {
|
||||||
char msg_line[2048];
|
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);
|
buffer_appendf(buffer, buf_size, &pos, "%s\033[K\r\n", msg_line);
|
||||||
}
|
}
|
||||||
free(msg_snapshot);
|
free(msg_snapshot);
|
||||||
|
|
@ -411,6 +425,7 @@ const char* tui_get_help_text(help_lang_t lang) {
|
||||||
"\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"
|
||||||
|
" @username - Mention user (bell + highlight)\n"
|
||||||
"\n"
|
"\n"
|
||||||
"HELP SCREEN KEYS:\n"
|
"HELP SCREEN KEYS:\n"
|
||||||
" q, ESC - Close help\n"
|
" q, ESC - Close help\n"
|
||||||
|
|
@ -454,6 +469,7 @@ const char* tui_get_help_text(help_lang_t lang) {
|
||||||
"\n"
|
"\n"
|
||||||
"特殊消息:\n"
|
"特殊消息:\n"
|
||||||
" /me <动作> - 发送动作 (如 /me 挥手)\n"
|
" /me <动作> - 发送动作 (如 /me 挥手)\n"
|
||||||
|
" @用户名 - 提及用户 (响铃+高亮)\n"
|
||||||
"\n"
|
"\n"
|
||||||
"帮助界面按键:\n"
|
"帮助界面按键:\n"
|
||||||
" q, ESC - 关闭帮助\n"
|
" q, ESC - 关闭帮助\n"
|
||||||
|
|
|
||||||
5
tnt.1
5
tnt.1
|
|
@ -85,6 +85,7 @@ Ctrl+W Delete last word
|
||||||
Ctrl+U Clear input line
|
Ctrl+U Clear input line
|
||||||
Ctrl+C Switch to NORMAL
|
Ctrl+C Switch to NORMAL
|
||||||
/me \fIaction\fR Send action message (e.g. /me waves)
|
/me \fIaction\fR Send action message (e.g. /me waves)
|
||||||
|
@\fIusername\fR Mention user (bell notification + highlight)
|
||||||
.TE
|
.TE
|
||||||
.SS NORMAL mode
|
.SS NORMAL mode
|
||||||
.TS
|
.TS
|
||||||
|
|
@ -152,6 +153,10 @@ Max new connections per IP per 60\-second window (default: 10).
|
||||||
.B TNT_RATE_LIMIT
|
.B TNT_RATE_LIMIT
|
||||||
Set to 0 to disable rate\-based blocking and auth\-failure IP blocking.
|
Set to 0 to disable rate\-based blocking and auth\-failure IP blocking.
|
||||||
Explicit capacity limits still apply (default: 1).
|
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
|
.SH FILES
|
||||||
.TP
|
.TP
|
||||||
.I messages.log
|
.I messages.log
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue