Добавлены функции резервного копирования контактов, улучшена обработка фотографий сотрудников и обновлены зависимости
Gitea Actions Demo / Explore-Gitea-Actions (push) Canceled after 0s

This commit is contained in:
Oleg-Nevsky
2026-08-12 16:25:00 +03:00
parent 34efa36d8b
commit 5acf3f9c96
7 changed files with 62 additions and 18 deletions
+1
View File
@@ -438,3 +438,4 @@ FodyWeavers.xsd
# JetBrains Rider # JetBrains Rider
*.sln.iml *.sln.iml
unique_contacts_backup/*
+2 -2
View File
@@ -4,10 +4,10 @@ namespace nsszcontactbot
{ {
internal static class InlineKeyboardMarkupExtensions internal static class InlineKeyboardMarkupExtensions
{ {
internal static InlineKeyboardMarkup AddCloseButton(this InlineKeyboardMarkup markup, bool newRow = true) internal static InlineKeyboardMarkup AddCloseButton(this InlineKeyboardMarkup markup, string text = "❌ Закрыть", bool newRow = true)
{ {
var buttonRow = newRow ? markup.AddNewRow() : markup; var buttonRow = newRow ? markup.AddNewRow() : markup;
return buttonRow.AddButton("❌ Закрыть", "close"); return buttonRow.AddButton(text, "close");
} }
internal static InlineKeyboardMarkup AddPrevPageButton(this InlineKeyboardMarkup markup, int currentPage, string context = "", string text = "⬅️ Назад") internal static InlineKeyboardMarkup AddPrevPageButton(this InlineKeyboardMarkup markup, int currentPage, string context = "", string text = "⬅️ Назад")
+3 -1
View File
@@ -13,9 +13,11 @@ public class PhotosManager(ConnectionMultiplexer redis, string PhotoBasePath)
public async Task CacheEmployeePhotoAsync(Employee employee, string fileId, TimeSpan ttl = default) public async Task CacheEmployeePhotoAsync(Employee employee, string fileId, TimeSpan ttl = default)
{ {
string entryName = GetEntryName(employee); string entryName = GetEntryName(employee);
#pragma warning disable CS4014 // Так как этот вызов не ожидается, выполнение существующего метода продолжается до тех пор, пока вызов не будет завершен
_redisDb.HashSetAsync(Key, [new(entryName, fileId)]).ConfigureAwait(false); _redisDb.HashSetAsync(Key, [new(entryName, fileId)]).ConfigureAwait(false);
ttl = ttl == default ? TimeSpan.FromDays(30) : ttl; ttl = ttl == default ? TimeSpan.FromDays(30) : ttl;
_redisDb.HashFieldExpireAsync(Key, [new(entryName)], ttl).ConfigureAwait(false); _redisDb.HashFieldExpireAsync(Key, [new(entryName)], ttl).ConfigureAwait(false);
#pragma warning restore CS4014 // Так как этот вызов не ожидается, выполнение существующего метода продолжается до тех пор, пока вызов не будет завершен
} }
public async Task<InputFile?> GetEmployeePhotoAsync(Employee employee) public async Task<InputFile?> GetEmployeePhotoAsync(Employee employee)
@@ -40,7 +42,7 @@ public class PhotosManager(ConnectionMultiplexer redis, string PhotoBasePath)
); );
return InputFile.FromStream(fileStream); return InputFile.FromStream(fileStream);
} }
return InputFile.FromFileId(DefaultPhotoFileId);; return InputFile.FromFileId(DefaultPhotoFileId); ;
} }
} }
+49 -14
View File
@@ -2,6 +2,8 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;
using StackExchange.Redis; using StackExchange.Redis;
using System.IO.Hashing;
using System.Text;
using System.Timers; using System.Timers;
using Telegram.Bot; using Telegram.Bot;
using Telegram.Bot.Polling; using Telegram.Bot.Polling;
@@ -14,12 +16,12 @@ namespace nsszcontactbot;
public class Program public class Program
{ {
private static ITelegramBotClient Bot; private static ITelegramBotClient Bot = default!;
private static List<Employee> Employees = []; private static List<Employee> Employees = [];
private static List<string> Departments = []; private static List<string> Departments = [];
private static IConfigurationRoot config; private static IConfigurationRoot config = default!;
private static FavoriteContactsManager FavoritesManager; private static FavoriteContactsManager FavoritesManager = default!;
private static PhotosManager PhotosManager; private static PhotosManager PhotosManager = default!;
//private static ILogger<Program> Logger; //private static ILogger<Program> Logger;
public static async Task<int> Main(string[] args) public static async Task<int> Main(string[] args)
@@ -57,7 +59,7 @@ public class Program
var me = await Bot.GetMe(); var me = await Bot.GetMe();
Log.Information($"Бот запущен: @{me.Username}"); Log.Information("Бот запущен: @{Username}", me.Username);
// Запуск обработки обновлений // Запуск обработки обновлений
var cancellationToken = new CancellationTokenSource().Token; var cancellationToken = new CancellationTokenSource().Token;
@@ -89,7 +91,7 @@ public class Program
if (update.Type == UpdateType.Message && update.Message?.Text != null) if (update.Type == UpdateType.Message && update.Message?.Text != null)
{ {
var message = update.Message; var message = update.Message;
Log.Debug($"INPUT: from={message.From?.Username} text={message.Text}"); Log.Debug("Входящий текст (From=\"{Username}\", MessageId=\"{MessageId}\"): {Text}", message.From?.ToString() ?? "<!--message.From is null-->", message.Id, message.Text);
if (message.Text.StartsWith("/start")) if (message.Text.StartsWith("/start"))
{ {
@@ -97,7 +99,7 @@ public class Program
} }
else if (message.Text.EndsWith("Подразделения")) else if (message.Text.EndsWith("Подразделения"))
{ {
await ChooseDepartment(message); await HandleShowDepartments(message);
} }
else if (message.Text.EndsWith("Избранные")) else if (message.Text.EndsWith("Избранные"))
{ {
@@ -111,6 +113,8 @@ public class Program
else if (update.Type == UpdateType.CallbackQuery) else if (update.Type == UpdateType.CallbackQuery)
{ {
string data = update.CallbackQuery!.Data ?? string.Empty; string data = update.CallbackQuery!.Data ?? string.Empty;
var callbackQuery = update.CallbackQuery!;
Log.Debug("Нажатие кнопки (From=\"{Username}\", CallbackQueryId=\"{CallbackQueryId}\"): {Data}", callbackQuery.From?.ToString() ?? "<!--cbquery.From is null-->", callbackQuery.Id, data);
string[] dataParts = data.Split('/'); string[] dataParts = data.Split('/');
if (dataParts.Length > 0) if (dataParts.Length > 0)
{ {
@@ -185,7 +189,7 @@ public class Program
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) private static async Task HandleShowDepartments(Message message)
{ {
Departments = [.. Employees.OrderBy(e => e.DepartmentCode).Select(e => e.Department).Distinct()]; Departments = [.. Employees.OrderBy(e => e.DepartmentCode).Select(e => e.Department).Distinct()];
await ShowDepartments(message.Chat.Id); await ShowDepartments(message.Chat.Id);
@@ -382,10 +386,10 @@ public class Program
if (employee != null) if (employee != null)
{ {
await FavoritesManager.RemoveFavoriteAsync(callbackQuery.From.Id, contactId); await FavoritesManager.RemoveFavoriteAsync(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) foreach (var cell in row)
{ {
if (cell.CallbackData.Contains("delfavorite")) if (cell.CallbackData!.Contains("delfavorite"))
{ {
cell.CallbackData = $"addfavorite/{contactId}"; cell.CallbackData = $"addfavorite/{contactId}";
cell.Text = "❤️ В избранные"; cell.Text = "❤️ В избранные";
@@ -533,7 +537,7 @@ public class Program
private static async Task ShowEmployeeResults(Message message, List<Employee> results, bool? isFavorite = false, int page = 1, int pageSize = 10) private static async Task ShowEmployeeResults(Message message, List<Employee> results, bool? isFavorite = false, int page = 1, int pageSize = 10)
{ {
if (results.Any()) if (results.Count != 0)
{ {
if (results.Count <= pageSize) if (results.Count <= pageSize)
{ {
@@ -670,7 +674,7 @@ public class Program
card += $"📞 {contact.ExtensionNumber}\n"; card += $"📞 {contact.ExtensionNumber}\n";
if (!string.IsNullOrEmpty(employee.IP)) if (!string.IsNullOrEmpty(employee.IP))
card += $"🌐 {employee.IP}\n"; card += $"🖥 {employee.IP}\n";
card += GetEventStatus(employee); card += GetEventStatus(employee);
@@ -692,7 +696,7 @@ public class Program
LoadEmployees(); LoadEmployees();
} }
private static int prevHash = 0; private static string prevHash = string.Empty;
private static bool LoadEmployees() private static bool LoadEmployees()
{ {
try try
@@ -713,13 +717,19 @@ public class Program
json = File.ReadAllText(filePath); json = File.ReadAllText(filePath);
} }
// Загрузка данных сотрудников из JSON файла при наличии изменений // Загрузка данных сотрудников из JSON файла при наличии изменений
int newHash = json.GetHashCode(); var newHash = CalculateHash(json);
if (prevHash == newHash) if (prevHash == newHash)
{ {
return true; return true;
} }
prevHash = newHash; prevHash = newHash;
Employees = JsonConvert.DeserializeObject<List<Employee>>(json)!; Employees = JsonConvert.DeserializeObject<List<Employee>>(json)!;
var backupFilename = BackupContactsJSON(json);
if (!string.IsNullOrEmpty(backupFilename))
{
Log.Debug("Данные обновлены, создана резервная копия: {BackupFilename}", backupFilename);
}
} }
else else
{ {
@@ -748,6 +758,31 @@ public class Program
} }
} }
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() private static void StartBirthdayNotifications()
{ {
var timer = new Timer(TimeSpan.FromHours(24).TotalMilliseconds); var timer = new Timer(TimeSpan.FromHours(24).TotalMilliseconds);
+2 -1
View File
@@ -2,7 +2,8 @@
"Paths": { "Paths": {
"ContactsJsonPath": "http://contacts.nssz.local/data/contacts.json", "ContactsJsonPath": "http://contacts.nssz.local/data/contacts.json",
"PersistentContactsJsonPath": "PersistentContacts.json", "PersistentContactsJsonPath": "PersistentContacts.json",
"PhotoBasePath": "photos" "PhotoBasePath": "photos",
"ContactsBackupDirectory": "unique_contacts_backup"
}, },
"Telegram": { "Telegram": {
"BotToken": "8155021946:AAGbfJnLa0sTWxqFB9yjbXhbTedBw4wvcQE" "BotToken": "8155021946:AAGbfJnLa0sTWxqFB9yjbXhbTedBw4wvcQE"
+1
View File
@@ -14,6 +14,7 @@
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="System.IO.Hashing" Version="10.0.10" />
<PackageReference Include="Telegram.Bot" Version="22.4.4" /> <PackageReference Include="Telegram.Bot" Version="22.4.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+4
View File
@@ -0,0 +1,4 @@
@echo off
dotnet build -c Release
cd bin\Release\net10.0
.\nsszcontactbot