diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index b08ce2d..1f301c6 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -942,6 +942,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx) ON_COMMAND(ID_MSGLOG_COPY, &CMy2015RemoteDlg::OnMsglogCopy) ON_COMMAND(ID_MSGLOG_CLEAR, &CMy2015RemoteDlg::OnMsglogClear) ON_COMMAND(ID_MSGLOG_SEARCH, &CMy2015RemoteDlg::OnMsglogSearch) + ON_COMMAND(ID_MSGLOG_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnMsglogLoginNotify) ON_COMMAND(ID_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch) ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify) ON_NOTIFY(NM_CUSTOMDRAW, IDC_ONLINE, &CMy2015RemoteDlg::OnNMCustomdrawOnline) @@ -9303,11 +9304,18 @@ void CMy2015RemoteDlg::OnRClickMessage(NMHDR* pNMHDR, LRESULT* pResult) menu.AppendMenu(MF_SEPARATOR); menu.AppendMenu(MF_STRING, ID_MSGLOG_SEARCH, m_bLogSearchVisible ? _TR("隐藏搜索") : _TR("搜索日志")); + menu.AppendMenu(MF_SEPARATOR); + menu.AppendMenu(MF_STRING, ID_MSGLOG_LOGIN_NOTIFY, _TR("上线提醒")); - // 没有选中项时禁用"删除选中" + // 没有选中项时禁用"删除选中"和"上线提醒" if (m_CList_Message.GetSelectedCount() == 0) { menu.EnableMenuItem(ID_MSGLOG_DELETE, MF_GRAYED); menu.EnableMenuItem(ID_MSGLOG_COPY, MF_GRAYED); + menu.EnableMenuItem(ID_MSGLOG_LOGIN_NOTIFY, MF_GRAYED); + } + // PowerShell不可用时禁用"上线提醒" + if (!GetNotifyManager().IsPowerShellAvailable()) { + menu.EnableMenuItem(ID_MSGLOG_LOGIN_NOTIFY, MF_GRAYED); } // 列表为空时禁用"清空日志" if (m_CList_Message.GetItemCount() == 0) { @@ -9379,6 +9387,168 @@ void CMy2015RemoteDlg::OnMsglogSearch() OnSize(SIZE_RESTORED, rc.Width(), rc.Height()); } +// Extract the first valid IPv4 address from a log message text. +// Returns true and sets outIP if a valid IPv4 address is found. +static bool ExtractFirstIPFromMessage(const CString& msg, CString& outIP) +{ + // Scan the string for patterns like d.d.d.d and validate each segment + int len = msg.GetLength(); + for (int i = 0; i < len; i++) { + TCHAR ch = msg[i]; + // Look for start of a potential IP: digit + if (ch >= _T('0') && ch <= _T('9')) { + int segments[4] = { -1, -1, -1, -1 }; + int segIdx = 0; + int numStart = i; + + for (int j = i; j < len && segIdx < 4; j++) { + TCHAR c = msg[j]; + if (c >= _T('0') && c <= _T('9')) { + if (numStart < 0) numStart = j; + } else if (c == _T('.')) { + if (numStart < 0) break; // consecutive dots or leading dot + // Parse the number + CString numStr = msg.Mid(numStart, j - numStart); + if (numStr.GetLength() > 3) break; // too many digits + int val = _tstoi(numStr); + if (val > 255) break; + segments[segIdx] = val; + segIdx++; + numStart = -1; + if (segIdx >= 4) break; // too many segments + } else { + break; // non-IP character + } + } + + // Check if we have exactly 4 valid segments and the last char ends a number + if (segIdx == 3 && numStart >= 0) { + // Find where the number ends + int numEnd = numStart; + while (numEnd < len && msg[numEnd] >= _T('0') && msg[numEnd] <= _T('9')) + numEnd++; + CString numStr = msg.Mid(numStart, numEnd - numStart); + if (numStr.GetLength() <= 3) { + int val = _tstoi(numStr); + if (val >= 0 && val <= 255) { + segments[3] = val; + // Success: build the IP string + outIP.Format(_T("%d.%d.%d.%d"), segments[0], segments[1], segments[2], segments[3]); + return true; + } + } + } + // Skip past the partial match to continue scanning + // (the outer loop will increment i, which is fine) + } + } + return false; +} + +void CMy2015RemoteDlg::OnMsglogLoginNotify() +{ + // Get selected log messages and extract IP addresses from them + std::vector ipsToAdd; + + POSITION pos = m_CList_Message.GetFirstSelectedItemPosition(); + while (pos) { + int row = m_CList_Message.GetNextSelectedItem(pos); + // Column 2 is the message content ("信息内容") + CString msgText = m_CList_Message.GetItemText(row, 2); + + CString extractedIP; + if (ExtractFirstIPFromMessage(msgText, extractedIP)) { + std::string ipUtf8 = CT2A(extractedIP, CP_UTF8); + ipsToAdd.push_back(ipUtf8); + } + } + + if (ipsToAdd.empty()) { + MessageBoxL("所选日志消息中未检测到有效的 IP 地址", "提示", MB_ICONINFORMATION); + return; + } + + // Helper to convert string to lowercase + auto toLower = [](const std::string& str) -> std::string { + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), ::tolower); + return result; + }; + + // Use the same deduplication logic as OnOnlineLoginNotify + NotifyConfig config = GetNotifyManager().GetConfig(); + NotifyRule& rule = config.GetRule(); + + // Parse existing keywords into a set for deduplication (case-insensitive) + std::set existingKeywordsLower; + std::vector existingKeywords; + std::string pattern = rule.matchPattern; + size_t patternPos = 0; + while ((patternPos = pattern.find(';')) != std::string::npos) { + std::string kw = pattern.substr(0, patternPos); + size_t start = kw.find_first_not_of(" \t"); + size_t end = kw.find_last_not_of(" \t"); + if (start != std::string::npos) { + std::string trimmed = kw.substr(start, end - start + 1); + existingKeywordsLower.insert(toLower(trimmed)); + existingKeywords.push_back(trimmed); + } + pattern.erase(0, patternPos + 1); + } + if (!pattern.empty()) { + size_t start = pattern.find_first_not_of(" \t"); + size_t end = pattern.find_last_not_of(" \t"); + if (start != std::string::npos) { + std::string trimmed = pattern.substr(start, end - start + 1); + existingKeywordsLower.insert(toLower(trimmed)); + existingKeywords.push_back(trimmed); + } + } + + // Add new IPs if not already present + int addedCount = 0; + for (const auto& ip : ipsToAdd) { + if (existingKeywordsLower.find(toLower(ip)) == existingKeywordsLower.end()) { + existingKeywordsLower.insert(toLower(ip)); + existingKeywords.push_back(ip); + addedCount++; + } + } + + if (addedCount == 0) { + MessageBoxL("所有解析出的 IP 已在上线提醒列表中", "提示", MB_ICONINFORMATION); + return; + } + + // Rebuild match pattern + std::string newPattern; + for (const auto& kw : existingKeywords) { + if (!newPattern.empty()) { + newPattern += ";"; + } + newPattern += kw; + } + + // Update config (preserve existing columnIndex and triggerType) + rule.matchPattern = newPattern; + rule.enabled = true; + if (rule.triggerType == NOTIFY_TRIGGER_NONE) { + rule.triggerType = NOTIFY_TRIGGER_HOST_ONLINE; + } + + GetNotifyManager().SetConfig(config); + GetNotifyManager().SaveConfig(); + + // Build message with SMTP warning if not configured + CString msg; + msg.Format(_TR("已添加 %d 个 IP 到上线提醒列表"), addedCount); + if (!config.smtp.IsValid()) { + msg += _T("\n\n"); + msg += _TR("注意: SMTP 未配置,请先在通知设置中配置邮箱"); + } + MessageBoxL(msg, _TR("上线提醒"), MB_ICONINFORMATION); +} + void CMy2015RemoteDlg::OnOnlineAddWatch() { EnterCriticalSection(&m_cs); diff --git a/server/2015Remote/2015RemoteDlg.h b/server/2015Remote/2015RemoteDlg.h index a9eafa6..2461e8a 100644 --- a/server/2015Remote/2015RemoteDlg.h +++ b/server/2015Remote/2015RemoteDlg.h @@ -564,6 +564,7 @@ public: afx_msg void OnMsglogCopy(); afx_msg void OnMsglogClear(); afx_msg void OnMsglogSearch(); + afx_msg void OnMsglogLoginNotify(); afx_msg void OnOnlineAddWatch(); afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnOnlineRunAsAdmin(); diff --git a/server/2015Remote/NotifyManager.cpp b/server/2015Remote/NotifyManager.cpp index bfbe1e4..578cba0 100644 --- a/server/2015Remote/NotifyManager.cpp +++ b/server/2015Remote/NotifyManager.cpp @@ -141,35 +141,52 @@ bool NotifyManager::ShouldNotify(context* ctx, std::string& outMatchedKeyword, c } } - // Get column text (for COMPUTER_NAME column, prefer remark if available) + // Split pattern by semicolon and get keywords + std::vector keywords = SplitString(rule.matchPattern, ';'); + + // Helper: check if any keyword matches a given column text + auto matchColumn = [&keywords, this](const CString& colText) -> std::string { + if (colText.IsEmpty()) return ""; + std::string colTextStr = CT2A(colText, CP_UTF8); + std::string colLower = colTextStr; + std::transform(colLower.begin(), colLower.end(), colLower.begin(), ::tolower); + + for (const auto& kw : keywords) { + std::string trimmed = Trim(kw); + if (trimmed.empty()) continue; + + std::string kwLower = trimmed; + std::transform(kwLower.begin(), kwLower.end(), kwLower.begin(), ::tolower); + + if (colLower.find(kwLower) != std::string::npos) { + return trimmed; + } + } + return ""; + }; + + // Check primary column (for COMPUTER_NAME column, prefer remark if available) CString colText; if (rule.columnIndex == ONLINELIST_COMPUTER_NAME && !remark.IsEmpty()) { colText = remark; } else { colText = ctx->GetClientData(rule.columnIndex); } - if (colText.IsEmpty()) return false; + std::string matched = matchColumn(colText); + if (!matched.empty()) { + outMatchedKeyword = matched; + m_config.lastNotifyTime[clientId] = now; + return true; + } - // Convert to std::string for matching - std::string colTextStr = CT2A(colText, CP_UTF8); - - // Split pattern by semicolon and check each keyword - std::vector keywords = SplitString(rule.matchPattern, ';'); - for (const auto& kw : keywords) { - std::string trimmed = Trim(kw); - if (trimmed.empty()) continue; - - // Case-insensitive substring search - std::string colLower = colTextStr; - std::string kwLower = trimmed; - std::transform(colLower.begin(), colLower.end(), colLower.begin(), ::tolower); - std::transform(kwLower.begin(), kwLower.end(), kwLower.begin(), ::tolower); - - if (colLower.find(kwLower) != std::string::npos) { - outMatchedKeyword = trimmed; - m_config.lastNotifyTime[clientId] = now; - return true; - } + // Also check the IP column (ONLINELIST_IP) so IP keywords added from log + // messages can match against host IP addresses when they come online. + CString ipText = ctx->GetClientData(ONLINELIST_IP); + matched = matchColumn(ipText); + if (!matched.empty()) { + outMatchedKeyword = matched; + m_config.lastNotifyTime[clientId] = now; + return true; } return false; diff --git a/server/2015Remote/lang/en_US.ini b/server/2015Remote/lang/en_US.ini index 4bc36c1..beb1880 100644 --- a/server/2015Remote/lang/en_US.ini +++ b/server/2015Remote/lang/en_US.ini @@ -1965,3 +1965,6 @@ FRPC Զ ־=Search Logs =Hide Search =Please +ѡ־Ϣδ⵽Ч IP ַ=No valid IP address detected in the selected log messages +н IP б=All extracted IPs are already in the login notify list + %d IP б=Added %d IP(s) to login notify list diff --git a/server/2015Remote/lang/zh_TW.ini b/server/2015Remote/lang/zh_TW.ini index a385dbc..6e7ffc2 100644 --- a/server/2015Remote/lang/zh_TW.ini +++ b/server/2015Remote/lang/zh_TW.ini @@ -1956,3 +1956,6 @@ FRPC Զ ־=־ = = +н IP б=н IP Ͼб + %d IP б= %d IP Ͼб +ѡ־Ϣδ⵽Ч IP ַ=xIϢδzyЧ IP λַ diff --git a/server/2015Remote/resource.h b/server/2015Remote/resource.h index 659ee50..4c7eeef 100644 --- a/server/2015Remote/resource.h +++ b/server/2015Remote/resource.h @@ -991,6 +991,7 @@ #define ID_CANCEL_SHARE 33042 #define ID_MSGLOG_COPY 33043 #define ID_MSGLOG_SEARCH 33078 +#define ID_MSGLOG_LOGIN_NOTIFY 33079 #define ID_WEB_REMOTE_CONTROL 33044 #define ID_TOOL_PLUGIN_SETTINGS 33045 #define ID_33046 33046 @@ -1032,7 +1033,7 @@ #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 398 -#define _APS_NEXT_COMMAND_VALUE 33079 +#define _APS_NEXT_COMMAND_VALUE 33080 #define _APS_NEXT_CONTROL_VALUE 2542 #define _APS_NEXT_SYMED_VALUE 105 #endif