using Telegram.Bot.Types; using TelegramAuthBot.Models; namespace TelegramAuthBot.Services { sealed partial class TelegramAuthBotSession { async Task NotifyAdminsOfPendingProvisionAsync(ITelegramBotClient bot, string newUserTgId, string username, string deviceUid, CancellationToken ct) { var conf = ModInit.conf; if (!conf.notify_admins_on_pending_provision) return; if (string.IsNullOrEmpty(conf.mutations_api_secret?.Trim())) { TelegramAuthBotSerilog.Log.Warning( "Новый пользователь ожидает активации (TelegramId={TgId}), mutations_api_secret пуст — уведомления админам не отправлены.", newUserTgId); return; } AdminUsersListResponseDto data; try { data = await _api.GetAdminUsersAsync(ct).ConfigureAwait(false); } catch (Exception ex) { TelegramAuthBotSerilog.Log.Warning(ex, "Не удалось загрузить список пользователей для уведомления о новом аккаунте"); return; } if (data?.users == null || data.users.Count == 0) return; var admins = data.users .Where(u => string.Equals(u.role, "admin", StringComparison.OrdinalIgnoreCase)) .Where(u => !string.IsNullOrWhiteSpace(u.telegramId)) .ToList(); if (admins.Count == 0) { TelegramAuthBotSerilog.Log.Warning( "Новый пользователь ожидает активации (TelegramId={TgId}), в базе нет пользователей с ролью admin.", newUserTgId); return; } var at = string.IsNullOrEmpty(username) ? "—" : "@" + EscapeHtml(username); var text = "🔔 Новый пользователь (ожидает решения)\n\n" + $"Telegram ID: {EscapeHtml(newUserTgId)}\n" + $"Username: {at}\n" + $"UID устройства: {EscapeHtml(deviceUid)}\n\n" + "Подтвердить — включить доступ. Отклонить — удалить запись из базы."; var cbApprove = CbApprovePending + "0|" + newUserTgId; var cbReject = CbRejectPending + "0|" + newUserTgId; InlineKeyboardMarkup notifyKb = null; if (Encoding.UTF8.GetByteCount(cbApprove) <= 64 && Encoding.UTF8.GetByteCount(cbReject) <= 64) { notifyKb = new InlineKeyboardMarkup(new[] { new[] { InlineKeyboardButton.WithCallbackData("✅ Принять", cbApprove), InlineKeyboardButton.WithCallbackData("❌ Отклонить", cbReject) } }); } foreach (var a in admins) { if (!long.TryParse(a.telegramId.Trim(), out var adminChatId)) continue; try { await bot.SendMessage(new ChatId(adminChatId), text, parseMode: ParseMode.Html, replyMarkup: notifyKb, cancellationToken: ct).ConfigureAwait(false); } catch (Exception ex) { TelegramAuthBotSerilog.Log.Warning(ex, "Не удалось отправить уведомление администратору {AdminTgId}", a.telegramId); } } } async Task TryBindAsync(ITelegramBotClient bot, ChatId chatId, string tgId, string username, string uid, bool fromStartDeepLink, CancellationToken ct) { var name = _displayName; var user = await _api.GetUserByTelegramAsync(tgId, ct).ConfigureAwait(false); if (user != null && user.found && !user.active) { string blocked; if (user.registrationPending) blocked = "Аккаунт ожидает подтверждения администратора. Когда доступ подтвердят, нажми «Проверить снова» в приложении."; else if (user.disabled) blocked = "Доступ отключён администратором. Если только что отправил UID, дождись включения и снова нажми «Проверить снова» в приложении."; else blocked = "Твой доступ истёк."; await bot.SendMessage(chatId, blocked, replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); return true; } var bind = await _api.BindCompleteAsync(uid, tgId, username, ct).ConfigureAwait(false); if (bind.Ok) { if (bind.PendingAdminApproval) await NotifyAdminsOfPendingProvisionAsync(bot, tgId, username, uid, ct).ConfigureAwait(false); string userText; if (bind.PendingAdminApproval) { userText = $"✅ Запрос принят\n\n{EscapeHtml(uid)}\n\n" + "Аккаунт создан и ждёт подтверждения администратора (уведомление с кнопками «Принять» / «Отклонить» или список /users).\n\n" + $"После подтверждения вернись в {EscapeHtml(name)} и нажми «Проверить снова»."; } else { var extra = fromStartDeepLink ? "" : "\n\n💡 Подсказка: кнопкой 📱 Мои устройства можно посмотреть и отвязать устройства."; userText = $"✅ Устройство привязано\n\n{EscapeHtml(uid)}\n\n" + $"Вернись в {EscapeHtml(name)} и нажми «Проверить снова».{extra}"; } await bot.SendMessage(chatId, userText, parseMode: ParseMode.Html, replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); return true; } if (fromStartDeepLink) return false; if (user != null && user.found) await bot.SendMessage(chatId, "Не удалось привязать устройство.", replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); else await bot.SendMessage(chatId, $"Тебя нет в базе {EscapeHtml(name)}. Обратись к администратору.", parseMode: ParseMode.Html, replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); return true; } async Task CmdMeAsync(ITelegramBotClient bot, ChatId chatId, string tgId, CancellationToken ct) { var name = _displayName; var data = await _api.GetUserByTelegramAsync(tgId, ct).ConfigureAwait(false); if (data == null || !data.found) { await bot.SendMessage(chatId, $"Тебя нет в базе авторизации {EscapeHtml(name)}.", parseMode: ParseMode.Html, replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); return; } var maxDev = data.maxDevices == -1 ? "∞" : data.maxDevices.ToString(); var expires = string.IsNullOrEmpty(data.expiresAt) ? "-" : data.expiresAt; var accessNote = data.registrationPending ? " (ожидает подтверждения)" : data.disabled ? " (отключён администратором)" : ""; var text = $"Профиль {EscapeHtml(name)}\n\n" + $"Пользователь: @{EscapeHtml(data.username ?? "-")}\n" + $"Telegram ID: {EscapeHtml(data.telegramId ?? tgId)}\n" + $"Роль: {EscapeHtml(data.role ?? "-")}\n" + $"Язык: {EscapeHtml(data.lang ?? "-")}\n" + $"Активен: {(data.active ? "да" : "нет")}{accessNote}\n" + $"Срок доступа: {EscapeHtml(expires)}\n" + $"Устройств: {data.deviceCount} / {maxDev}"; await bot.SendMessage(chatId, text, parseMode: ParseMode.Html, replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); } async Task CmdDevicesAsync(ITelegramBotClient bot, ChatId chatId, string tgId, CancellationToken ct) { var data = await _api.GetDevicesAsync(tgId, ct).ConfigureAwait(false); var devices = data?.devices ?? new List(); if (devices.Count == 0) { await bot.SendMessage(chatId, "У тебя пока нет привязанных устройств.", cancellationToken: ct).ConfigureAwait(false); return; } var lines = new List { $"Устройства @{EscapeHtml(data!.username ?? tgId)}:" }; var keyboard = new List(); foreach (var d in devices) { var uid = d.uid ?? ""; var devName = string.IsNullOrEmpty(d.name) ? "без имени" : d.name; var state = d.active ? "активно" : "отключено"; lines.Add($"• {EscapeHtml(uid)} — {EscapeHtml(devName)} ({state})"); if (!string.IsNullOrEmpty(uid) && d.active) keyboard.Add(new[] { InlineKeyboardButton.WithCallbackData($"Отвязать {devName}", "unbind:" + uid) }); else if (!string.IsNullOrEmpty(uid) && !d.active) keyboard.Add(new[] { InlineKeyboardButton.WithCallbackData($"✅ Включить · {devName}", CbReactivateDevice + uid) }); } var markup = keyboard.Count > 0 ? new InlineKeyboardMarkup(keyboard) : null; await bot.SendMessage(chatId, string.Join("\n", lines), parseMode: ParseMode.Html, replyMarkup: markup, cancellationToken: ct).ConfigureAwait(false); } async Task CmdDeviceNameAsync(ITelegramBotClient bot, ChatId chatId, string tgId, string text, CancellationToken ct) { var t = text.Trim(); var cmdLen = "/devicename".Length; if (t.Length > cmdLen && t[cmdLen] == '@') { var at = t.IndexOf(' ', cmdLen); if (at < 0) { await bot.SendMessage(chatId, "Переименование устройства\n\n/devicename <uid> <имя>\n\nUID из «Мои устройства». Чтобы сбросить имя: вместо имени отправь -.", parseMode: ParseMode.Html, replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); return; } cmdLen = at; } var args = t.Length > cmdLen ? t.Substring(cmdLen).TrimStart() : ""; if (string.IsNullOrEmpty(args)) { await bot.SendMessage(chatId, "Переименование устройства\n\n/devicename <uid> <имя>\n\nПример: /devicename abc12xyz Телевизор в зале\nСброс имени: /devicename abc12xyz -", parseMode: ParseMode.Html, replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); return; } var firstSpace = args.IndexOf(' '); if (firstSpace < 0) { await bot.SendMessage(chatId, "Нужны UID и имя. Пример: /devicename abc12xyz Телевизор", parseMode: ParseMode.Html, replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); return; } var uid = args.Substring(0, firstSpace).Trim(); var newName = args.Substring(firstSpace + 1).Trim(); if (string.IsNullOrEmpty(newName)) { await bot.SendMessage(chatId, "Укажи новое имя после UID или - для сброса.", parseMode: ParseMode.Html, replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); return; } if (!UidRe.IsMatch(uid)) { await bot.SendMessage(chatId, "Некорректный UID.", replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); return; } var devicesResp = await _api.GetDevicesAsync(tgId, ct).ConfigureAwait(false); var devices = devicesResp?.devices ?? new List(); var owned = devices.Any(d => d != null && d.active && !string.IsNullOrEmpty(d.uid) && string.Equals(d.uid, uid, StringComparison.OrdinalIgnoreCase)); if (!owned) { await bot.SendMessage(chatId, "Активного устройства с таким UID у тебя нет. Список — в «Мои устройства».", replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); return; } string apiName = string.Equals(newName, "-", StringComparison.Ordinal) ? null : newName; var (ok, detail) = await _api.SetDeviceDisplayNameAsync(uid, apiName, ct).ConfigureAwait(false); if (ok) { var shown = apiName == null ? "сброшено (без имени)" : $"«{EscapeHtml(apiName)}»"; await bot.SendMessage(chatId, $"✅ Имя устройства {EscapeHtml(uid)} — {shown}.", parseMode: ParseMode.Html, replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); } else { await bot.SendMessage(chatId, "❌ Не удалось сохранить имя. Если доступ истёк или устройство неактивно — привяжи снова.\n" + TruncateForTelegram(detail, 500), replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); } } async Task CmdHelpAsync(ITelegramBotClient bot, ChatId chatId, CancellationToken ct) { var name = _displayName; var conf = ModInit.conf; var text = $"❓ Помощь по входу в {EscapeHtml(name)}\n\n" + "Быстрый вход:\n" + $"1. Открой {EscapeHtml(name)} и дойди до экрана авторизации\n" + "2. Скопируй UID устройства\n" + "3. Отправь UID мне сюда\n" + "4. Вернись в " + EscapeHtml(name) + " и нажми «Проверить снова»\n\n" + "Кнопки:\n" + "👤 Мой статус — профиль и срок доступа\n" + "📱 Мои устройства — список; отвязать активные, включить отключённые\n" + "/devicename <uid> <имя> — имя в базе (для админки); - сбрасывает\n" + "❓ Помощь — эта подсказка\n\n" + "Владелец: числовой user id в TelegramAuth.owner_telegram_ids — при старте Lampac запись admin создаётся в базе. Остальные шлют UID; новых пользователей подтверждаешь в /users (Принять / Отклонить) или кнопками в уведомлении.\n\n" + "Админ-команды: /users, /user <id>, /setuser …, /import, /cleanup + одинаковый mutations_api_secret." + (conf.admin_chat_ids != null && conf.admin_chat_ids.Length > 0 ? "\n\nadmin_chat_ids задан: команды из группы — только там; из лички — если твой id в owner_telegram_ids бота (как на сервере)." : "\n\nПустой admin_chat_ids — админ-команды из лички без доп. списков."); await bot.SendMessage(chatId, text, parseMode: ParseMode.Html, replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false); } } }