using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using Serilog; using StackExchange.Redis; using System.IO.Hashing; using System.Text; using System.Timers; using Telegram.Bot; using Telegram.Bot.Polling; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.ReplyMarkups; using Timer = System.Timers.Timer; namespace nsszcontactbot; public class Program { private static ITelegramBotClient Bot = default!; private static List Employees = []; private static List Departments = []; private static IConfigurationRoot config = default!; private static FavoriteContactsManager FavoritesManager = default!; private static PhotosManager PhotosManager = default!; //private static ILogger Logger; public static async Task Main(string[] args) { config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(config).CreateLogger(); var botToken = config["Telegram:BotToken"]; if (botToken == null) { Log.Error("Токен Telegram-бота не указан в конфигурации."); return 1; } ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(config["Redis:Configuration"]!, (conf) => { conf.User = config["Redis:User"]; conf.Password = config["Redis:Password"]; }); FavoritesManager = new FavoriteContactsManager(redis); PhotosManager = new PhotosManager(redis, config["Paths:PhotoBasePath"]!); Bot = new TelegramBotClient(botToken); List cmds = []; await Bot.SetMyCommands(cmds); if (!LoadEmployees()) { return 1; } StartUpdatingEmployeesData(15 * 60 * 1000); // обновлять данные каждые 15 минут var me = await Bot.GetMe(); Log.Information("Бот запущен: @{Username}", me.Username); // Запуск обработки обновлений var cancellationToken = new CancellationTokenSource().Token; ReceiverOptions receiverOptions = new() { AllowedUpdates = Array.Empty() // receive all update types except ChatMember related updates }; Bot.StartReceiving( HandleUpdateAsync, HandleErrorAsync, receiverOptions: receiverOptions, cancellationToken: cancellationToken ); StartBirthdayNotifications(); Console.WriteLine("Нажмите Enter для выхода..."); Console.ReadLine(); return 0; } private static async Task HandleUpdateAsync( ITelegramBotClient botClient, Update update, CancellationToken cancellationToken ) { if (update.Type == UpdateType.Message && update.Message?.Text != null) { var message = update.Message; Log.Debug("Входящий текст (From=\"{Username}\", MessageId=\"{MessageId}\"): {Text}", message.From?.ToString() ?? "", message.Id, message.Text); if (message.Text.StartsWith("/start")) { await StartCommand(message); } else if (message.Text.EndsWith("Подразделения")) { await HandleShowDepartments(message); } else if (message.Text.EndsWith("Избранные")) { await HandleShowFavorites(message); } else { await HandleSearchInput(message); } } else if (update.Type == UpdateType.CallbackQuery) { string data = update.CallbackQuery!.Data ?? string.Empty; var callbackQuery = update.CallbackQuery!; Log.Debug("Нажатие кнопки (From=\"{Username}\", CallbackQueryId=\"{CallbackQueryId}\"): {Data}", callbackQuery.From?.ToString() ?? "", callbackQuery.Id, data); string[] dataParts = data.Split('/'); if (dataParts.Length > 0) { switch (dataParts[0]) { case "addcontact": { await HandleAddContact(update.CallbackQuery, dataParts[1]); break; } case "addfavorite": { if (dataParts.Length != 2) break; await HandleAddFavorite(update.CallbackQuery, dataParts[1]); break; } case "delfavorite": { if (dataParts.Length != 2) break; await HandleDelFavorite(update.CallbackQuery, dataParts[1]); break; } case "showdepartment": { if (dataParts.Length != 2) break; await HandleShowDepartment(update.CallbackQuery, int.Parse(dataParts[1])); break; } case "showemployee": { if (dataParts.Length != 2) break; await HandleShowEmployee(update.CallbackQuery, dataParts[1]); break; } case "page": { if (dataParts.Length < 3) break; var arg = dataParts.Length == 3 ? dataParts[2] : dataParts[2] + "/" + dataParts[3]; await HandlePageNavigation(update.CallbackQuery, int.Parse(dataParts[1]), arg); break; } case "close": { await HandleClose( update.CallbackQuery, dataParts.Length > 1 ? dataParts[1] : null ); break; } default: break; } } } } private static async Task StartCommand(Message message) { string text = "Привет! Я бот для поиска контактов сотрудников. Введите критерии поиска или воспользуйтесь кнопками внизу.\n"; await Bot.SendMessage(message.Chat.Id, text, replyMarkup: new string[] { "❤️ Избранные", "🏢 Подразделения" }); } private static async Task HandleShowDepartments(Message message) { Departments = [.. Employees.OrderBy(e => e.DepartmentCode).Select(e => e.Department).Distinct()]; await ShowDepartments(message.Chat.Id); } private static async Task ShowDepartments(long chatId, int page = 1, int pageSize = 10) { if (Departments.Count == 0) { await Bot.SendMessage(chatId, "Подразделения не найдены."); return; } var totalPages = (int)Math.Ceiling((double)Departments.Count / pageSize); var pagedDepartments = Departments.Skip((page - 1) * pageSize).Take(pageSize).ToList(); string text = $"Выберите подразделение (страница {page}/{totalPages}):"; var keyboardMarkup = new InlineKeyboardMarkup(); var departmentIndex = (page - 1) * pageSize; foreach (var department in pagedDepartments) { keyboardMarkup .AddNewRow() .AddButton($"{department}", $"showdepartment/{departmentIndex++}"); } if (totalPages > 1) { var navigationRow = keyboardMarkup.AddNewRow(); navigationRow.AddPaginator(page, totalPages, "departments"); } //keyboardMarkup.AddCloseButton(); await Bot.SendMessage(chatId, text, replyMarkup: keyboardMarkup); await Task.Delay(999); } class EmployeeComparer : IComparer { public int Compare(Employee? a, Employee? b) { if (a == null || b == null) return 0; // Сортировка по DepartmentCode int cd = CompareDepartmentCode(a.DepartmentCode, b.DepartmentCode); if (cd != 0) return cd; // Сортировка по Category int cc = a.Category.CompareTo(b.Category); if (cc != 0) return cc; // Сортировка по Position (с учетом пустых значений) int cp = string.Compare(a.Position ?? "", b.Position ?? "", StringComparison.OrdinalIgnoreCase); if (cp != 0) return cp; // Сортировка по FullName return string.Compare(a.FullName, b.FullName, StringComparison.OrdinalIgnoreCase); } private static int CompareDepartmentCode(string codeA, string codeB) { if (int.TryParse(codeA, out int a) && int.TryParse(codeB, out int b)) { return a.CompareTo(b); } return string.Compare(codeA, codeB, StringComparison.OrdinalIgnoreCase); } } private static async Task HandleSearchInput(Message message) { if (!string.IsNullOrEmpty(message.Text)) { string query = message.Text.Trim().ToLower(); if (query.Length < 4) { await Bot.SendMessage( message.Chat.Id, "Минимальная длина строки поиска - 4 символа." ); return; } // Поиск сотрудников по полям var queries = query.Split(' ', StringSplitOptions.RemoveEmptyEntries); var results = Employees .Where(e => queries.All(q => { var res = e.FullName.Contains(q, StringComparison.CurrentCultureIgnoreCase) || (e.Mail != null && e.Mail.Contains(q, StringComparison.CurrentCultureIgnoreCase)) || (e.Phone != null && e.Phone.Contains(q)) || (e.Mobile != null && e.Mobile.Contains(q)) || (e.IP != null && e.IP.Contains(q)); if (queries.Length == 1) return res; return res || (e.Department != null && e.Department.Contains(q, StringComparison.CurrentCultureIgnoreCase)) || (e.Position != null && e.Position.Contains(q, StringComparison.CurrentCultureIgnoreCase)); } )) .Order(new EmployeeComparer()) .ToList(); // Отправка результатов поиска await ShowEmployeeResults(message, results, null); } } private static async Task HandleShowFavorites(Message message) { if (!string.IsNullOrEmpty(message.Text)) { var ids = await FavoritesManager.GetAllFavoritesAsync(message.From!.Id); // Поиск сотрудников по полям var results = Employees .Where( e => ids.Contains(e.Id) ) .OrderBy(e => e.FullName) //.Order(new EmployeeComparer()) .ToList(); // Отправка результатов поиска await ShowEmployeeResults(message, results, true); } } private static async Task HandleAddContact(CallbackQuery callbackQuery, string id) { if (callbackQuery.Message != null) { var employee = Employees.First(e => e.Id == id); if (employee != null) { EmployeeContact contact = new(employee); string phoneNumber = !string.IsNullOrEmpty(contact.Mobile) ? contact.Mobile : contact.Phone; var replyParameters = new ReplyParameters() { MessageId = callbackQuery.Message.MessageId, QuoteParseMode = ParseMode.Html, Quote = $"{contact.FullName}" }; var keyboardMarkup = new InlineKeyboardMarkup() //.AddNewRow() .AddCloseButton(); await Bot.SendContact( callbackQuery.Message.Chat.Id, phoneNumber, contact.FirstName, lastName: contact.LastName, vcard: contact.VCard, replyParameters: replyParameters, replyMarkup: keyboardMarkup, protectContent: false ); await Task.Delay(999); } } await Bot.AnswerCallbackQuery(callbackQuery.Id); } private static async Task HandleAddFavorite(CallbackQuery callbackQuery, string contactId) { if (callbackQuery.Message != null) { var employee = Employees.First(e => e.Id == contactId); if (employee != null) { await FavoritesManager.AddFavoriteAsync(callbackQuery.From.Id, contactId); foreach (var row in callbackQuery.Message!.ReplyMarkup!.InlineKeyboard) foreach (var cell in row) { if (cell.CallbackData!.Contains("addfavorite")) { cell.CallbackData = $"delfavorite/{contactId}"; cell.Text = "💔 Не избранный"; } } await Bot.EditMessageReplyMarkup(callbackQuery.Message.Chat.Id, callbackQuery.Message.MessageId, callbackQuery.Message.ReplyMarkup); await Bot.AnswerCallbackQuery(callbackQuery.Id, "✔️ Контакт добавлен в избранные"); } } } private static async Task HandleDelFavorite(CallbackQuery callbackQuery, string contactId) { if (callbackQuery.Message != null) { var employee = Employees.First(e => e.Id == contactId); if (employee != null) { await FavoritesManager.RemoveFavoriteAsync(callbackQuery.From.Id, contactId); foreach (var row in callbackQuery.Message.ReplyMarkup!.InlineKeyboard) foreach (var cell in row) { if (cell.CallbackData!.Contains("delfavorite")) { cell.CallbackData = $"addfavorite/{contactId}"; cell.Text = "❤️ В избранные"; } } await Bot.EditMessageReplyMarkup(callbackQuery.Message.Chat.Id, callbackQuery.Message.MessageId, callbackQuery.Message.ReplyMarkup); await Bot.AnswerCallbackQuery(callbackQuery.Id, "✔️ Контакт удален из избранных"); } } } private static async Task HandleShowDepartment(CallbackQuery callbackQuery, int departmentIndex) { await Bot.AnswerCallbackQuery(callbackQuery.Id); if (callbackQuery.Message != null) { await Bot.DeleteMessage( callbackQuery.Message.Chat.Id, callbackQuery.Message.MessageId ); var employees = Employees .Where(e => e.Department == Departments[departmentIndex]) //.OrderBy(e => e.FullName) .Order(new EmployeeComparer()) .ToList(); if (employees != null) { await ShowEmployeeResults(callbackQuery.Message, employees, null); } } //await Bot.AnswerCallbackQueryAsync(callbackQuery.Id); } private static async Task HandleShowEmployee(CallbackQuery callbackQuery, string id) { if (callbackQuery.Message != null) { var employee = Employees.First(e => e.Id == id); if (employee != null) { var isFavorite = await FavoritesManager.IsFavoriteAsync(callbackQuery.From.Id, id); await ShowEmployeeCard(callbackQuery.Message.Chat.Id, employee, isFavorite, closeButton: true); } } await Bot.AnswerCallbackQuery(callbackQuery.Id); } private static async Task HandlePageNavigation(CallbackQuery callbackQuery, int page, string context) { if (callbackQuery.Message != null) { await Bot.DeleteMessage(callbackQuery.Message.Chat.Id, callbackQuery.Message.MessageId); var messageHash = (callbackQuery.Message.Chat.Id << 32) | (uint)callbackQuery.Message.Id; if (context.Contains("employees")) { long hash = long.Parse(context.Split('/')[1]); var results = _resultsCache[hash]; await ShowEmployeeResults(callbackQuery.Message, results, null, page); //_resultsCache.Remove(messageHash); } else if (context == "departments") { await ShowDepartments(callbackQuery.Message.Chat.Id, page); } } await Bot.AnswerCallbackQuery(callbackQuery.Id); } private static async Task HandleClose(CallbackQuery callbackQuery, string? nextAction) { if (callbackQuery.Message != null) { await Bot.DeleteMessage( callbackQuery.Message.Chat.Id, callbackQuery.Message.MessageId ); await Task.Delay(500); } await Bot.AnswerCallbackQuery(callbackQuery.Id); } private static async Task ShowEmployeeCard(long chatId, Employee employee, bool isFavorite = false, bool contactButton = true, bool favoriteButton = true, bool closeButton = false) { { var keyboardMarkup = new InlineKeyboardMarkup(); var keyboardRow = keyboardMarkup.AddNewRow(); if (contactButton) keyboardRow.AddButton("👤 Контакт", $"addcontact/{employee.Id}"); if (favoriteButton) { if (isFavorite) { keyboardRow.AddButton("💔 Не избранный", $"delfavorite/{employee.Id}"); } else { keyboardRow.AddButton("❤️ В избранные", $"addfavorite/{employee.Id}"); } } if (closeButton) keyboardRow.AddCloseButton(); var card = CreateEmployeeCard(employee); var photo = await PhotosManager.GetEmployeePhotoAsync(employee); if (photo != null) { var msg = await Bot.SendPhoto( chatId, photo, caption: card, parseMode: ParseMode.Html, replyMarkup: keyboardMarkup ); if (photo.FileType == FileType.Stream) { FileStream? stream = (photo as InputFileStream)!.Content as FileStream; stream?.Dispose(); await PhotosManager.CacheEmployeePhotoAsync(employee, msg.Photo!.FirstOrDefault()!.FileId); // TODO test ! //await PhotosManager.CacheEmployeePhotoAsync(employee, msg.Photo.FirstOrDefault().FileId); } } else { await Bot.SendMessage( chatId, card, ParseMode.Html, replyMarkup: keyboardMarkup, protectContent: false ); } await Task.Delay(999); } } private static Dictionary> _resultsCache = []; private static async Task ShowEmployeeResults(Message message, List results, bool? isFavorite = false, int page = 1, int pageSize = 10) { if (results.Count != 0) { if (results.Count <= pageSize) { foreach (var employee in results) { bool bFavorite = isFavorite.HasValue ? isFavorite == true : await FavoritesManager.IsFavoriteAsync(message.Chat.Id, employee.Id); await ShowEmployeeCard(message.Chat.Id, employee, bFavorite); } } else { var totalPages = (int)Math.Ceiling((double)results.Count / pageSize); var pagedResults = results.Skip((page - 1) * pageSize).Take(pageSize).ToList(); string text = $"Найдено {results.Count} сотрудников (страница {page}/{totalPages}):"; var keyboardMarkup = new InlineKeyboardMarkup(); foreach (var employee in pagedResults) { var title = CreateEmployeeTitle(employee); keyboardMarkup .AddNewRow() .AddButton(title, $"showemployee/{employee.Id}"); } if (totalPages > 1) { var messageHash = results.GetHashCode();// (message.Chat.Id << 32) | (uint)message.Id; _resultsCache[messageHash] = results; var navigationRow = keyboardMarkup.AddNewRow(); navigationRow.AddPaginator(page, totalPages, $"employees/{messageHash}"); } //keyboardMarkup.AddCloseButton(); await Bot.SendMessage(message.Chat.Id, text, replyMarkup: keyboardMarkup, protectContent: false); await Task.Delay(999); } } else { await Bot.SendMessage(message.Chat.Id, "🔍 Извините, мы ничего не нашли."); await Task.Delay(999); } } static string GetEventStatus(Employee employee) { string result = ""; if (employee.Birthday) { result += "🎂 Празднует день рождения\n"; } if (employee.StartDate == null || employee.EndDate == null) { return result; } DateTime today = DateTime.Now; TimeSpan oneWeekBeforeEvent = (DateTime)employee.StartDate - today; if (employee.StartDate <= today && today <= employee.EndDate) { // Сотрудник участвует в событии result += $"🟥 {employee.State} с {employee.StartDate:dd.MM.yyyy} по {employee.EndDate:dd.MM.yyyy}"; } else if (oneWeekBeforeEvent.TotalDays <= 7 && oneWeekBeforeEvent.TotalDays > 0) { // Событие начнется через неделю result += $"🟩 {employee.State} с {employee.StartDate:dd.MM.yyyy} по {employee.EndDate:dd.MM.yyyy}"; } return result; // Если событие не указано или неактуально //return string.Empty; } private static string CreateEmployeeTitle(Employee employee) { EmployeeContact contact = new(employee); string prefix = string.Empty; if (employee.StartDate != null || employee.EndDate != null) { DateTime today = DateTime.Now; TimeSpan oneWeekBeforeEvent = (DateTime)employee.StartDate! - today; if (employee.StartDate <= today && today <= employee.EndDate) { // Сотрудник участвует в событии prefix += "🟥"; } else if (oneWeekBeforeEvent.TotalDays <= 7 && oneWeekBeforeEvent.TotalDays > 0) { // Событие начнется через неделю prefix += "🟩"; } } if (employee.Birthday) { prefix += "🎂"; } if (string.IsNullOrEmpty(prefix)) { //prefix = "👤 "; } else { prefix += " "; } return $"{prefix}{contact.ShortName}"; } private static string CreateEmployeeCard(Employee employee) { EmployeeContact contact = new(employee); string card = $"👤 {contact.FullName}\n" + $"🏢 {contact.Department}\n"; if (!string.IsNullOrEmpty(contact.Position)) card += $"🎓 {contact.Position}\n"; if (!string.IsNullOrEmpty(contact.Mail)) card += $"✉️ {contact.Mail}\n"; if (!string.IsNullOrEmpty(contact.Mobile)) card += $"📱 {contact.Mobile}\n"; if (!string.IsNullOrEmpty(contact.ExtensionNumber)) card += $"📞 {contact.ExtensionNumber}\n"; if (!string.IsNullOrEmpty(employee.IP)) card += $"🖥 {employee.IP}\n"; card += GetEventStatus(employee); return card; } private static Timer? updateTimer = null; private static void StartUpdatingEmployeesData(int intervalMs = 1 * 60 * 60 * 1000) { if (updateTimer != null) return; updateTimer = new Timer(intervalMs); updateTimer.Elapsed += UpdateTimer_Elapsed; updateTimer.AutoReset = true; // Запускать снова после завершения updateTimer.Enabled = true; } private static void UpdateTimer_Elapsed(object? sender, ElapsedEventArgs e) { LoadEmployees(); } private static string prevHash = string.Empty; private static bool LoadEmployees() { try { string filePath = config["Paths:ContactsJsonPath"] ?? string.Empty; if (!string.IsNullOrEmpty(filePath)) { string json; if (filePath.StartsWith("http", StringComparison.InvariantCultureIgnoreCase)) { using var client = new HttpClient(); using HttpResponseMessage response = client.GetAsync(filePath).Result; using HttpContent content = response.Content; json = content.ReadAsStringAsync().Result; } else { json = File.ReadAllText(filePath); } // Загрузка данных сотрудников из JSON файла при наличии изменений var newHash = CalculateHash(json); if (prevHash == newHash) { return true; } prevHash = newHash; Employees = JsonConvert.DeserializeObject>(json)!; var backupFilename = BackupContactsJSON(json); if (!string.IsNullOrEmpty(backupFilename)) { Log.Debug("Данные обновлены, создана резервная копия: {BackupFilename}", backupFilename); } } else { Employees.Clear(); } filePath = config["Paths:PersistentContactsJsonPath"] ?? string.Empty; if (!string.IsNullOrEmpty(filePath)) { var json = File.ReadAllText(filePath); Employees.AddRange(JsonConvert.DeserializeObject>(json)!); } if (Employees.Count > 0) { Log.Information("Загружены данные о {Count} сотрудниках", Employees.Count); return true; } Log.Error("Нет данных о сотрудниках"); return false; } catch (Exception ex) { Log.Error(ex, "Ошибка при загрузке списка сотрудников"); return false; } } private static string CalculateHash(string data) { byte[] hashBytes = Crc32.Hash(Encoding.UTF8.GetBytes(data)); return Convert.ToHexString(hashBytes); } private static string BackupContactsJSON(string json) { ArgumentException.ThrowIfNullOrEmpty(json, nameof(json)); var backupDirectory = config["Paths:ContactsBackupDirectory"] ?? string.Empty; if (!string.IsNullOrEmpty(backupDirectory)) { if (!Directory.Exists(backupDirectory)) { Directory.CreateDirectory(backupDirectory); } var hashString = CalculateHash(json).ToUpperInvariant(); var filename = Path.GetFullPath(Path.Combine(backupDirectory, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{Employees.Count}_contacts_{hashString}.json")); File.WriteAllText(filename, json); return filename; } return string.Empty; } private static void StartBirthdayNotifications() { var timer = new Timer(TimeSpan.FromHours(24).TotalMilliseconds); timer.Elapsed += async (sender, e) => { var today = DateTime.Today; var birthdayEmployees = Employees.Where(e => e.Birthday).ToList(); foreach (var employee in birthdayEmployees) { var users = await FavoritesManager.GetUsersWithFavoriteAsync(employee.Id); foreach (var userId in users) { await Bot.SendMessage(userId, $"🎂 Сегодня день рождения у {employee.FullName}!"); await Task.Delay(999); } } }; timer.AutoReset = true; timer.Start(); } private static Task HandleErrorAsync( ITelegramBotClient botClient, Exception exception, CancellationToken cancellationToken ) { Log.Error(exception, "Произошла ошибка"); return Task.CompletedTask; } }