nitai/projects
rblxtool / src / ui / RblxTool / Views / AccountsPage.xaml.cs
218 lines · 6.9 KB Raw
1using System.Collections.ObjectModel;
2using Microsoft.UI.Xaml;
3using Microsoft.UI.Xaml.Controls;
4using RblxTool.Models;
5using RblxTool.Services;
6
7namespace RblxTool.Views;
8
9public sealed partial class AccountsPage : Page
10{
11 private readonly ObservableCollection<AccountSummary> _accounts = new();
12 private string? _pendingLoginId;
13 private string? _oauthVerifier;
14
15 public AccountsPage()
16 {
17 InitializeComponent();
18 AccountList.ItemsSource = _accounts;
19 Loaded += (_, _) => Reload();
20 }
21
22 private void Reload()
23 {
24 _accounts.Clear();
25 foreach (var account in AppState.Current.Accounts())
26 {
27 _accounts.Add(account);
28 }
29 var active = AppState.Current.Active;
30 if (active is not null)
31 {
32 AccountList.SelectedItem = _accounts.FirstOrDefault(a => a.Id == active.Id);
33 }
34 }
35
36 private void Status(string message) => StatusText.Text = message;
37
38 private async void OnAddCookie(object sender, RoutedEventArgs e)
39 {
40 var cookie = CookieBox.Password.Trim();
41 if (cookie.Length == 0)
42 {
43 Status("paste a cookie first");
44 return;
45 }
46 var label = CookieLabelBox.Text.Trim();
47 Status("validating cookie...");
48 try
49 {
50 var summary = await Task.Run(() =>
51 AppState.Current.Engine.AddCookie(cookie, label.Length == 0 ? null : label));
52 CookieBox.Password = string.Empty;
53 Status($"added {summary.Username} ({summary.UserId})");
54 AppState.Current.NotifyAccountsChanged();
55 Reload();
56 }
57 catch (Exception error)
58 {
59 Status(error.Message);
60 }
61 }
62
63 private async void OnLogin(object sender, RoutedEventArgs e)
64 {
65 var identifier = UsernameBox.Text.Trim();
66 var password = PasswordBox.Password;
67 if (identifier.Length == 0 || password.Length == 0)
68 {
69 Status("username and password are required");
70 return;
71 }
72 Status("signing in...");
73 var result = await Task.Run(() => AppState.Current.Engine.Login(identifier, password, null));
74 if (!result.Ok)
75 {
76 Status(result.Error ?? "login failed");
77 return;
78 }
79 switch (result.State)
80 {
81 case "authenticated":
82 PasswordBox.Password = string.Empty;
83 Status($"signed in as {result.Account?.Username}");
84 AppState.Current.NotifyAccountsChanged();
85 Reload();
86 break;
87 case "two_step":
88 _pendingLoginId = result.PendingId;
89 TwoStepPanel.Visibility = Visibility.Visible;
90 TwoStepHint.Text = $"enter the code sent via {result.MediaType}";
91 Status("two step verification required");
92 break;
93 case "challenge":
94 Status($"roblox raised a {result.ChallengeType} captcha challenge; solve it in the browser window");
95 await ShowChallengeAsync(result);
96 break;
97 default:
98 Status("unexpected login state");
99 break;
100 }
101 }
102
103 private async Task ShowChallengeAsync(LoginResult result)
104 {
105 var dialog = new ContentDialog
106 {
107 XamlRoot = XamlRoot,
108 Title = "Captcha challenge",
109 CloseButtonText = "Close",
110 Content = new TextBlock
111 {
112 TextWrapping = TextWrapping.Wrap,
113 Text =
114 "Roblox requires a FunCaptcha before this login can continue.\n\n" +
115 $"challenge id: {result.ChallengeId}\n" +
116 $"arkose key: {result.ArkoseKey}\n\n" +
117 "Sign in once in a browser on this machine, then paste the resulting .ROBLOSECURITY cookie " +
118 "into the cookie card above. Cookie sessions never hit this challenge again.",
119 },
120 };
121 await dialog.ShowAsync();
122 }
123
124 private async void OnSubmitCode(object sender, RoutedEventArgs e)
125 {
126 if (_pendingLoginId is null)
127 {
128 return;
129 }
130 var code = CodeBox.Text.Trim();
131 try
132 {
133 var summary = await Task.Run(() =>
134 AppState.Current.Engine.SubmitTwoStep(_pendingLoginId, code));
135 _pendingLoginId = null;
136 TwoStepPanel.Visibility = Visibility.Collapsed;
137 PasswordBox.Password = string.Empty;
138 CodeBox.Text = string.Empty;
139 Status($"signed in as {summary.Username}");
140 AppState.Current.NotifyAccountsChanged();
141 Reload();
142 }
143 catch (Exception error)
144 {
145 Status(error.Message);
146 }
147 }
148
149 private OauthRequest BuildOauthRequest() => new()
150 {
151 ClientId = ClientIdBox.Text.Trim(),
152 RedirectUri = RedirectBox.Text.Trim(),
153 };
154
155 private async void OnOauthUrl(object sender, RoutedEventArgs e)
156 {
157 var request = BuildOauthRequest();
158 if (request.ClientId.Length == 0)
159 {
160 Status("client id is required");
161 return;
162 }
163 var result = AppState.Current.Engine.OauthUrl(request);
164 if (!result.Ok || result.Url is null)
165 {
166 Status(result.Error ?? "could not build the authorize url");
167 return;
168 }
169 _oauthVerifier = result.Verifier;
170 await Windows.System.Launcher.LaunchUriAsync(new Uri(result.Url));
171 Status("approve in the browser, then paste the returned code");
172 }
173
174 private async void OnOauthExchange(object sender, RoutedEventArgs e)
175 {
176 if (_oauthVerifier is null)
177 {
178 Status("open the authorize page first");
179 return;
180 }
181 var code = OauthCodeBox.Text.Trim();
182 var request = BuildOauthRequest();
183 try
184 {
185 var summary = await Task.Run(() =>
186 AppState.Current.Engine.OauthExchange(request, code, _oauthVerifier));
187 Status($"linked {summary.Username} (read only)");
188 AppState.Current.NotifyAccountsChanged();
189 Reload();
190 }
191 catch (Exception error)
192 {
193 Status(error.Message);
194 }
195 }
196
197 private void OnAccountSelected(object sender, SelectionChangedEventArgs e)
198 {
199 if (AccountList.SelectedItem is AccountSummary account)
200 {
201 AppState.Current.SetActive(account);
202 }
203 }
204
205 private void OnRemove(object sender, RoutedEventArgs e)
206 {
207 if (AccountList.SelectedItem is not AccountSummary account)
208 {
209 return;
210 }
211 AppState.Current.Engine.Remove(account.Id);
212 AppState.Current.NotifyAccountsChanged();
213 Reload();
214 Status($"removed {account.Username}");
215 }
216
217 private void OnRefresh(object sender, RoutedEventArgs e) => Reload();
218}