Skip to content

Commit f86d1e2

Browse files
committed
feat: QR code login option in the web login page
The QR login plumbing (CallQrGenerator wrapping LoginWithQRCode, QR rendering in Index) existed but nothing invoked it, and it could not have worked from the web anyway: when the account has 2FA, LoginWithQRCode asks for the password through Config("password"), which with the convenience constructor prompts on the console - QR login only worked when driving the library from a terminal. Wire it end to end: - The main client is now built with a custom config callback that routes the "password" request to the web UI: a QrPasswordNeeded event switches the login form to the 2FA password step and the value is handed back to the waiting login task, which blocks on a TaskCompletionSource (5 minute timeout) on its background thread. - The login page offers "Log in with QR code" next to the phone step: it shows the tg://login QR (auto-refreshed by the library when each token expires), instructions and a back button; on success the shared post-login initialization runs and navigation proceeds as with the normal flow. - DoLogin's post-login tail is extracted into CompleteLogin, reused by the QR flow. - checkAuth: when a session exists but no user data file is saved (the QR case - there is no phone to save), attempt a silent session restore via DoLogin(null) instead of always bouncing to the phone step, so QR sessions survive app restarts.
1 parent 0715286 commit f86d1e2

3 files changed

Lines changed: 165 additions & 4 deletions

File tree

TelegramDownloader/Data/ITelegramService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public interface ITelegramService
1919
Task<InvitationInfo?> getInvitationHash(long id);
2020
Task joinChatInvitationHash(string? hash);
2121
Task<User> CallQrGenerator(Action<string> func, CancellationToken ct, bool logoutFirst = false);
22+
void ProvideQrLoginPassword(string password);
2223
Task<string> DownloadFile(ChatMessages message, string fileName = null, string folder = null, DownloadModel model = null, bool shouldAddToList = false);
2324
Task<Byte[]> DownloadFileStream(Message message, long offset, int limit);
2425
IAsyncEnumerable<byte[]> DownloadFileStreamChunks(Message message, long offset, long limit, CancellationToken ct = default);

TelegramDownloader/Data/TelegramService.cs

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,20 @@ private void newClient()
173173
{
174174
_logger.LogDebug(ex, "Could not snapshot the session file");
175175
}
176-
client = new WTelegram.Client(Convert.ToInt32(GeneralConfigStatic.tlconfig?.api_id ?? Environment.GetEnvironmentVariable("api_id")), GeneralConfigStatic.tlconfig?.hash_id ?? Environment.GetEnvironmentVariable("hash_id"), UserService.USERDATAFOLDER + "/WTelegram.session");
176+
string apiId = GeneralConfigStatic.tlconfig?.api_id ?? Environment.GetEnvironmentVariable("api_id");
177+
string apiHash = GeneralConfigStatic.tlconfig?.hash_id ?? Environment.GetEnvironmentVariable("hash_id");
178+
// Custom config callback instead of the convenience constructor: the
179+
// QR login flow (LoginWithQRCode) asks for the 2FA password through
180+
// Config("password") - with the default config that prompt would go
181+
// to the console. Route it to the web UI instead.
182+
client = new WTelegram.Client(what => what switch
183+
{
184+
"api_id" => apiId,
185+
"api_hash" => apiHash,
186+
"session_pathname" => UserService.USERDATAFOLDER + "/WTelegram.session",
187+
"password" => RequestLoginPassword(),
188+
_ => null
189+
});
177190
ApplyConfiguredParallelTransfers(client);
178191
if (GeneralConfigStatic.config.ShouldShowLogInTerminal)
179192
{
@@ -585,9 +598,41 @@ async Task GuardedWorker(WTelegram.Client pc)
585598

586599
#endregion
587600

601+
// 2FA password bridge for the QR login flow: WTelegram asks for the
602+
// password via Config("password") on a background task; the UI is
603+
// notified through QrPasswordNeeded and supplies the value with
604+
// ProvideQrLoginPassword, unblocking the pending request.
605+
private static TaskCompletionSource<string> qrPasswordRequest;
606+
public static event EventHandler QrPasswordNeeded;
607+
608+
private static string RequestLoginPassword()
609+
{
610+
TaskCompletionSource<string> tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
611+
qrPasswordRequest = tcs;
612+
try
613+
{
614+
QrPasswordNeeded?.Invoke(null, EventArgs.Empty);
615+
}
616+
catch (Exception)
617+
{
618+
}
619+
// Blocks the QR login background task, never the UI thread.
620+
if (!tcs.Task.Wait(TimeSpan.FromMinutes(5)))
621+
throw new TimeoutException("2FA password was not provided in time");
622+
return tcs.Task.Result;
623+
}
624+
625+
public void ProvideQrLoginPassword(string password)
626+
{
627+
qrPasswordRequest?.TrySetResult(password);
628+
}
629+
588630
public async Task<User> CallQrGenerator(Action<string> func, CancellationToken ct, bool logoutFirst = false)
589631
{
590-
return await client.LoginWithQRCode(func, logoutFirst: logoutFirst, ct: ct);
632+
User user = await client.LoginWithQRCode(func, logoutFirst: logoutFirst, ct: ct);
633+
if (user != null)
634+
await CompleteLogin();
635+
return user;
591636
}
592637

593638
public async Task<User> GetUser()
@@ -632,6 +677,16 @@ async Task<string> DoLogin(string loginInfo) // (add this method to your code)
632677
return "pass"; // if user has enabled 2FA
633678
default: break;
634679
}
680+
return await CompleteLogin();
681+
}
682+
683+
/// <summary>
684+
/// Post-login initialization shared by the interactive login flow and the
685+
/// QR login flow: loads chats, resolves premium limits and notifies
686+
/// subscribers.
687+
/// </summary>
688+
private async Task<string> CompleteLogin()
689+
{
635690
await getAllChats();
636691
SetSplitSizeGB();
637692
if (client.User.flags.HasFlag(User.Flags.premium))
@@ -696,8 +751,20 @@ public async Task<string> checkAuth(string number, bool isPhone = false)
696751
}
697752
else
698753
{
699-
_logger.LogInformation("No saved user data found, requesting phone");
700-
return "phone";
754+
// No saved phone (e.g. the session was created via QR login):
755+
// try to restore the session directly - Login(null) completes
756+
// silently when the stored session is still authorized and
757+
// returns the "phone" step otherwise.
758+
try
759+
{
760+
_logger.LogInformation("No saved user data found, attempting session restore");
761+
return await DoLogin(null) ?? "phone";
762+
}
763+
catch (Exception ex)
764+
{
765+
_logger.LogWarning(ex, "Session restore without saved user data failed, requesting phone");
766+
return "phone";
767+
}
701768
}
702769

