From 40dfe5d10da814dd353d381c5ed58f04e34f808a Mon Sep 17 00:00:00 2001 From: VovcheG1 Date: Tue, 21 Jul 2026 23:00:37 +0300 Subject: [PATCH] =?UTF-8?q?ServerVk.=20names=20=D0=BA=D0=BE=D0=BD=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=82=D0=B8=D1=80=D1=83=D1=8E=D1=82=D1=81=D1=8F=20?= =?UTF-8?q?=D0=B2=20=D1=87=D0=B8=D1=81=D0=BB=D0=BE=D0=B2=D1=8B=D0=B5=20ID,?= =?UTF-8?q?=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=B0?= =?UTF-8?q?=D0=B4=D1=80=D0=B5=D1=81=20=D0=BD=D0=B0=20ru?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- project/OsEngine/Language/LoggingLocal.cs | 4 + project/OsEngine/Logging/ServerVk.cs | 419 ++++++++++++++++---- project/OsEngine/Logging/ServerVkUi.xaml | 5 +- project/OsEngine/Logging/ServerVkUi.xaml.cs | 40 +- project/OsEngine/bin/Debug/OsEngine.dll | Bin 10621952 -> 10626048 bytes project/OsEngine/bin/Debug/OsEngine.exe | Bin 166912 -> 166912 bytes 6 files changed, 379 insertions(+), 89 deletions(-) diff --git a/project/OsEngine/Language/LoggingLocal.cs b/project/OsEngine/Language/LoggingLocal.cs index 016cf22d27..3fe0463e4e 100644 --- a/project/OsEngine/Language/LoggingLocal.cs +++ b/project/OsEngine/Language/LoggingLocal.cs @@ -51,6 +51,10 @@ public class LoggingLocal "Eng:Configure VK_" + "Ru:Настроить ВК_"); + public string Button5 => OsLocalization.ConvertToLocString( + "Eng:Disconnect_" + + "Ru:Отключить_"); + public string ButtonClearExtraLog => OsLocalization.ConvertToLocString( "Eng:Clean up_" + "Ru:Очистить _"); diff --git a/project/OsEngine/Logging/ServerVk.cs b/project/OsEngine/Logging/ServerVk.cs index 5a9a6250c3..607fb002ce 100644 --- a/project/OsEngine/Logging/ServerVk.cs +++ b/project/OsEngine/Logging/ServerVk.cs @@ -12,7 +12,6 @@ using System.IO; using System.Net.Http; using System.Threading; -using System.Windows; namespace OsEngine.Logging { @@ -35,16 +34,32 @@ public static ServerVk GetServer() #region Fields - private readonly HttpClient _httpClient; + private HttpClient _httpClient; - private readonly Random _random = new Random(); + /// + /// Base URL VK API (api.vk.ru — актуальный домен, api.vk.com в некоторых сетях не резолвится) + /// + private string _apiBaseUrl = "https://api.vk.ru/method/"; - private readonly object _userIdsLocker = new object(); + private Random _random = new Random(); + + private string _userIdsLocker = "UserIdsLocker"; /// - /// Last message Update Id (используется для Long Poll) + /// Long Poll server URL (переиспользуется при реконнекте) /// - private long _lastUpdateId; + private string _longPollServerUrl; + + /// + /// Long Poll server key (переиспользуется при реконнекте) + /// + private string _longPollKey; + + /// + /// Last Long Poll timestamp (сохраняется между реконнектами, + /// чтобы не обрабатывать уже полученные апдейты повторно) + /// + private long _lastUpdateTs; /// /// Access Token for VK API @@ -64,7 +79,7 @@ public static ServerVk GetServer() /// /// Shows whether the server is ready to work /// - private static bool _isReady; + private bool _isReady; /// /// Queue of messages @@ -221,6 +236,18 @@ public void ShowDialog() ui.ShowDialog(); } + /// + /// Disconnect VK server (до повторного включения через Save в окне настроек) + /// + public void Disconnect() + { + _isReady = false; + + _messagesQueue.Clear(); + + ServerMaster.SendNewLogMessage("VK server disconnected by user", LogMessageType.System); + } + /// /// Parse user IDs from comma/space/semicolon separated string /// @@ -248,6 +275,257 @@ public List ParseUserIds(string line) return result; } + /// + /// Parse user IDs and screen names (короткие адреса вида durov или vk.com/durov), + /// screen names конвертируются в числовые ID через users.get + /// + public List ParseAndResolveUserIds(string line, out List unresolvedNames) + { + List result = new List(); + List screenNames = new List(); + unresolvedNames = new List(); + + if (string.IsNullOrWhiteSpace(line)) + { + return result; + } + + string[] parts = line.Split(new char[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries); + + for (int i = 0; i < parts.Length; i++) + { + string part = parts[i].Trim(); + + if (long.TryParse(part, out long id)) + { + if (!result.Contains(id)) + { + result.Add(id); + } + } + else + { + string screenName = CleanScreenName(part); + + if (!string.IsNullOrEmpty(screenName) + && !screenNames.Contains(screenName)) + { + screenNames.Add(screenName); + } + } + } + + List resolved = ResolveScreenNames(screenNames, out unresolvedNames); + + for (int i = 0; i < resolved.Count; i++) + { + if (!result.Contains(resolved[i])) + { + result.Add(resolved[i]); + } + } + + return result; + } + + /// + /// Validate Access Token and user IDs via users.get + /// + public bool ValidateTokenAndUserIds(List userIds, out string errorMessage) + { + errorMessage = null; + + if (string.IsNullOrEmpty(AccessToken)) + { + errorMessage = "Access Token is empty"; + ServerMaster.SendNewLogMessage($"VK validation failed: {errorMessage}", LogMessageType.Error); + return false; + } + + if (userIds == null + || userIds.Count == 0) + { + errorMessage = "User IDs list is empty"; + ServerMaster.SendNewLogMessage($"VK validation failed: {errorMessage}", LogMessageType.Error); + return false; + } + + try + { + string ids = string.Join(",", userIds); + + string url = _apiBaseUrl + "users.get?" + + $"user_ids={ids}" + + $"&access_token={AccessToken}&v=5.131"; + + HttpResponseMessage response = _httpClient.GetAsync(url).GetAwaiter().GetResult(); + string responseContent = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + dynamic parsed = JsonConvert.DeserializeObject(responseContent); + + if (parsed?.error != null) + { + errorMessage = $"VK API error {parsed.error.error_code}: {parsed.error.error_msg}"; + ServerMaster.SendNewLogMessage($"VK validation failed: {errorMessage}", LogMessageType.Error); + return false; + } + + if (parsed?.response == null) + { + errorMessage = $"VK API unexpected response: {responseContent}"; + ServerMaster.SendNewLogMessage($"VK validation failed: {errorMessage}", LogMessageType.Error); + return false; + } + + List found = new List(); + + foreach (var user in parsed.response) + { + found.Add((long)user.id); + } + + List missing = new List(); + + for (int i = 0; i < userIds.Count; i++) + { + if (!found.Contains(userIds[i])) + { + missing.Add(userIds[i]); + } + } + + if (missing.Count > 0) + { + errorMessage = $"User IDs not found: {string.Join(", ", missing)}"; + ServerMaster.SendNewLogMessage($"VK validation failed: {errorMessage}", LogMessageType.Error); + return false; + } + + return true; + } + catch (Exception error) + { + errorMessage = error.Message; + ServerMaster.SendNewLogMessage($"VK validation failed: {errorMessage}", LogMessageType.Error); + return false; + } + } + + /// + /// Remove prefixes @, vk.com/, https://vk.com/ from screen name + /// + private string CleanScreenName(string name) + { + name = name.Trim(); + + if (name.StartsWith("@")) + { + name = name.Substring(1); + } + + int vkComIndex = name.IndexOf("vk.com/", StringComparison.OrdinalIgnoreCase); + + if (vkComIndex >= 0) + { + name = name.Substring(vkComIndex + "vk.com/".Length); + } + + name = name.TrimEnd('/'); + + return name; + } + + /// + /// Convert screen names to numeric user IDs via users.get + /// + private List ResolveScreenNames(List screenNames, out List unresolvedNames) + { + List result = new List(); + unresolvedNames = new List(); + + if (screenNames == null + || screenNames.Count == 0) + { + return result; + } + + if (string.IsNullOrEmpty(AccessToken)) + { + unresolvedNames.AddRange(screenNames); + + ServerMaster.SendNewLogMessage( + "VK: can't resolve screen names, Access Token is empty", + LogMessageType.Error); + return result; + } + + try + { + string names = string.Join(",", screenNames); + + string url = _apiBaseUrl + "users.get?" + + $"user_ids={Uri.EscapeDataString(names)}" + + $"&fields=screen_name" + + $"&access_token={AccessToken}&v=5.131"; + + HttpResponseMessage response = _httpClient.GetAsync(url).GetAwaiter().GetResult(); + string responseContent = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + dynamic parsed = JsonConvert.DeserializeObject(responseContent); + + if (parsed?.response == null) + { + unresolvedNames.AddRange(screenNames); + + ServerMaster.SendNewLogMessage( + $"VK users.get failed for '{names}'. Response: {responseContent}", + LogMessageType.Error); + return result; + } + + List foundNames = new List(); + + foreach (var user in parsed.response) + { + long id = (long)user.id; + + if (!result.Contains(id)) + { + result.Add(id); + } + + string screenName = user.screen_name?.ToString(); + + if (!string.IsNullOrEmpty(screenName)) + { + foundNames.Add(screenName.ToLowerInvariant()); + } + } + + for (int i = 0; i < screenNames.Count; i++) + { + if (!foundNames.Contains(screenNames[i].ToLowerInvariant())) + { + unresolvedNames.Add(screenNames[i]); + } + } + + if (unresolvedNames.Count > 0) + { + ServerMaster.SendNewLogMessage( + $"VK: screen names not found: {string.Join(", ", unresolvedNames)}", + LogMessageType.Error); + } + } + catch (Exception error) + { + unresolvedNames.AddRange(screenNames); + ServerMaster.SendNewLogMessage(error.ToString(), LogMessageType.Error); + } + + return result; + } + /// /// Create command event /// @@ -259,7 +537,7 @@ public void ExecuteCommand(string botName, CommandVk cmd) } catch (Exception ex) { - MessageBox.Show(ex.ToString()); + ServerMaster.SendNewLogMessage(ex.ToString(), LogMessageType.Error); } } @@ -303,7 +581,7 @@ private void SendMessageAsync(string messageText) StringContent content = new StringContent(postData, System.Text.Encoding.UTF8, "application/x-www-form-urlencoded"); - HttpResponseMessage response = _httpClient.PostAsync("https://api.vk.com/method/messages.send", content).Result; + HttpResponseMessage response = _httpClient.PostAsync(_apiBaseUrl + "messages.send", content).Result; string responseContent = response.Content.ReadAsStringAsync().Result; if (responseContent.Contains("\"error\"")) @@ -336,7 +614,7 @@ private void SendToEachUser(string messageText, List userIds) StringContent content = new StringContent(postData, System.Text.Encoding.UTF8, "application/x-www-form-urlencoded"); - HttpResponseMessage response = _httpClient.PostAsync("https://api.vk.com/method/messages.send", content).Result; + HttpResponseMessage response = _httpClient.PostAsync(_apiBaseUrl + "messages.send", content).Result; string responseContent = response.Content.ReadAsStringAsync().Result; if (responseContent.Contains("\"error\"")) @@ -401,37 +679,70 @@ private void PollAndHandleUpdatesAsync() try { - string getLongPollServerUrl = $"https://api.vk.com/method/messages.getLongPollServer?" + - $"access_token={AccessToken}" + - $"&v=5.131"; + if (string.IsNullOrEmpty(_longPollKey)) + { + string getLongPollServerUrl = _apiBaseUrl + "messages.getLongPollServer?" + + $"access_token={AccessToken}" + + $"&v=5.131"; - HttpResponseMessage response = _httpClient.GetAsync(getLongPollServerUrl).GetAwaiter().GetResult(); - string responseContent = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + HttpResponseMessage response = _httpClient.GetAsync(getLongPollServerUrl).GetAwaiter().GetResult(); + string responseContent = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - dynamic longPollServer = JsonConvert.DeserializeObject(responseContent); + dynamic longPollServer = JsonConvert.DeserializeObject(responseContent); - if (longPollServer?.response == null) - { - Thread.Sleep(5000); - continue; + if (longPollServer?.response == null) + { + Thread.Sleep(5000); + continue; + } + + string server = longPollServer.response.server; + _longPollKey = longPollServer.response.key; + _lastUpdateTs = longPollServer.response.ts; + _longPollServerUrl = server; } - string server = longPollServer.response.server; - string key = longPollServer.response.key; - long ts = longPollServer.response.ts; + string serverUrl = _longPollServerUrl; - if (!server.StartsWith("http://") && !server.StartsWith("https://")) + if (!serverUrl.StartsWith("http://") && !serverUrl.StartsWith("https://")) { - server = "https://" + server; + serverUrl = "https://" + serverUrl; } - string longPollUrl = $"{server}?act=a_check&key={key}&ts={ts}&wait=25&mode=2&version=3"; + string longPollUrl = $"{serverUrl}?act=a_check&key={_longPollKey}&ts={_lastUpdateTs}&wait=25&mode=2&version=3"; HttpResponseMessage longPollResponse = _httpClient.GetAsync(longPollUrl).GetAwaiter().GetResult(); string longPollContent = longPollResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult(); dynamic updates = JsonConvert.DeserializeObject(longPollContent); + // Обработка ошибок Long Poll (https://dev.vk.com/ru/api/user-long-poll/getting-started) + if (updates?.failed != null) + { + int failed = (int)updates.failed; + + if (failed == 1) + { + // История устарела — обновляем ts и продолжаем + _lastUpdateTs = updates.ts; + continue; + } + + if (failed == 4) + { + // Недопустимый номер версии — перезапрос сервера не поможет + ServerMaster.SendNewLogMessage( + $"VK Long Poll: invalid version. Response: {longPollContent}", + LogMessageType.Error); + Thread.Sleep(60000); + continue; + } + + // failed == 2 (key expired) или 3 — перезапрашиваем сервер + _longPollKey = null; + continue; + } + if (updates?.updates != null) { foreach (var update in updates.updates) @@ -466,7 +777,7 @@ private void PollAndHandleUpdatesAsync() if (updates?.ts != null) { - _lastUpdateId = updates.ts; + _lastUpdateTs = updates.ts; } Thread.Sleep(500); @@ -588,60 +899,6 @@ private string CheckString(string str) #region Helper - public class VkLongPollServerResponse - { - public VkLongPollServer response { get; set; } - } - - public class VkLongPollServer - { - public string key { get; set; } - public string server { get; set; } - public long ts { get; set; } - } - - public class VkLongPollUpdates - { - public long ts { get; set; } - public List updates { get; set; } - } - - public class VkLongPollUpdate - { - public int type { get; set; } - public object @object { get; set; } // Может быть разным в зависимости от type - - // Для type = 4 (новое сообщение) - public VkLongPollMessage message => GetMessage(); - - private VkLongPollMessage GetMessage() - { - if (type == 4 && @object != null) - { - var objDict = @object as Newtonsoft.Json.Linq.JObject; - if (objDict != null) - { - var msg = objDict["message"]?.ToObject(); - if (msg != null) return msg; - - // Альтернативный формат: некоторые версии API возвращают message напрямую - return objDict.ToObject(); - } - } - return null; - } - } - - public class VkLongPollMessage - { - public long id { get; set; } - public long date { get; set; } - public long from_id { get; set; } - public long peer_id { get; set; } - public string text { get; set; } - public int @out { get; set; } - } - /// /// Commands sent to the bot /// - Посылаемые боту команды diff --git a/project/OsEngine/Logging/ServerVkUi.xaml b/project/OsEngine/Logging/ServerVkUi.xaml index 539a45767d..d57165f392 100644 --- a/project/OsEngine/Logging/ServerVkUi.xaml +++ b/project/OsEngine/Logging/ServerVkUi.xaml @@ -6,12 +6,13 @@ WindowStartupLocation="CenterScreen" Icon="/Images/OsLogo.ico" Topmost="True">