#794: Telegram-Client-Anmeldung ueber die WebUI
Code und 2FA-Passwort wurden nur im Desktop-Dialog abgefragt; eine Instanz ohne Fenster liess die Anmeldung aus. Jetzt haelt die Instanz die Frage (TelegramLoginBroker), die Einstellungsseite beantwortet sie. Die Anmeldung laeuft im Hintergrund, damit die HTTP-Flaeche, ueber die der Code kommt, nicht auf ihn wartet. Eine offene Frage verfaellt nach 15 Minuten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
93395c9e01
commit
4e41fc12d7
@@ -459,8 +459,10 @@ public sealed class AppHost : IAsyncDisposable
|
||||
// ─── 5. Werkzeuge ───
|
||||
var tools = RegisterTools();
|
||||
|
||||
// #794: Registered whenever it is configured. Before, an instance without a window
|
||||
// skipped it ("no way to log in") - the login now comes through the web interface.
|
||||
TelegramClientManager? telegram = null;
|
||||
if (instance.TelegramClient is not null && callbacks.TelegramLogin is not null)
|
||||
if (instance.TelegramClient is not null)
|
||||
{
|
||||
telegram = new TelegramClientManager(instance, instancePath,
|
||||
loggerFactory.CreateLogger("ClawdDotNet.Tools.TelegramClient"));
|
||||
@@ -468,11 +470,6 @@ public sealed class AppHost : IAsyncDisposable
|
||||
tools.Register(new TelegramClientTool(telegram));
|
||||
logger.LogInformation("TelegramClient-Tool registriert");
|
||||
}
|
||||
else if (instance.TelegramClient is not null)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"TelegramClient konfiguriert, aber kein Anmeldeweg vorhanden – übersprungen.");
|
||||
}
|
||||
|
||||
var host = BuildCore(settings, directories, instance, instancePath,
|
||||
logDirectory, loggerFactory, tools, telegram, logger);
|
||||
@@ -1031,9 +1028,61 @@ public sealed class AppHost : IAsyncDisposable
|
||||
return (scanner, staging, board, followUp);
|
||||
}
|
||||
|
||||
/// <summary>#794: The Telegram client login as the WebUI sees it and answers it.</summary>
|
||||
public TelegramLoginBroker TelegramLogin { get; private set; } = new(configured: false);
|
||||
|
||||
private ILogger? _telegramLogger;
|
||||
private int _telegramConnecting;
|
||||
|
||||
/// <summary>
|
||||
/// #794: Starts (or restarts) the Telegram login in the background; questions go to
|
||||
/// <see cref="TelegramLogin"/>. Does nothing while a login is already running.
|
||||
/// </summary>
|
||||
public AdminResult StartTelegramLogin()
|
||||
{
|
||||
if (TelegramClient is null)
|
||||
return AdminResult.Failure("No Telegram client is configured in this instance.");
|
||||
if (Interlocked.Exchange(ref _telegramConnecting, 1) != 0)
|
||||
return AdminResult.Failure("A Telegram login is already running.");
|
||||
|
||||
TelegramLogin.MarkConnecting();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await TelegramClient.ConnectAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
TelegramLogin.MarkConnected(TelegramClient.Self?.first_name ?? "?");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// The message of the library, not the input: the code never ends up in it.
|
||||
_telegramLogger?.LogError(ex, "Telegram: Login fehlgeschlagen");
|
||||
TelegramLogin.MarkFailed($"Login failed: {(ex is AggregateException { InnerException: { } inner } ? inner.Message : ex.Message)}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _telegramConnecting, 0);
|
||||
}
|
||||
});
|
||||
|
||||
return AdminResult.Success("Telegram login started.");
|
||||
}
|
||||
|
||||
private async Task ConnectTelegramAsync(Callbacks callbacks, ILogger logger)
|
||||
{
|
||||
if (TelegramClient is null || callbacks.TelegramLogin is null) return;
|
||||
if (TelegramClient is null) return;
|
||||
|
||||
// #794: Without a dialog (every instance without a window), the login runs through
|
||||
// the web interface - in the background, so the HTTP interface it needs comes up.
|
||||
if (callbacks.TelegramLogin is null)
|
||||
{
|
||||
_telegramLogger = logger;
|
||||
TelegramLogin = new TelegramLoginBroker(configured: true);
|
||||
TelegramClient.OnLoginCodeRequired += prompt => TelegramLogin.AskForCodeAsync(prompt);
|
||||
TelegramClient.On2FAPasswordRequired += () => TelegramLogin.AskForPasswordAsync();
|
||||
StartTelegramLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
TelegramClient.OnLoginCodeRequired += prompt => callbacks.TelegramLogin(prompt);
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
namespace ClawdDotNet.App.Services;
|
||||
|
||||
/// <summary>#794: Where the Telegram client login stands.</summary>
|
||||
public enum TelegramLoginState
|
||||
{
|
||||
/// <summary>No Telegram client configured in this instance.</summary>
|
||||
NotConfigured,
|
||||
|
||||
/// <summary>Connecting; no question pending.</summary>
|
||||
Connecting,
|
||||
|
||||
/// <summary>Telegram sent a login code; it has to be entered.</summary>
|
||||
NeedsCode,
|
||||
|
||||
/// <summary>The account has two-step verification and no password is stored.</summary>
|
||||
NeedsPassword,
|
||||
|
||||
Connected,
|
||||
|
||||
Failed
|
||||
}
|
||||
|
||||
/// <summary>#794: The login as the WebUI sees it — never the code or password itself.</summary>
|
||||
public sealed record TelegramLoginStatus(TelegramLoginState State, string Message);
|
||||
|
||||
/// <summary>
|
||||
/// #794: The Telegram client login through the web interface.
|
||||
///
|
||||
/// <para>Until now the login code and the two-step password were asked for in a desktop dialog,
|
||||
/// and an instance without a window skipped the login altogether — a new Telegram session
|
||||
/// could only be created in the desktop window. Here the question becomes a pending state the
|
||||
/// WebUI shows, and the answer comes back through an endpoint.</para>
|
||||
///
|
||||
/// <para><b>Nothing waits on the person at startup.</b> The login runs in the background after
|
||||
/// the instance is up: waiting for a code during startup would hold back the HTTP interface —
|
||||
/// the very way the code has to come in.</para>
|
||||
///
|
||||
/// <para><b>A question does not wait forever.</b> After <see cref="AnswerTimeout"/> the
|
||||
/// login fails with a clear state; it can be started again. Otherwise a forgotten browser tab
|
||||
/// would leave a thread blocked inside the Telegram library until the next restart.</para>
|
||||
/// </summary>
|
||||
public sealed class TelegramLoginBroker
|
||||
{
|
||||
public static readonly TimeSpan AnswerTimeout = TimeSpan.FromMinutes(15);
|
||||
|
||||
private readonly object _lock = new();
|
||||
private TaskCompletionSource<string>? _pending;
|
||||
private TelegramLoginStatus _status;
|
||||
|
||||
public TelegramLoginBroker(bool configured)
|
||||
=> _status = configured
|
||||
? new TelegramLoginStatus(TelegramLoginState.Connecting, "Connecting to Telegram…")
|
||||
: new TelegramLoginStatus(TelegramLoginState.NotConfigured, "No Telegram client is configured in this instance.");
|
||||
|
||||
public TelegramLoginStatus Status
|
||||
{
|
||||
get { lock (_lock) return _status; }
|
||||
}
|
||||
|
||||
/// <summary>Raised whenever the state changes, so the event stream can tell the WebUI.</summary>
|
||||
public event Action? Changed;
|
||||
|
||||
/// <summary>Asks for the login code — called by the Telegram client, waits for <see cref="Answer"/>.</summary>
|
||||
public Task<string> AskForCodeAsync(string prompt)
|
||||
=> AskAsync(TelegramLoginState.NeedsCode, "Telegram sent a login code to the account's devices. Enter it here.");
|
||||
|
||||
/// <summary>Asks for the two-step password when none is stored.</summary>
|
||||
public Task<string> AskForPasswordAsync()
|
||||
=> AskAsync(TelegramLoginState.NeedsPassword, "The account uses two-step verification. Enter its password.");
|
||||
|
||||
private async Task<string> AskAsync(TelegramLoginState state, string message)
|
||||
{
|
||||
TaskCompletionSource<string> pending;
|
||||
lock (_lock)
|
||||
{
|
||||
_pending?.TrySetCanceled();
|
||||
pending = _pending = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_status = new TelegramLoginStatus(state, message);
|
||||
}
|
||||
Changed?.Invoke();
|
||||
|
||||
var finished = await Task.WhenAny(pending.Task, Task.Delay(AnswerTimeout)).ConfigureAwait(false);
|
||||
if (finished != pending.Task)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (ReferenceEquals(_pending, pending)) _pending = null;
|
||||
}
|
||||
pending.TrySetCanceled();
|
||||
throw new TimeoutException("No answer within 15 minutes.");
|
||||
}
|
||||
|
||||
return await pending.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>The answer from the WebUI. Refused when no question is pending.</summary>
|
||||
public AdminResult Answer(string value)
|
||||
{
|
||||
value = value.Trim();
|
||||
if (value.Length == 0)
|
||||
return AdminResult.Failure("Enter the code or password.");
|
||||
|
||||
TaskCompletionSource<string>? pending;
|
||||
lock (_lock)
|
||||
{
|
||||
pending = _pending;
|
||||
_pending = null;
|
||||
if (pending is null)
|
||||
return AdminResult.Failure("Telegram is not waiting for an answer.");
|
||||
_status = new TelegramLoginStatus(TelegramLoginState.Connecting, "Answer sent, logging in…");
|
||||
}
|
||||
|
||||
pending.TrySetResult(value);
|
||||
Changed?.Invoke();
|
||||
return AdminResult.Success("Sent to Telegram.");
|
||||
}
|
||||
|
||||
public void MarkConnecting() => Set(TelegramLoginState.Connecting, "Connecting to Telegram…");
|
||||
|
||||
public void MarkConnected(string who) => Set(TelegramLoginState.Connected, $"Logged in as {who}.");
|
||||
|
||||
/// <summary>The reason is shown to the person; it must not carry the code or password.</summary>
|
||||
public void MarkFailed(string reason) => Set(TelegramLoginState.Failed, reason);
|
||||
|
||||
private void Set(TelegramLoginState state, string message)
|
||||
{
|
||||
lock (_lock) _status = new TelegramLoginStatus(state, message);
|
||||
Changed?.Invoke();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user