Files
nssz-contact-bot/Program.cs
T

782 lines
29 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 Timer = System.Timers.Timer;
namespace nsszcontactbot;
public class Program
{
private static ITelegramBotClient Bot;
private static List<Employee> Employees = [];
private static List<string> Departments = [];
private static IConfigurationRoot config;
private static FavoriteContactsManager FavoritesManager;
private static PhotosManager PhotosManager;
//private static ILogger<Program> Logger;
public static async Task<int> 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<BotCommand> cmds = [];
await Bot.SetMyCommands(cmds);
if (!LoadEmployees())
{
return 1;
}
StartUpdatingEmployeesData(15 * 60 * 1000); // обновлять данные каждые 15 минут
var me = await Bot.GetMe();
Log.Information($"Бот запущен: @{me.Username}");
// Запуск обработки обновлений
var cancellationToken = new CancellationTokenSource().Token;
ReceiverOptions receiverOptions =
new()
{
AllowedUpdates = Array.Empty<UpdateType>() // 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($"INPUT: from={message.From?.Username} text={message.Text}");
if (message.Text.StartsWith("/start"))
{
await StartCommand(message);
}
else if (message.Text.EndsWith("Подразделения"))
{
await ChooseDepartment(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;
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 ChooseDepartment(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<Employee>
{
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 = $"<b>{contact.FullName}</b>"
};
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<long, List<Employee>> _resultsCache = [];
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 <= 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 =
$"👤 <b>{contact.FullName}</b>\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 int prevHash = 0;
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 файла при наличии изменений
int newHash = json.GetHashCode();
if (prevHash == newHash)
{
return true;
}
prevHash = newHash;
Employees = JsonConvert.DeserializeObject<List<Employee>>(json)!;
}
else
{
Employees.Clear();
}
filePath = config["Paths:PersistentContactsJsonPath"] ?? string.Empty;
if (!string.IsNullOrEmpty(filePath))
{
var json = File.ReadAllText(filePath);
Employees.AddRange(JsonConvert.DeserializeObject<List<Employee>>(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 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;
}
}