703770
}

TelegramDownloader/Pages/Index.razor

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
<h1 class="login-title">Two-Factor Auth</h1>
4444
<p class="login-subtitle">Enter your 2FA password</p>
4545
break;
46+
case "qr":
47+
<h1 class="login-title">Scan QR Code</h1>
48+
<p class="login-subtitle">Log in with your phone, no number needed</p>
49+
break;
4650
case "ok":
4751
<h1 class="login-title">Welcome Back!</h1>
4852
<p class="login-subtitle">You are successfully authenticated</p>
@@ -81,6 +85,15 @@
8185
@bind-Value="Model!.value" placeholder="Enter your 2FA password" />
8286
</div>
8387
break;
88+
case "qr":
89+
<div class="text-center">
90+
<p class="login-subtitle" style="margin-bottom: 0;">
91+
Open Telegram on your phone and go to<br />
92+
<b>Settings &gt; Devices &gt; Link Desktop Device</b>,<br />
93+
then scan this code.
94+
</p>
95+
</div>
96+
break;
8497
case "ok":
8598
<div class="text-center">
8699
<div class="success-badge">
@@ -98,6 +111,12 @@
98111
<i class="bi bi-box-arrow-right"></i> Disconnect
99112
</button>
100113
}
114+
else if (Model.type == "qr")
115+
{
116+
<button class="btn-secondary-custom" type="button" @onclick="CancelQrLogin">
117+
<i class="bi bi-arrow-left"></i> Back to phone login
118+
</button>
119+
}
101120
else
102121
{
103122
<button class="btn-primary-custom" type="submit">
@@ -119,6 +138,12 @@
119138
</button>
120139
}
121140
}
141+
@if (Model.type == "phone")
142+
{
143+
<button class="btn-secondary-custom" type="button" @onclick="StartQrLogin">
144+
<i class="bi bi-qr-code-scan"></i> Log in with QR code
145+
</button>
146+
}
122147
</div>
123148

124149
@if (!string.IsNullOrEmpty(imageString))
@@ -248,8 +273,75 @@
248273
}
249274
}
250275

276+
private bool qrMode = false;
277+
278+
private async Task StartQrLogin()
279+
{
280+
qrMode = true;
281+
imageString = null;
282+
Model.type = "qr";
283+
source?.Cancel();
284+
source = new CancellationTokenSource();
285+
StateHasChanged();
286+
TelegramService.QrPasswordNeeded += OnQrPasswordNeeded;
287+
try
288+
{
289+
var user = await ts.CallQrGenerator(url => { _ = InvokeAsync(() => GenerateQR(url)); }, source.Token);
290+
if (user != null)
291+
{
292+
imageString = null;
293+
Model.type = "ok";
294+
await InvokeAsync(StateHasChanged);
295+
await isLogin();
296+
}
297+
}
298+
catch (OperationCanceledException)
299+
{
300+
// User went back to phone login or left the page
301+
}
302+
catch (Exception ex)
303+
{
304+
Console.WriteLine($"QR login failed: {ex.Message}");
305+
qrMode = false;
306+
imageString = null;
307+
Model.type = "phone";
308+
await InvokeAsync(StateHasChanged);
309+
}
310+
finally
311+
{
312+
TelegramService.QrPasswordNeeded -= OnQrPasswordNeeded;
313+
}
314+
}
315+
316+
private void OnQrPasswordNeeded(object sender, System.EventArgs e)
317+
{
318+
// The QR was accepted on the phone but the account has 2FA: switch the
319+
// form to the password step; Submit routes the value back to the
320+
// pending QR login.
321+
imageString = null;
322+
Model.value = "";
323+
Model.type = "pass";
324+
_ = InvokeAsync(StateHasChanged);
325+
}
326+
327+
private void CancelQrLogin()
328+
{
329+
qrMode = false;
330+
imageString = null;
331+
source?.Cancel();
332+
Model.type = "phone";
333+
}
334+
251335
private async void Submit()
252336
{
337+
if (qrMode && Model.type == "pass")
338+
{
339+
// 2FA password for a QR login: hand it to the waiting login task,
340+
// which finishes the flow and navigates from StartQrLogin.
341+
ts.ProvideQrLoginPassword(Model.value);
342+
Model.value = "";
343+
return;
344+
}
253345
if (Model.type == "phone")
254346
{
255347
var phone = await JSRuntime.InvokeAsync<string>("getNumber");
@@ -308,6 +400,7 @@
308400

309401
public void Dispose()
310402
{
403+
TelegramService.QrPasswordNeeded -= OnQrPasswordNeeded;
311404
if (source != null)
312405
{
313406
if (source.Token.CanBeCanceled)

0 commit comments

Comments
 (0)