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);
}
}
}