TelegramAuth + TelegramAuthBot + WatchTogether; README/AGENT
- mods/TelegramAuth, mods/TelegramAuthBot, mods/WatchTogether: копии из образа с enable: true (правило 1), секции в config/init.conf - TelegramAuth в API-режиме (enable: false -> accsdb/гейт не поднимается), TelegramAuthBot: бот @GatusMonBot, long polling - WatchTogether: customPlugins watchtogether.js, relay /wt/c/ - README: секция про SQLite/volume (первый запуск), таблица модулей, состояние Tg-notify и TelegramAuth, раздел WatchTogether - AGENT: правило 10 дополнено секретами init.conf, §12 переписан, добавлен §13 WatchTogether, список mods/ в §1 - в git bot_token и mutations_api_secret оставлены пустыми (правило 10), реальные значения только в рабочем файле на сервере
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
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 =
|
||||
"🔔 <b>Новый пользователь (ожидает решения)</b>\n\n" +
|
||||
$"<b>Telegram ID:</b> <code>{EscapeHtml(newUserTgId)}</code>\n" +
|
||||
$"<b>Username:</b> {at}\n" +
|
||||
$"<b>UID устройства:</b> <code>{EscapeHtml(deviceUid)}</code>\n\n" +
|
||||
"<b>Подтвердить</b> — включить доступ. <b>Отклонить</b> — удалить запись из базы.";
|
||||
|
||||
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<bool> 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 =
|
||||
$"✅ <b>Запрос принят</b>\n\n<code>{EscapeHtml(uid)}</code>\n\n" +
|
||||
"Аккаунт создан и <b>ждёт подтверждения</b> администратора (уведомление с кнопками «Принять» / «Отклонить» или список <code>/users</code>).\n\n" +
|
||||
$"После подтверждения вернись в {EscapeHtml(name)} и нажми <b>«Проверить снова»</b>.";
|
||||
}
|
||||
else
|
||||
{
|
||||
var extra = fromStartDeepLink
|
||||
? ""
|
||||
: "\n\n💡 Подсказка: кнопкой <b>📱 Мои устройства</b> можно посмотреть и отвязать устройства.";
|
||||
userText =
|
||||
$"✅ <b>Устройство привязано</b>\n\n<code>{EscapeHtml(uid)}</code>\n\n" +
|
||||
$"Вернись в {EscapeHtml(name)} и нажми <b>«Проверить снова»</b>.{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 =
|
||||
$"<b>Профиль {EscapeHtml(name)}</b>\n\n" +
|
||||
$"<b>Пользователь:</b> @{EscapeHtml(data.username ?? "-")}\n" +
|
||||
$"<b>Telegram ID:</b> <code>{EscapeHtml(data.telegramId ?? tgId)}</code>\n" +
|
||||
$"<b>Роль:</b> {EscapeHtml(data.role ?? "-")}\n" +
|
||||
$"<b>Язык:</b> {EscapeHtml(data.lang ?? "-")}\n" +
|
||||
$"<b>Активен:</b> {(data.active ? "да" : "нет")}{accessNote}\n" +
|
||||
$"<b>Срок доступа:</b> {EscapeHtml(expires)}\n" +
|
||||
$"<b>Устройств:</b> {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<DeviceDto>();
|
||||
if (devices.Count == 0)
|
||||
{
|
||||
await bot.SendMessage(chatId, "У тебя пока нет привязанных устройств.", cancellationToken: ct).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var lines = new List<string> { $"<b>Устройства @{EscapeHtml(data!.username ?? tgId)}:</b>" };
|
||||
var keyboard = new List<InlineKeyboardButton[]>();
|
||||
foreach (var d in devices)
|
||||
{
|
||||
var uid = d.uid ?? "";
|
||||
var devName = string.IsNullOrEmpty(d.name) ? "без имени" : d.name;
|
||||
var state = d.active ? "активно" : "отключено";
|
||||
lines.Add($"• <code>{EscapeHtml(uid)}</code> — {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,
|
||||
"<b>Переименование устройства</b>\n\n<code>/devicename <uid> <имя></code>\n\nUID из «Мои устройства». Чтобы сбросить имя: вместо имени отправь <code>-</code>.",
|
||||
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,
|
||||
"<b>Переименование устройства</b>\n\n<code>/devicename <uid> <имя></code>\n\nПример: <code>/devicename abc12xyz Телевизор в зале</code>\nСброс имени: <code>/devicename abc12xyz -</code>",
|
||||
parseMode: ParseMode.Html,
|
||||
replyMarkup: MainMenuKeyboard(),
|
||||
cancellationToken: ct).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var firstSpace = args.IndexOf(' ');
|
||||
if (firstSpace < 0)
|
||||
{
|
||||
await bot.SendMessage(chatId, "Нужны UID и имя. Пример: <code>/devicename abc12xyz Телевизор</code>", 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 или <code>-</code> для сброса.", 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<DeviceDto>();
|
||||
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, $"✅ Имя устройства <code>{EscapeHtml(uid)}</code> — {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 =
|
||||
$"❓ <b>Помощь по входу в {EscapeHtml(name)}</b>\n\n" +
|
||||
"<b>Быстрый вход:</b>\n" +
|
||||
$"1. Открой {EscapeHtml(name)} и дойди до экрана авторизации\n" +
|
||||
"2. Скопируй UID устройства\n" +
|
||||
"3. Отправь UID мне сюда\n" +
|
||||
"4. Вернись в " + EscapeHtml(name) + " и нажми <b>«Проверить снова»</b>\n\n" +
|
||||
"<b>Кнопки:</b>\n" +
|
||||
"👤 Мой статус — профиль и срок доступа\n" +
|
||||
"📱 Мои устройства — список; отвязать активные, включить отключённые\n" +
|
||||
"<code>/devicename <uid> <имя></code> — имя в базе (для админки); <code>-</code> сбрасывает\n" +
|
||||
"❓ Помощь — эта подсказка\n\n" +
|
||||
"<b>Владелец:</b> числовой user id в <code>TelegramAuth.owner_telegram_ids</code> — при старте Lampac запись admin создаётся в базе. Остальные шлют UID; новых пользователей подтверждаешь в <code>/users</code> (Принять / Отклонить) или кнопками в уведомлении.\n\n" +
|
||||
"<b>Админ-команды:</b> <code>/users</code>, <code>/user</code> <id>, <code>/setuser</code> …, <code>/import</code>, <code>/cleanup</code> + одинаковый <code>mutations_api_secret</code>." +
|
||||
(conf.admin_chat_ids != null && conf.admin_chat_ids.Length > 0
|
||||
? "\n\n<code>admin_chat_ids</code> задан: команды из группы — только там; из лички — если твой id в <code>owner_telegram_ids</code> бота (как на сервере)."
|
||||
: "\n\nПустой <code>admin_chat_ids</code> — админ-команды из лички без доп. списков.");
|
||||
await bot.SendMessage(chatId, text, parseMode: ParseMode.Html, replyMarkup: MainMenuKeyboard(), cancellationToken: ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user