From 34efa36d8bf258f81255b189f812621220148aaf Mon Sep 17 00:00:00 2001 From: Oleg-Nevsky Date: Tue, 28 Apr 2026 19:07:34 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D1=84=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=83?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=82=D0=B0=D0=BA=D1=82=D0=B0=D0=BC=D0=B8=20=D0=B8?= =?UTF-8?q?=20=D1=84=D0=BE=D1=82=D0=BE=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D1=8F?= =?UTF-8?q?=D0=BC=D0=B8=20=D1=81=D0=BE=D1=82=D1=80=D1=83=D0=B4=D0=BD=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2,=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D0=BA=D1=82=D0=BE=D0=B2,=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=83=D0=B2=D0=B5=D0=B4?= =?UTF-8?q?=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BE=20=D0=B4?= =?UTF-8?q?=D0=BD=D1=8F=D1=85=20=D1=80=D0=BE=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=81=D0=BE=D1=82=D1=80=D1=83=D0=B4=D0=BD=D0=B8=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 + Employee.cs | 1 + EmployeeContact.cs | 3 + FavoriteContactsManager.cs | 39 ++- InlineKeyboardMarkupExtensions.cs | 29 ++- PersistentContacts.json | 115 +++++++++ PhotosManager.cs | 48 ++++ Program.cs | 396 ++++++++++++++++++++++-------- appsettings.json | 20 +- nsszcontactbot.csproj | 5 +- run.sh | 3 + 11 files changed, 527 insertions(+), 135 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 PersistentContacts.json create mode 100644 PhotosManager.cs create mode 100644 run.sh diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4ec39be --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.enabled": false +} \ No newline at end of file diff --git a/Employee.cs b/Employee.cs index d780807..d05cc0c 100644 --- a/Employee.cs +++ b/Employee.cs @@ -9,6 +9,7 @@ public class Employee public string Department { get; set; } = string.Empty; // Название отдела public string DepartmentCode { get; set; } = string.Empty; // Код отдела public string Position { get; set; } = string.Empty; // Должность + public bool Birthday { get; set; } = false; public string? Phone { get; set; } // Опциональное поле для телефона public string? Mobile { get; set; } // Опциональное поле для хранения номера мобильного телефона public string? Mail { get; set; } // Опциональное поле для электронной почты diff --git a/EmployeeContact.cs b/EmployeeContact.cs index f6b2dc5..068f8d6 100644 --- a/EmployeeContact.cs +++ b/EmployeeContact.cs @@ -12,10 +12,12 @@ public class EmployeeContact { LastName = nameParts[0]; FirstName = nameParts[1]; + ShortName = $"{LastName} {FirstName}"; } else { FirstName = FullName; + ShortName = FullName; LastName = string.Empty; } Department = employee.Department; @@ -60,6 +62,7 @@ public class EmployeeContact } } public string FullName { get; set; } // Полное имя сотрудника + public string ShortName { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Department { get; set; } // Название отдела diff --git a/FavoriteContactsManager.cs b/FavoriteContactsManager.cs index 1e8cf44..d2bce23 100644 --- a/FavoriteContactsManager.cs +++ b/FavoriteContactsManager.cs @@ -1,27 +1,30 @@ using StackExchange.Redis; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; + namespace nsszcontactbot; public class FavoriteContactsManager { private readonly IDatabase _redisDb; - private const string KeyPrefix = "user:favorites:"; public FavoriteContactsManager(ConnectionMultiplexer redis) { _redisDb = redis.GetDatabase(); } + private static string GetUserKey(long userId) => $"user:favorites:{userId}"; + private static string GetContactKey(string contactId) => $"contact:favorites:{contactId}"; + /// /// Добавить контакт в избранное. /// public async Task AddFavoriteAsync(long userId, string contactId) { - string key = GetRedisKey(userId); - await _redisDb.SetAddAsync(key, contactId); + string userKey = GetUserKey(userId); + string contactKey = GetContactKey(contactId); + await Task.WhenAll( + _redisDb.SetAddAsync(userKey, contactId), + _redisDb.SetAddAsync(contactKey, userId.ToString()) + ); } /// @@ -29,8 +32,12 @@ public class FavoriteContactsManager /// public async Task RemoveFavoriteAsync(long userId, string contactId) { - string key = GetRedisKey(userId); - await _redisDb.SetRemoveAsync(key, contactId); + string userKey = GetUserKey(userId); + string contactKey = GetContactKey(contactId); + await Task.WhenAll( + _redisDb.SetRemoveAsync(userKey, contactId), + _redisDb.SetRemoveAsync(contactKey, userId.ToString()) + ); } /// @@ -38,7 +45,7 @@ public class FavoriteContactsManager /// public async Task IsFavoriteAsync(long userId, string contactId) { - string key = GetRedisKey(userId); + string key = GetUserKey(userId); return await _redisDb.SetContainsAsync(key, contactId); } @@ -47,10 +54,16 @@ public class FavoriteContactsManager /// public async Task> GetAllFavoritesAsync(long userId) { - string key = GetRedisKey(userId); + string key = GetUserKey(userId); var members = await _redisDb.SetMembersAsync(key); - return members.Select(m => m.ToString()).ToList(); + return [.. members.Select(m => m.ToString())]; + } + + public async Task> GetUsersWithFavoriteAsync(string contactId) + { + string contactKey = GetContactKey(contactId); + var members = await _redisDb.SetMembersAsync(contactKey); + return [.. members.Select(m => long.Parse(m.ToString()))]; } - private string GetRedisKey(long userId) => $"{KeyPrefix}{userId}"; } diff --git a/InlineKeyboardMarkupExtensions.cs b/InlineKeyboardMarkupExtensions.cs index 75ad520..50a6c7f 100644 --- a/InlineKeyboardMarkupExtensions.cs +++ b/InlineKeyboardMarkupExtensions.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Telegram.Bot.Types.ReplyMarkups; +using Telegram.Bot.Types.ReplyMarkups; namespace nsszcontactbot { @@ -14,5 +9,27 @@ namespace nsszcontactbot var buttonRow = newRow ? markup.AddNewRow() : markup; return buttonRow.AddButton("❌ Закрыть", "close"); } + + internal static InlineKeyboardMarkup AddPrevPageButton(this InlineKeyboardMarkup markup, int currentPage, string context = "", string text = "⬅️ Назад") + { + return markup.AddButton(text, $"page/{currentPage - 1}{(string.IsNullOrEmpty(context) ? "" : "/")}{context}"); + } + + internal static InlineKeyboardMarkup AddNextPageButton(this InlineKeyboardMarkup markup, int currentPage, string context = "", string text = "Вперед ➡️") + { + return markup.AddButton(text, $"page/{currentPage + 1}{(string.IsNullOrEmpty(context) ? "" : "/")}{context}"); + } + + internal static InlineKeyboardMarkup AddPaginator(this InlineKeyboardMarkup markup, int currentPage, int totalPages, string context = "", string textPrev = "⬅️ Назад", string textNext = "Вперед ➡️") + { + if (totalPages > 1) + { + if (currentPage > 1) + AddPrevPageButton(markup, currentPage, context, textPrev); + if (currentPage < totalPages) + AddNextPageButton(markup, currentPage, context, textNext); + } + return markup; + } } } diff --git a/PersistentContacts.json b/PersistentContacts.json new file mode 100644 index 0000000..a462d05 --- /dev/null +++ b/PersistentContacts.json @@ -0,0 +1,115 @@ +[ + { + "id": "cp", + "fullName": "Центральная проходная", + "department": "Проходные", + "departmentCode": 8888, + "position": "", + "phone": "8057", + "category": 1 + }, + { + "id": "tp", + "fullName": "Транспортная проходная", + "department": "Проходные", + "departmentCode": 8888, + "position": "", + "phone": "8059", + "category": 2 + }, + { + "id": "po701", + "fullName": "Пост охраны проект 701", + "department": "Посты охраны", + "departmentCode": 9999, + "position": "", + "phone": "8443", + "category": 2 + }, + { + "id": "po702", + "fullName": "Пост охраны проект 702", + "department": "Посты охраны", + "departmentCode": 9999, + "position": "", + "phone": "8168", + "category": 2 + }, + { + "id": "po402", + "fullName": "Пост охраны проект 402", + "department": "Посты охраны", + "departmentCode": 9999, + "position": "", + "phone": "8498", + "category": 2 + }, + { + "id": "po703", + "fullName": "Пост охраны проект 703", + "department": "Посты охраны", + "departmentCode": 9999, + "position": "", + "phone": "8169", + "category": 2 + }, + { + "id": "pov", + "fullName": "Пост охраны видеонаблюдения", + "department": "Посты охраны", + "departmentCode": 9999, + "position": "", + "phone": "8468", + "category": 2 + }, + { + "id": "mk", + "fullName": "Медицинский кабинет", + "department": "Дежурные службы", + "departmentCode": -1, + "position": "", + "mobile": "79312040016", + "phone": "8220", + "mail": "medkab@nssz.ru", + "category": 1 + }, + { + "id": "de", + "fullName": "Дежурный электрик", + "department": "Дежурные службы", + "departmentCode": -1, + "position": "", + "mobile": "79213973747", + "phone": "8143", + "mail": "medkab2@nssz.ru", + "category": 2 + }, + { + "id": "dves", + "fullName": "Дежурный ВЭС", + "department": "Дежурные службы", + "departmentCode": -1, + "position": "", + "phone": "8147", + "mobile": "79213279276", + "category": 2 + }, + { + "id": "dvtk", + "fullName": "Дежурный водоснабжения, теплоснабжения, канализации", + "department": "Дежурные службы", + "departmentCode": -1, + "position": "", + "phone": "8129", + "category": 2 + }, + { + "id": "durptm", + "fullName": "Дежурный по участку ремонта ПТМ", + "department": "Дежурные службы", + "departmentCode": -1, + "position": "", + "phone": "8142", + "category": 2 + } +] \ No newline at end of file diff --git a/PhotosManager.cs b/PhotosManager.cs new file mode 100644 index 0000000..9a5142a --- /dev/null +++ b/PhotosManager.cs @@ -0,0 +1,48 @@ +using StackExchange.Redis; +using Telegram.Bot.Types; + +namespace nsszcontactbot; + +public class PhotosManager(ConnectionMultiplexer redis, string PhotoBasePath) +{ + private readonly IDatabase _redisDb = redis.GetDatabase(); + private readonly string _basePath = PhotoBasePath; + private const string Key = "cache:photos"; + private const string DefaultPhotoFileId = "AgACAgIAAxkDAAIDTWgtK4jsuoRTb7C4p0uhfVAOvWy2AAK1-TEb3cloSdmbxB-nuRFCAQADAgADcwADNgQ"; + + public async Task CacheEmployeePhotoAsync(Employee employee, string fileId, TimeSpan ttl = default) + { + string entryName = GetEntryName(employee); + _redisDb.HashSetAsync(Key, [new(entryName, fileId)]).ConfigureAwait(false); + ttl = ttl == default ? TimeSpan.FromDays(30) : ttl; + _redisDb.HashFieldExpireAsync(Key, [new(entryName)], ttl).ConfigureAwait(false); + } + + public async Task GetEmployeePhotoAsync(Employee employee) + { + string entryName = GetEntryName(employee); + var val = await _redisDb.HashGetAsync(Key, entryName); + if (val != RedisValue.Null && val.HasValue) + { + return InputFile.FromFileId(val!); + } + else + { + string photoPath = Path.Combine(_basePath, $"{employee.Id}.jpg"); + photoPath = File.Exists(photoPath) ? photoPath : Path.Combine(_basePath, $"{employee.FullName}.jpg"); + + if (File.Exists(photoPath)) + { + var fileStream = new FileStream( + photoPath, + FileMode.Open, + FileAccess.Read + ); + return InputFile.FromStream(fileStream); + } + return InputFile.FromFileId(DefaultPhotoFileId);; + } + } + + private static string GetEntryName(Employee employee) => $"employee:{employee.Id}:fileid"; +} diff --git a/Program.cs b/Program.cs index 78c0219..39a0962 100644 --- a/Program.cs +++ b/Program.cs @@ -1,29 +1,25 @@ -using Newtonsoft.Json; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using Serilog; +using StackExchange.Redis; +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 Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Serilog; -using System.Timers; -using Serilog.Core; -using NRedisStack; -using NRedisStack.RedisStackCommands; - using Timer = System.Timers.Timer; -using StackExchange.Redis; namespace nsszcontactbot; public class Program { private static ITelegramBotClient Bot; - private static List Employees = new(); - private static List Departments = new(); + private static List Employees = []; + private static List Departments = []; private static IConfigurationRoot config; private static FavoriteContactsManager FavoritesManager; + private static PhotosManager PhotosManager; //private static ILogger Logger; public static async Task Main(string[] args) @@ -49,14 +45,15 @@ public class Program }); FavoritesManager = new FavoriteContactsManager(redis); + PhotosManager = new PhotosManager(redis, config["Paths:PhotoBasePath"]!); Bot = new TelegramBotClient(botToken); - List cmds = new(); + List cmds = []; await Bot.SetMyCommands(cmds); - if(!LoadEmployees()) + if (!LoadEmployees()) { return 1; } - StartUpdatingEmployeesData(); + StartUpdatingEmployeesData(15 * 60 * 1000); // обновлять данные каждые 15 минут var me = await Bot.GetMe(); @@ -76,6 +73,8 @@ public class Program cancellationToken: cancellationToken ); + StartBirthdayNotifications(); + Console.WriteLine("Нажмите Enter для выхода..."); Console.ReadLine(); return 0; @@ -96,11 +95,11 @@ public class Program { await StartCommand(message); } - else if (message.Text.EndsWith("Список подразделений")) + else if (message.Text.EndsWith("Подразделения")) { await ChooseDepartment(message); } - else if (message.Text.EndsWith("Избранные контакты")) + else if (message.Text.EndsWith("Избранные")) { await HandleShowFavorites(message); } @@ -111,8 +110,7 @@ public class Program } else if (update.Type == UpdateType.CallbackQuery) { - var message = update.CallbackQuery.Message; - string data = update.CallbackQuery.Data ?? string.Empty; + string data = update.CallbackQuery!.Data ?? string.Empty; string[] dataParts = data.Split('/'); if (dataParts.Length > 0) { @@ -155,7 +153,16 @@ public class Program 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( @@ -175,32 +182,80 @@ public class Program private static async Task StartCommand(Message message) { string text = "Привет! Я бот для поиска контактов сотрудников. Введите критерии поиска или воспользуйтесь кнопками внизу.\n"; - await Bot.SendMessage(message.Chat.Id, text, replyMarkup: new string[] { "⭐️ Избранные контакты", "🏢 Список подразделений" }); + await Bot.SendMessage(message.Chat.Id, text, replyMarkup: new string[] { "❤️ Избранные", "🏢 Подразделения" }); } private static async Task ChooseDepartment(Message message) { - Departments = Employees.Select(e => e.Department).Distinct().ToList(); + Departments = [.. Employees.OrderBy(e => e.DepartmentCode).Select(e => e.Department).Distinct()]; await ShowDepartments(message.Chat.Id); } - private static async Task ShowDepartments(long chatId) + private static async Task ShowDepartments(long chatId, int page = 1, int pageSize = 10) { - string text = "Выберите подразделение:"; + 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 = 0; - foreach (var department in Departments) + 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)) @@ -216,18 +271,25 @@ public class Program } // Поиск сотрудников по полям + var queries = query.Split(' ', StringSplitOptions.RemoveEmptyEntries); var results = Employees - .Where( - e => - e.FullName.ToLower().Contains(query) - || (e.Mail != null && e.Mail.ToLower().Contains(query)) - || (e.Phone != null && e.Phone.Contains(query)) - || (e.Mobile != null && e.Mobile.Contains(query)) - || (e.IP != null && e.IP.Contains(query)) - ) - .OrderBy(e => e.FullName.ToLower().IndexOf(query)) - .ToList(); + .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); } @@ -237,13 +299,14 @@ public class Program { if (!string.IsNullOrEmpty(message.Text)) { - var ids = await FavoritesManager.GetAllFavoritesAsync(message.From.Id); + 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(); // Отправка результатов поиска @@ -255,7 +318,7 @@ public class Program { if (callbackQuery.Message != null) { - var employee = Employees.Where(e => e.Id == id).First(); + var employee = Employees.First(e => e.Id == id); if (employee != null) { EmployeeContact contact = new(employee); @@ -279,7 +342,7 @@ public class Program vcard: contact.VCard, replyParameters: replyParameters, replyMarkup: keyboardMarkup, - protectContent: true + protectContent: false ); await Task.Delay(999); } @@ -291,14 +354,14 @@ public class Program { if (callbackQuery.Message != null) { - var employee = Employees.Where(e => e.Id == contactId).First(); + 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 row in callbackQuery.Message!.ReplyMarkup!.InlineKeyboard) foreach (var cell in row) { - if (cell.CallbackData.Contains("addfavorite")) + if (cell.CallbackData!.Contains("addfavorite")) { cell.CallbackData = $"delfavorite/{contactId}"; cell.Text = "💔 Не избранный"; @@ -315,7 +378,7 @@ public class Program { if (callbackQuery.Message != null) { - var employee = Employees.Where(e => e.Id == contactId).First(); + var employee = Employees.First(e => e.Id == contactId); if (employee != null) { await FavoritesManager.RemoveFavoriteAsync(callbackQuery.From.Id, contactId); @@ -347,7 +410,8 @@ public class Program var employees = Employees .Where(e => e.Department == Departments[departmentIndex]) - .OrderBy(e => e.FullName) + //.OrderBy(e => e.FullName) + .Order(new EmployeeComparer()) .ToList(); if (employees != null) { @@ -361,7 +425,7 @@ public class Program { if (callbackQuery.Message != null) { - var employee = Employees.Where(e => e.Id == id).First(); + var employee = Employees.First(e => e.Id == id); if (employee != null) { var isFavorite = await FavoritesManager.IsFavoriteAsync(callbackQuery.From.Id, id); @@ -371,6 +435,27 @@ public class Program 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) { @@ -410,65 +495,81 @@ public class Program var card = CreateEmployeeCard(employee); - string photoPath = Path.Combine(config["Paths:PhotoBasePath"]!, $"{employee.FullName}.jpg"); - try + var photo = await PhotosManager.GetEmployeePhotoAsync(employee); + + if (photo != null) { - await using var fileStream = new FileStream( - photoPath, - FileMode.Open, - FileAccess.Read - ); - - await Bot.SendPhoto( + var msg = await Bot.SendPhoto( chatId, - fileStream, + 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); + } } - catch (FileNotFoundException) + else { await Bot.SendMessage( chatId, card, ParseMode.Html, replyMarkup: keyboardMarkup, - protectContent: true + protectContent: false ); } - finally - { - await Task.Delay(999); - } + await Task.Delay(999); + } } - private static async Task ShowEmployeeResults(Message message, List results, bool? isFavorite = false) + private static Dictionary> _resultsCache = []; + + private static async Task ShowEmployeeResults(Message message, List results, bool? isFavorite = false, int page = 1, int pageSize = 10) { if (results.Any()) { - if (results.Count < 10) + if (results.Count <= pageSize) { foreach (var employee in results) { - bool bFavorite = isFavorite.HasValue ? isFavorite == true : await FavoritesManager.IsFavoriteAsync(message.From.Id, employee.Id); + bool bFavorite = isFavorite.HasValue ? isFavorite == true : await FavoritesManager.IsFavoriteAsync(message.Chat.Id, employee.Id); await ShowEmployeeCard(message.Chat.Id, employee, bFavorite); } } else { - string text = "Вот, что удалось найти:"; + 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 results) + foreach (var employee in pagedResults) { + var title = CreateEmployeeTitle(employee); keyboardMarkup .AddNewRow() - .AddButton(employee.FullName, $"showemployee/{employee.Id}"); + .AddButton(title, $"showemployee/{employee.Id}"); } - await Bot.SendMessage(message.Chat.Id, text, replyMarkup: keyboardMarkup, protectContent: true); + 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); } } @@ -481,9 +582,14 @@ public class Program static string GetEventStatus(Employee employee) { + string result = ""; + if (employee.Birthday) + { + result += "🎂 Празднует день рождения\n"; + } if (employee.StartDate == null || employee.EndDate == null) { - return string.Empty; + return result; } DateTime today = DateTime.Now; TimeSpan oneWeekBeforeEvent = (DateTime)employee.StartDate - today; @@ -491,16 +597,58 @@ public class Program if (employee.StartDate <= today && today <= employee.EndDate) { // Сотрудник участвует в событии - return $"🟥 {employee.State} с {employee.StartDate:dd.MM.yyyy} по {employee.EndDate:dd.MM.yyyy}"; + result += $"🟥 {employee.State} с {employee.StartDate:dd.MM.yyyy} по {employee.EndDate:dd.MM.yyyy}"; } else if (oneWeekBeforeEvent.TotalDays <= 7 && oneWeekBeforeEvent.TotalDays > 0) { // Событие начнется через неделю - return $"🟩 {employee.State} с {employee.StartDate:dd.MM.yyyy} по {employee.EndDate:dd.MM.yyyy}"; + result += $"🟩 {employee.State} с {employee.StartDate:dd.MM.yyyy} по {employee.EndDate:dd.MM.yyyy}"; } + return result; // Если событие не указано или неактуально - return string.Empty; + //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) @@ -508,8 +656,10 @@ public class Program EmployeeContact contact = new(employee); string card = $"👤 {contact.FullName}\n" - + $"🏢 {contact.Department}\n" - + $"🎓 {contact.Position}\n"; + + $"🏢 {contact.Department}\n"; + + if (!string.IsNullOrEmpty(contact.Position)) + card += $"🎓 {contact.Position}\n"; if (!string.IsNullOrEmpty(contact.Mail)) card += $"✉️ {contact.Mail}\n"; @@ -527,31 +677,6 @@ public class Program return card; } - private static string CreateEmployeeCard2(Employee employee) - { - string photoUrl = - $"https://contacts.int.nssz.ru/data/photos/{employee.FullName.Replace(" ", "%20")}.jpg"; // Формирование пути к фотографии - - string card = - $"👤 {employee.FullName}\n" - + $"🏢 Отдел: {employee.Department}\n" - + $"🎓 Должность: {employee.Position}\n" - + $"📸 Фото: Посмотреть фото\n"; // Добавление ссылки на фото - - if (!string.IsNullOrEmpty(employee.Mail)) - card += $"✉️ Почта: {employee.Mail}\n"; - if (!string.IsNullOrEmpty(employee.Phone)) - card += $"📞 Телефон: {employee.Phone}\n"; - if (!string.IsNullOrEmpty(employee.Mobile)) - card += $"📱 Мобильный: +{employee.Mobile}\n"; - if (!string.IsNullOrEmpty(employee.IP)) - card += $"🌐 IP: {employee.IP}\n"; - - card += GetEventStatus(employee); - - return card; - } - private static Timer? updateTimer = null; private static void StartUpdatingEmployeesData(int intervalMs = 1 * 60 * 60 * 1000) { @@ -572,23 +697,78 @@ public class Program { try { - // Загрузка данных сотрудников из JSON файла при наличии изменений - var json = System.IO.File.ReadAllText(config["Paths:ContactsJsonPath"]!); - int newHash = json.GetHashCode(); - if (prevHash == newHash) + 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 файла при наличии изменений + int newHash = json.GetHashCode(); + if (prevHash == newHash) + { + return true; + } + prevHash = newHash; + Employees = JsonConvert.DeserializeObject>(json)!; + } + 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; } - prevHash = newHash; - Employees = JsonConvert.DeserializeObject>(json)!; - Log.Information("Загружены данные о {Count} сотрудниках", Employees.Count); - return true; - } catch (Exception ex) { + Log.Error("Нет данных о сотрудниках"); + return false; + + } + catch (Exception ex) + { Log.Error(ex, "Ошибка при загрузке списка сотрудников"); return false; } } + 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, diff --git a/appsettings.json b/appsettings.json index ce0412f..17a287b 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,21 +1,27 @@ { "Paths": { - "ContactsJsonPath": "data/contacts.json", - "PhotoBasePath": "data/photos" + "ContactsJsonPath": "http://contacts.nssz.local/data/contacts.json", + "PersistentContactsJsonPath": "PersistentContacts.json", + "PhotoBasePath": "photos" }, "Telegram": { "BotToken": "8155021946:AAGbfJnLa0sTWxqFB9yjbXhbTedBw4wvcQE" }, "Redis": { - "Configuration": "redis-15840.crce198.eu-central-1-3.ec2.redns.redis-cloud.com:15840", - "User": "tgbot", - "Password": "0aCKMee9Ocq88KzfjWyC9AmB25tLiY93%" + "Configuration": "contacts.nssz.local:6379", + "User": "default", + "Password": "" }, "Serilog": { - "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File" + ], "MinimumLevel": "Debug", "WriteTo": [ - { "Name": "Console" }, + { + "Name": "Console" + }, { "Name": "File", "Args": { diff --git a/nsszcontactbot.csproj b/nsszcontactbot.csproj index 10e6652..4caf798 100644 --- a/nsszcontactbot.csproj +++ b/nsszcontactbot.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net10.0 enable enable @@ -20,6 +20,9 @@ PreserveNewest + + PreserveNewest + diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..3998599 --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +dotnet build +cd bin/Debug/net7.0 +screen ./nsszcontactbot \ No newline at end of file