Skip to content

Commit 8ff954e

Browse files
authored
Merge pull request #110 from mateof/feature/qr-login
feat: QR code login option in the web login page
2 parents b6874fb + f86d1e2 commit 8ff954e

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
@@ -44,6 +44,10 @@
4444
<h1 class="login-title">Two-Factor Auth</h1>
4545
<p class="login-subtitle">Enter your 2FA password</p>
4646
break;
47+
case "qr":
48+
<h1 class="login-title">Scan QR Code</h1>
49+
<p class="login-subtitle">Log in with your phone, no number needed</p>
50+
break;
4751
case "ok":
4852
<h1 class="login-title">Welcome Back!</h1>
4953
<p class="login-subtitle">You are successfully authenticated</p>
@@ -82,6 +86,15 @@
8286
@bind-Value="Model!.value" placeholder="Enter your 2FA password" />
8387
</div>
8488
break;
89+
case "qr":
90+
<div class="text-center">
91+
<p class="login-subtitle" style="margin-bottom: 0;">
92+
Open Telegram on your phone and go to<br />
93+
<b>Settings &gt; Devices &gt; Link Desktop Device</b>,<br />
94+
then scan this code.
95+
</p>
96+
</div>
97+
break;
8598
case "ok":
8699
<div class="text-center">
87100
<div class="success-badge">
@@ -99,6 +112,12 @@
99112
<i class="bi bi-box-arrow-right"></i> Disconnect
100113
</button>
101114
}
115+
else if (Model.type == "qr")
116+
{
117+
<button class="btn-secondary-custom" type="button" @onclick="CancelQrLogin">
118+
<i class="bi bi-arrow-left"></i> Back to phone login
119+
</button>
120+
}
102121
else
103122
{
104123
<button class="btn-primary-custom" type="submit">
@@ -120,6 +139,12 @@
120139
</button>
121140
}
122141
}
142+
@if (Model.type == "phone")
143+
{
144+
<button class="btn-secondary-custom" type="button" @onclick="StartQrLogin">
145+
<i class="bi bi-qr-code-scan"></i> Log in with QR code
146+
</button>
147+
}
123148
</div>
124149

125150
@if (!string.IsNullOrEmpty(imageString))
@@ -249,8 +274,75 @@
249274
}
250275
}
251276

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

334426
public void Dispose()
335427
{
428+
TelegramService.QrPasswordNeeded -= OnQrPasswordNeeded;
336429
if (source != null)
337430
{
338431
if (source.Token.CanBeCanceled)

0 commit comments

Comments
 (0)