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:
2026-10-04 15:03:00 +00:00
parent 67e5713064
commit b6acef01ef
44 changed files with 13692 additions and 14 deletions
@@ -0,0 +1,185 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using TelegramAuthBot.Models;
namespace TelegramAuthBot.Services
{
sealed class LampacTelegramAuthHttpClient : IDisposable
{
public const string MutationsSecretHeaderName = "X-TelegramAuth-Mutations-Secret";
readonly HttpClient _http;
readonly string _mutationsSecret;
public LampacTelegramAuthHttpClient(TelegramAuthBotConf conf)
{
var baseUrl = (conf.lampac_base_url ?? "").Trim().TrimEnd('/') + "/";
_http = new HttpClient { BaseAddress = new Uri(baseUrl, UriKind.Absolute) };
_http.Timeout = TimeSpan.FromSeconds(Math.Max(1, conf.request_timeout_sec));
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_mutationsSecret = conf.mutations_api_secret?.Trim() ?? "";
}
public void Dispose() => _http.Dispose();
public async Task<UserByTelegramDto> GetUserByTelegramAsync(string telegramId, CancellationToken ct)
{
var path = "tg/auth/user/by-telegram?" + Uri.EscapeDataString("telegramId") + "=" + Uri.EscapeDataString(telegramId);
using var resp = await _http.GetAsync(path, ct).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
if (!resp.IsSuccessStatusCode)
return null;
return JsonConvert.DeserializeObject<UserByTelegramDto>(body);
}
public async Task<DevicesResponseDto> GetDevicesAsync(string telegramId, CancellationToken ct)
{
var path = "tg/auth/devices?" + Uri.EscapeDataString("telegramId") + "=" + Uri.EscapeDataString(telegramId);
using var resp = await _http.GetAsync(path, ct).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
if (resp.StatusCode == HttpStatusCode.NotFound)
return null;
if (!resp.IsSuccessStatusCode)
return null;
return JsonConvert.DeserializeObject<DevicesResponseDto>(body);
}
public async Task<BindCompleteResult> BindCompleteAsync(string uid, string telegramId, string username, CancellationToken ct)
{
var payload = new { uid, telegramId, username };
using var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json");
using var req = new HttpRequestMessage(HttpMethod.Post, "tg/auth/bind/complete") { Content = content };
AddMutationsSecret(req);
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
if (!resp.IsSuccessStatusCode)
return new BindCompleteResult();
var jo = JObject.Parse(body);
return new BindCompleteResult
{
Ok = jo.Value<bool?>("ok") == true,
PendingAdminApproval = jo.Value<bool?>("pendingAdminApproval") == true
};
}
public async Task<bool> UnbindDeviceAsync(string telegramId, string uid, CancellationToken ct)
{
var payload = new { telegramId, uid };
using var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json");
using var resp = await _http.PostAsync("tg/auth/device/unbind", content, ct).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
if (!resp.IsSuccessStatusCode)
return false;
var jo = JObject.Parse(body);
return jo.Value<bool?>("ok") == true;
}
public async Task<(bool ok, string detail)> ReactivateDeviceAsync(string telegramId, string uid, CancellationToken ct)
{
var payload = new { telegramId, uid };
using var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json");
using var resp = await _http.PostAsync("tg/auth/device/reactivate", content, ct).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode ? (true, body) : (false, body);
}
public async Task<(bool ok, string detail)> SetDeviceDisplayNameAsync(string uid, string name, CancellationToken ct)
{
var payload = new { uid, name };
using var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json");
using var resp = await _http.PostAsync("tg/auth/device/name", content, ct).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode ? (true, body) : (false, body);
}
public async Task<(bool ok, string detail)> ImportLegacyAsync(CancellationToken ct)
{
using var req = new HttpRequestMessage(HttpMethod.Post, "tg/auth/import");
AddMutationsSecret(req);
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode ? (true, body) : (false, body);
}
public async Task<(bool ok, string detail)> CleanupDevicesAsync(CancellationToken ct)
{
using var req = new HttpRequestMessage(HttpMethod.Post, "tg/auth/devices/cleanup");
AddMutationsSecret(req);
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode ? (true, body) : (false, body);
}
public async Task<AdminUsersListResponseDto> GetAdminUsersAsync(CancellationToken ct)
{
using var req = new HttpRequestMessage(HttpMethod.Get, "tg/auth/admin/users");
AddMutationsSecret(req);
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
if (!resp.IsSuccessStatusCode)
return null;
return JsonConvert.DeserializeObject<AdminUsersListResponseDto>(body);
}
public async Task<(bool ok, string detail)> SetUserDisabledAsync(string telegramId, bool disabled, CancellationToken ct)
{
var payload = new { telegramId, disabled };
using var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json");
using var req = new HttpRequestMessage(HttpMethod.Post, "tg/auth/admin/user/disabled") { Content = content };
AddMutationsSecret(req);
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode ? (true, body) : (false, body);
}
public async Task<(bool ok, string detail)> ResolveRegistrationPendingAsync(string telegramId, bool approve, CancellationToken ct)
{
var payload = new { telegramId, approve };
using var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json");
using var req = new HttpRequestMessage(HttpMethod.Post, "tg/auth/admin/user/pending") { Content = content };
AddMutationsSecret(req);
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode ? (true, body) : (false, body);
}
public async Task<(bool ok, JObject data, string errorBody)> GetAdminUserDetailAsync(string telegramId, CancellationToken ct)
{
var path = "tg/auth/admin/user?" + Uri.EscapeDataString("telegramId") + "=" + Uri.EscapeDataString(telegramId);
using var req = new HttpRequestMessage(HttpMethod.Get, path);
AddMutationsSecret(req);
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
if (!resp.IsSuccessStatusCode)
return (false, null, body);
try
{
return (true, JObject.Parse(body), body);
}
catch
{
return (false, null, body);
}
}
public async Task<(bool ok, string detail)> PatchAdminUserAsync(JObject patch, CancellationToken ct)
{
using var content = new StringContent(patch.ToString(Formatting.None), Encoding.UTF8, "application/json");
using var req = new HttpRequestMessage(HttpMethod.Post, "tg/auth/admin/user/patch") { Content = content };
AddMutationsSecret(req);
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode ? (true, body) : (false, body);
}
void AddMutationsSecret(HttpRequestMessage req)
{
if (_mutationsSecret.Length > 0)
req.Headers.TryAddWithoutValidation(MutationsSecretHeaderName, _mutationsSecret);
}
}
}