diff --git a/.gitignore b/.gitignore index f5f5f5f..f44737c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ tests/unit/test_message tests/unit/test_chat_room tests/unit/test_history_view tests/unit/test_i18n +tests/unit/test_system_message diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2f29545..ea61960 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -22,6 +22,9 @@ the i18n table instead of being scattered through command flow logic. - TUI title-bar status labels, including online count, mute marker, and help hint, now follow the session UI language. +- Join, leave, and nickname-change system messages now use a dedicated + `system_message` module, follow the sender's session language, and keep + `:mute-joins` filtering compatible with both Chinese and English logs. ### Changed - NORMAL mode now opens at the latest visible messages instead of the oldest diff --git a/include/i18n.h b/include/i18n.h index bf56f2e..4ab5302 100644 --- a/include/i18n.h +++ b/include/i18n.h @@ -19,6 +19,10 @@ typedef enum { I18N_TITLE_ONLINE_FORMAT, I18N_TITLE_MUTED, I18N_TITLE_HELP_HINT, + I18N_SYSTEM_USERNAME, + I18N_SYSTEM_JOIN_FORMAT, + I18N_SYSTEM_LEAVE_FORMAT, + I18N_SYSTEM_NICK_FORMAT, I18N_USERS_TITLE, I18N_MSG_USAGE, I18N_MSG_SENT_FORMAT, diff --git a/include/system_message.h b/include/system_message.h new file mode 100644 index 0000000..a82ece7 --- /dev/null +++ b/include/system_message.h @@ -0,0 +1,17 @@ +#ifndef SYSTEM_MESSAGE_H +#define SYSTEM_MESSAGE_H + +#include "common.h" +#include "message.h" + +void system_message_make_join(message_t *msg, const char *username, + help_lang_t lang); +void system_message_make_leave(message_t *msg, const char *username, + help_lang_t lang); +void system_message_make_nick(message_t *msg, const char *old_name, + const char *new_name, help_lang_t lang); + +bool system_message_is_system(const message_t *msg); +bool system_message_is_join_leave(const message_t *msg); + +#endif /* SYSTEM_MESSAGE_H */ diff --git a/src/commands.c b/src/commands.c index e872b9c..44eab1e 100644 --- a/src/commands.c +++ b/src/commands.c @@ -11,6 +11,7 @@ #include "i18n.h" #include "message.h" #include "support.h" +#include "system_message.h" #include "tui.h" #include "utf8.h" #include @@ -425,10 +426,9 @@ void commands_dispatch(client_t *client) { i18n_text(client->help_lang, I18N_NICK_UNCHANGED)); } else { - message_t nick_msg = { .timestamp = time(NULL) }; - snprintf(nick_msg.username, MAX_USERNAME_LEN, "系统"); - snprintf(nick_msg.content, MAX_MESSAGE_LEN, - "%s 更名为 %s", old_name, client->username); + message_t nick_msg; + system_message_make_nick(&nick_msg, old_name, + client->username, client->help_lang); room_broadcast(g_room, &nick_msg); message_save(&nick_msg); diff --git a/src/i18n.c b/src/i18n.c index 19ee198..7dde39f 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -101,6 +101,14 @@ const char *i18n_text(help_lang_t lang, i18n_text_id_t id) { return "静音"; case I18N_TITLE_HELP_HINT: return "? 帮助"; + case I18N_SYSTEM_USERNAME: + return "系统"; + case I18N_SYSTEM_JOIN_FORMAT: + return "%s 加入了聊天室"; + case I18N_SYSTEM_LEAVE_FORMAT: + return "%s 离开了聊天室"; + case I18N_SYSTEM_NICK_FORMAT: + return "%s 更名为 %s"; case I18N_USERS_TITLE: return "在线用户"; case I18N_MSG_USAGE: @@ -174,6 +182,14 @@ const char *i18n_text(help_lang_t lang, i18n_text_id_t id) { return "muted"; case I18N_TITLE_HELP_HINT: return "? help"; + case I18N_SYSTEM_USERNAME: + return "system"; + case I18N_SYSTEM_JOIN_FORMAT: + return "%s joined the room"; + case I18N_SYSTEM_LEAVE_FORMAT: + return "%s left the room"; + case I18N_SYSTEM_NICK_FORMAT: + return "%s renamed to %s"; case I18N_USERS_TITLE: return "Online users"; case I18N_MSG_USAGE: diff --git a/src/input.c b/src/input.c index 707faea..441db18 100644 --- a/src/input.c +++ b/src/input.c @@ -8,6 +8,7 @@ #include "i18n.h" #include "message.h" #include "ratelimit.h" +#include "system_message.h" #include "tui.h" #include "utf8.h" #include @@ -701,12 +702,8 @@ void input_run_session(client_t *client) { bracketed_paste_enabled = true; /* Broadcast join message */ - message_t join_msg = { - .timestamp = time(NULL), - }; - strncpy(join_msg.username, "系统", MAX_USERNAME_LEN - 1); - join_msg.username[MAX_USERNAME_LEN - 1] = '\0'; - snprintf(join_msg.content, MAX_MESSAGE_LEN, "%s 加入了聊天室", client->username); + message_t join_msg; + system_message_make_join(&join_msg, client->username, client->help_lang); room_broadcast(g_room, &join_msg); message_save(&join_msg); @@ -892,12 +889,9 @@ cleanup: /* Broadcast leave message */ if (joined_room) { - message_t leave_msg = { - .timestamp = time(NULL), - }; - strncpy(leave_msg.username, "系统", MAX_USERNAME_LEN - 1); - leave_msg.username[MAX_USERNAME_LEN - 1] = '\0'; - snprintf(leave_msg.content, MAX_MESSAGE_LEN, "%s 离开了聊天室", client->username); + message_t leave_msg; + system_message_make_leave(&leave_msg, client->username, + client->help_lang); client->connected = false; room_remove_client(g_room, client); diff --git a/src/system_message.c b/src/system_message.c new file mode 100644 index 0000000..930b1d2 --- /dev/null +++ b/src/system_message.c @@ -0,0 +1,72 @@ +#include "system_message.h" +#include "i18n.h" +#include +#include +#include + +static void system_message_init(message_t *msg, help_lang_t lang) { + if (!msg) { + return; + } + + memset(msg, 0, sizeof(*msg)); + msg->timestamp = time(NULL); + snprintf(msg->username, sizeof(msg->username), "%s", + i18n_text(lang, I18N_SYSTEM_USERNAME)); +} + +void system_message_make_join(message_t *msg, const char *username, + help_lang_t lang) { + system_message_init(msg, lang); + if (!msg) { + return; + } + + snprintf(msg->content, sizeof(msg->content), + i18n_text(lang, I18N_SYSTEM_JOIN_FORMAT), + username ? username : ""); +} + +void system_message_make_leave(message_t *msg, const char *username, + help_lang_t lang) { + system_message_init(msg, lang); + if (!msg) { + return; + } + + snprintf(msg->content, sizeof(msg->content), + i18n_text(lang, I18N_SYSTEM_LEAVE_FORMAT), + username ? username : ""); +} + +void system_message_make_nick(message_t *msg, const char *old_name, + const char *new_name, help_lang_t lang) { + system_message_init(msg, lang); + if (!msg) { + return; + } + + snprintf(msg->content, sizeof(msg->content), + i18n_text(lang, I18N_SYSTEM_NICK_FORMAT), + old_name ? old_name : "", new_name ? new_name : ""); +} + +bool system_message_is_system(const message_t *msg) { + if (!msg) { + return false; + } + + return strcmp(msg->username, "系统") == 0 || + strcmp(msg->username, "system") == 0; +} + +bool system_message_is_join_leave(const message_t *msg) { + if (!system_message_is_system(msg)) { + return false; + } + + return strstr(msg->content, "加入了聊天室") != NULL || + strstr(msg->content, "离开了聊天室") != NULL || + strstr(msg->content, "joined the room") != NULL || + strstr(msg->content, "left the room") != NULL; +} diff --git a/src/tui.c b/src/tui.c index b099607..9df356a 100644 --- a/src/tui.c +++ b/src/tui.c @@ -4,16 +4,11 @@ #include "chat_room.h" #include "history_view.h" #include "i18n.h" +#include "system_message.h" #include "tui_status.h" #include "utf8.h" #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", @@ -37,7 +32,7 @@ static void format_message_colored(const message_t *msg, char *buffer, * marker so they can scan their own contributions when scrolling. */ bool is_self = false; if (my_username && my_username[0] != '\0' && - strcmp(msg->username, "系统") != 0) { + !system_message_is_system(msg)) { if (strcmp(msg->username, "*") == 0) { /* /me message: content starts with the actor's username */ size_t un_len = strlen(my_username); @@ -54,7 +49,7 @@ static void format_message_colored(const message_t *msg, char *buffer, bool mentioned = false; if (my_username && my_username[0] != '\0' && - strcmp(msg->username, "系统") != 0) { + !system_message_is_system(msg)) { char mention[MAX_USERNAME_LEN + 2]; snprintf(mention, sizeof(mention), "@%s", my_username); if (strstr(msg->content, mention) != NULL) { @@ -64,7 +59,7 @@ static void format_message_colored(const message_t *msg, char *buffer, const char *hl_start = mentioned ? "\033[1;33m" : ""; const char *hl_end = mentioned ? "\033[0m" : ""; - if (strcmp(msg->username, "系统") == 0) { + if (system_message_is_system(msg)) { snprintf(buffer, buf_size, "%s\033[90m--> %s\033[0m", gutter, msg->content); } else if (strcmp(msg->username, "*") == 0) { @@ -80,7 +75,7 @@ static void format_message_colored(const message_t *msg, char *buffer, /* Plain-text version for width calculation — gutter is 1 column. */ char plain[MAX_MESSAGE_LEN + 128]; - if (strcmp(msg->username, "系统") == 0) { + if (system_message_is_system(msg)) { snprintf(plain, sizeof(plain), " --> %s", msg->content); } else if (strcmp(msg->username, "*") == 0) { snprintf(plain, sizeof(plain), " %s * %s", time_str, msg->content); @@ -94,7 +89,7 @@ static void format_message_colored(const message_t *msg, char *buffer, * 1-column gutter so the budget math comes out right. */ int prefix_width; char prefix_plain[256]; - if (strcmp(msg->username, "系统") == 0) { + if (system_message_is_system(msg)) { snprintf(prefix_plain, sizeof(prefix_plain), " --> "); } else if (strcmp(msg->username, "*") == 0) { snprintf(prefix_plain, sizeof(prefix_plain), " %s * ", time_str); @@ -107,7 +102,7 @@ static void format_message_colored(const message_t *msg, char *buffer, if (content_width < 4) content_width = 4; char truncated_content[MAX_MESSAGE_LEN]; - if (strcmp(msg->username, "系统") == 0) { + if (system_message_is_system(msg)) { strncpy(truncated_content, msg->content, sizeof(truncated_content) - 1); truncated_content[sizeof(truncated_content) - 1] = '\0'; } else if (strcmp(msg->username, "*") == 0) { @@ -118,7 +113,7 @@ static void format_message_colored(const message_t *msg, char *buffer, } utf8_truncate(truncated_content, content_width); - if (strcmp(msg->username, "系统") == 0) { + if (system_message_is_system(msg)) { snprintf(buffer, buf_size, "%s\033[90m--> %s\033[0m", gutter, truncated_content); } else if (strcmp(msg->username, "*") == 0) { @@ -325,7 +320,7 @@ void tui_render_screen(client_t *client) { 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])) { + if (!system_message_is_join_leave(&msg_snapshot[i])) { msg_snapshot[filtered++] = msg_snapshot[i]; } } diff --git a/tests/test_interactive_input.sh b/tests/test_interactive_input.sh index f2474a1..20e412d 100755 --- a/tests/test_interactive_input.sh +++ b/tests/test_interactive_input.sh @@ -322,6 +322,48 @@ else FAIL=$((FAIL + 1)) fi +SYSTEM_MESSAGES_SCRIPT="$STATE_DIR/system-messages.expect" +cat >"$SYSTEM_MESSAGES_SCRIPT" < systemuser2" +expect "Press any key" +send -- "q" +sleep 0.2 +send -- "\003" +sleep 0.2 +send -- "\003" +expect eof +EOF + +if expect "$SYSTEM_MESSAGES_SCRIPT" >"$STATE_DIR/system-messages.log" 2>&1 && + grep -q 'system|systemuser renamed to systemuser2' "$STATE_DIR/messages.log" && + grep -q 'system|systemuser2 left the room' "$STATE_DIR/messages.log"; then + echo "✓ system messages follow session language" + PASS=$((PASS + 1)) +else + echo "x localized system messages failed" + sed -n '1,220p' "$STATE_DIR/system-messages.log" 2>/dev/null || true + cat "$STATE_DIR/messages.log" 2>/dev/null || true + sed -n '1,120p' "$STATE_DIR/server.log" + FAIL=$((FAIL + 1)) +fi + printf '维护窗口\n' >"$STATE_DIR/motd.txt" MOTD_SCRIPT="$STATE_DIR/motd.expect" cat >"$MOTD_SCRIPT" < +#include +#include + +#define TEST(name) static void test_##name() +#define RUN_TEST(name) do { \ + printf("Running %s... ", #name); \ + test_##name(); \ + printf("✓\n"); \ + tests_passed++; \ +} while(0) + +static int tests_passed = 0; + +TEST(join_leave_follow_language) { + message_t msg; + + system_message_make_join(&msg, "alice", LANG_ZH); + assert(strcmp(msg.username, "系统") == 0); + assert(strstr(msg.content, "alice") != NULL); + assert(strstr(msg.content, "加入了聊天室") != NULL); + assert(system_message_is_system(&msg)); + assert(system_message_is_join_leave(&msg)); + + system_message_make_leave(&msg, "bob", LANG_EN); + assert(strcmp(msg.username, "system") == 0); + assert(strstr(msg.content, "bob") != NULL); + assert(strstr(msg.content, "left the room") != NULL); + assert(system_message_is_system(&msg)); + assert(system_message_is_join_leave(&msg)); +} + +TEST(nick_messages_are_system_events_not_join_leave) { + message_t msg; + + system_message_make_nick(&msg, "old", "new", LANG_EN); + assert(strcmp(msg.username, "system") == 0); + assert(strstr(msg.content, "old") != NULL); + assert(strstr(msg.content, "new") != NULL); + assert(strstr(msg.content, "renamed") != NULL); + assert(system_message_is_system(&msg)); + assert(!system_message_is_join_leave(&msg)); + + system_message_make_nick(&msg, "旧", "新", LANG_ZH); + assert(strcmp(msg.username, "系统") == 0); + assert(strstr(msg.content, "更名为") != NULL); + assert(system_message_is_system(&msg)); + assert(!system_message_is_join_leave(&msg)); +} + +TEST(legacy_system_names_are_recognized) { + message_t msg = {0}; + + snprintf(msg.username, sizeof(msg.username), "系统"); + snprintf(msg.content, sizeof(msg.content), "alice 离开了聊天室"); + assert(system_message_is_system(&msg)); + assert(system_message_is_join_leave(&msg)); + + snprintf(msg.username, sizeof(msg.username), "system"); + snprintf(msg.content, sizeof(msg.content), "alice joined the room"); + assert(system_message_is_system(&msg)); + assert(system_message_is_join_leave(&msg)); +} + +int main(void) { + printf("Running system message unit tests...\n\n"); + + RUN_TEST(join_leave_follow_language); + RUN_TEST(nick_messages_are_system_events_not_join_leave); + RUN_TEST(legacy_system_names_are_recognized); + + printf("\n✓ All %d tests passed!\n", tests_passed); + return 0; +}