// ValoLogin — вход в аккаунты Riot для Discord-бота магазина Valorant. // // Что делает: // 1. Спрашивает код из команды /login в Discord и проверяет его на сервере бота. // 2. Показывает официальную страницу входа Riot во встроенном браузере WebView2 (чистый временный профиль). // 3. Когда Riot после входа перенаправляет на playvalorant.com, забирает куки сессии auth.riotgames.com // и отправляет их боту вместе с кодом. Сам переход на playvalorant.com отменяется. // 4. Стирает куки и даёт войти в следующий аккаунт. При выходе временная папка удаляется. // Пароль вводится только на странице Riot: программа его не читает, не хранит и никуда не отправляет. // // Сборка — build.cmd (csc из .NET Framework 4.x; DLL WebView2 из NuGet-пакета Microsoft.Web.WebView2 вшиты ресурсами). using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Net; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using System.Web.Script.Serialization; using System.Windows.Forms; using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.WinForms; [assembly: AssemblyTitle("ValoLogin")] [assembly: AssemblyDescription("Вход в аккаунты Riot для Discord-бота магазина Valorant")] [assembly: AssemblyProduct("ValoLogin")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] static class Program { public const string DefaultServer = "https://valo.thisisforgotten.com"; public static string LibDir; static readonly string[] Libs = { "Microsoft.Web.WebView2.Core.dll", "Microsoft.Web.WebView2.WinForms.dll", "WebView2Loader.dll" }; [DllImport("user32.dll")] static extern bool SetProcessDPIAware(); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] static extern bool SetDllDirectory(string path); [STAThread] static int Main(string[] args) { string server = DefaultServer, code = "", selftest = null; for (int i = 0; i < args.Length; i++) { if (args[i] == "--server" && i + 1 < args.Length) server = args[++i].TrimEnd('/'); else if (args[i] == "--selftest" && i + 1 < args.Length) selftest = args[++i]; else code = args[i]; } string tmp = Path.Combine(Path.GetTempPath(), "ValoLogin-" + Guid.NewGuid().ToString("N").Substring(0, 12)); LibDir = Path.Combine(tmp, "lib"); Directory.CreateDirectory(LibDir); Assembly self = Assembly.GetExecutingAssembly(); foreach (string name in Libs) { using (Stream src = self.GetManifestResourceStream(name)) using (FileStream dst = File.Create(Path.Combine(LibDir, name))) src.CopyTo(dst); } SetDllDirectory(LibDir); AppDomain.CurrentDomain.AssemblyResolve += OnResolve; try { SetProcessDPIAware(); } catch { } ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); int rc = Ui.Run(server, code, Path.Combine(tmp, "profile"), selftest); // WebView2 держит файлы профиля до выхода процесса — папку удалит отдельный cmd через пару секунд try { var psi = new ProcessStartInfo("cmd.exe", "/c ping -n 5 127.0.0.1 >nul & rmdir /s /q \"" + tmp + "\""); psi.CreateNoWindow = true; psi.UseShellExecute = false; psi.WindowStyle = ProcessWindowStyle.Hidden; Process.Start(psi); } catch { } return rc; } static Assembly OnResolve(object sender, ResolveEventArgs e) { string path = Path.Combine(LibDir, new AssemblyName(e.Name).Name + ".dll"); return File.Exists(path) ? Assembly.LoadFrom(path) : null; } } // Отдельный класс: типы WebView2 загружаются только после того, как Main подключил вшитые DLL. static class Ui { public static int Run(string server, string code, string profileDir, string selftest) { var form = new LoginForm(server, code, profileDir, selftest); Application.Run(form); return form.ExitCode; } } class LoginForm : Form { const string AuthUrl = "https://auth.riotgames.com/authorize?redirect_uri=https%3A%2F%2Fplayvalorant.com%2Fopt_in&client_id=play-valorant-web-prod&response_type=token%20id_token&scope=account%20openid&nonce=1"; static readonly Color Dark = Color.FromArgb(15, 25, 35); static readonly Color Accent = Color.FromArgb(255, 70, 85); static readonly Color Good = Color.FromArgb(20, 120, 60); public int ExitCode = 1; readonly string server, profileDir, selftest; readonly float k = 1f; string code; readonly WebView2 web = new WebView2(); readonly Panel codePanel = new Panel(), resultPanel = new Panel(); readonly Label header = new Label(), codeStatus = new Label(), resultText = new Label(); readonly TextBox codeBox = new TextBox(); readonly Button goButton = new Button(), moreButton = new Button(), doneButton = new Button(); bool coreReady, capturing; int added; public LoginForm(string server, string code, string profileDir, string selftest) { this.server = server; this.code = code; this.profileDir = profileDir; this.selftest = selftest; using (Graphics g = CreateGraphics()) k = g.DpiX / 96f; Text = "ValoLogin — вход в Riot для бота магазина Valorant"; Font = new Font("Segoe UI", 10f); AutoScaleMode = AutoScaleMode.None; ClientSize = new Size(S(540), S(800)); StartPosition = FormStartPosition.CenterScreen; BackColor = Color.White; header.Dock = DockStyle.Top; header.Height = S(40); header.BackColor = Dark; header.ForeColor = Color.White; header.TextAlign = ContentAlignment.MiddleLeft; header.Padding = new Padding(S(12), 0, S(12), 0); header.Text = "ValoLogin · бот магазина Valorant"; web.Dock = DockStyle.Fill; web.Visible = false; BuildCodePanel(); BuildResultPanel(); resultPanel.Visible = false; // порядок важен: элемент с Dock=Fill добавляется первым, шапка Dock=Top — последней Controls.Add(web); Controls.Add(codePanel); Controls.Add(resultPanel); Controls.Add(header); Load += OnLoad; FormClosing += delegate { try { if (coreReady) web.CoreWebView2.CookieManager.DeleteAllCookies(); } catch { } }; } int S(int px) { return (int)Math.Round(px * k); } Label MakeLabel(string text, float size, FontStyle style, Color color) { var l = new Label(); l.Text = text; l.AutoSize = true; l.MaximumSize = new Size(S(470), 0); l.Font = new Font("Segoe UI", size, style); l.ForeColor = color; l.Margin = new Padding(0, 0, 0, S(12)); return l; } void StyleButton(Button b, string text, bool primary) { b.Text = text; b.AutoSize = true; b.Padding = new Padding(S(14), S(6), S(14), S(6)); b.FlatStyle = FlatStyle.Flat; b.Cursor = Cursors.Hand; b.BackColor = primary ? Accent : Color.White; b.ForeColor = primary ? Color.White : Dark; b.FlatAppearance.BorderColor = primary ? Accent : Color.Silver; b.Font = new Font("Segoe UI", 10.5f, FontStyle.Bold); b.Margin = new Padding(0, 0, S(10), 0); } FlowLayoutPanel Column() { var f = new FlowLayoutPanel(); f.Dock = DockStyle.Fill; f.FlowDirection = FlowDirection.TopDown; f.WrapContents = false; f.AutoScroll = true; return f; } void BuildCodePanel() { codePanel.Dock = DockStyle.Fill; codePanel.Padding = new Padding(S(28)); codePanel.BackColor = Color.White; FlowLayoutPanel flow = Column(); flow.Controls.Add(MakeLabel("Вход в аккаунты Riot", 17f, FontStyle.Bold, Dark)); flow.Controls.Add(MakeLabel( "1. В Discord набери /login — бот покажет код.\n" + "2. Вставь код сюда и нажми «Продолжить».\n" + "3. Войди в аккаунт Riot и поставь галку «Оставаться в системе».\n" + "4. После входа можно сразу добавить следующий аккаунт.", 10.5f, FontStyle.Regular, Color.FromArgb(40, 40, 40))); flow.Controls.Add(MakeLabel( "Пароль вводится только на странице Riot — программа его не видит и никуда не отправляет. " + "Боту уходит только кука сессии входа.", 9.5f, FontStyle.Regular, Color.DimGray)); codeBox.Font = new Font("Consolas", 18f); codeBox.Width = S(270); codeBox.CharacterCasing = CharacterCasing.Upper; codeBox.Margin = new Padding(0, S(14), 0, S(10)); codeBox.Text = code; codeBox.KeyDown += (s, e) => { if (e.KeyCode == Keys.Enter) { e.SuppressKeyPress = true; goButton.PerformClick(); } }; flow.Controls.Add(codeBox); StyleButton(goButton, "Продолжить", true); goButton.Click += async (s, e) => await CheckCode(); flow.Controls.Add(goButton); codeStatus.AutoSize = true; codeStatus.MaximumSize = new Size(S(470), 0); codeStatus.Margin = new Padding(0, S(12), 0, 0); flow.Controls.Add(codeStatus); codePanel.Controls.Add(flow); } void BuildResultPanel() { resultPanel.Dock = DockStyle.Fill; resultPanel.Padding = new Padding(S(28)); resultPanel.BackColor = Color.White; FlowLayoutPanel flow = Column(); resultText.AutoSize = true; resultText.MaximumSize = new Size(S(470), 0); resultText.Font = new Font("Segoe UI", 11.5f); resultText.Margin = new Padding(0, 0, 0, S(20)); flow.Controls.Add(resultText); var row = new FlowLayoutPanel(); row.AutoSize = true; row.FlowDirection = FlowDirection.LeftToRight; row.WrapContents = false; row.Margin = new Padding(0); StyleButton(moreButton, "Добавить ещё аккаунт", true); moreButton.Click += async (s, e) => await OpenLogin(); StyleButton(doneButton, "Готово", false); doneButton.Click += (s, e) => Close(); row.Controls.Add(moreButton); row.Controls.Add(doneButton); flow.Controls.Add(row); resultPanel.Controls.Add(flow); } void ShowOnly(Control c) { codePanel.Visible = c == codePanel; resultPanel.Visible = c == resultPanel; web.Visible = c == web; } void Status(string text, bool error) { codeStatus.Text = text; codeStatus.ForeColor = error ? Accent : Color.DimGray; } void SetResult(string text, Color color) { resultText.Text = text; resultText.ForeColor = color; } static string Norm(string s) { var sb = new StringBuilder(); foreach (char ch in (s ?? "").ToUpperInvariant()) if ((ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')) sb.Append(ch); return sb.ToString(); } async void OnLoad(object sender, EventArgs e) { if (selftest != null) { await SelfTest(); return; } ShowOnly(codePanel); codeBox.Focus(); if (Norm(code).Length == 8) await CheckCode(); } async Task CheckCode() { code = Norm(codeBox.Text); if (code.Length != 8) { Status("Код — 8 символов, как в /login (например ABCD-EFGH).", true); return; } goButton.Enabled = false; Status("Проверяю код…", false); var payload = new Dictionary(); payload["code"] = code; Dictionary r = await Api.PostAsync(server + "/api/check", payload); goButton.Enabled = true; if (!Api.Ok(r)) { Status(Api.Message(r, "Код не подошёл."), true); return; } header.Text = "Аккаунты добавляются для: " + Api.Str(r, "user"); await OpenLogin(); } async Task InitWebView() { if (coreReady) return; try { CoreWebView2Environment.SetLoaderDllFolderPath(Program.LibDir); } catch { } CoreWebView2Environment env = await CoreWebView2Environment.CreateAsync(null, profileDir); await web.EnsureCoreWebView2Async(env); coreReady = true; } async Task OpenLogin() { try { ShowOnly(web); if (!coreReady) { await InitWebView(); web.CoreWebView2.NavigationStarting += OnNavigationStarting; } else { web.CoreWebView2.CookieManager.DeleteAllCookies(); await web.CoreWebView2.Profile.ClearBrowsingDataAsync(); } capturing = false; web.CoreWebView2.Navigate(AuthUrl); } catch (WebView2RuntimeNotFoundException) { ShowOnly(codePanel); Status("Не найден компонент Microsoft Edge WebView2. Установи его с " + "https://go.microsoft.com/fwlink/p/?LinkId=2124703 и запусти программу снова.", true); } catch (Exception ex) { ShowOnly(codePanel); Status("Не удалось открыть окно входа: " + ex.Message, true); } } async void OnNavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e) { if (capturing || !e.Uri.StartsWith("https://playvalorant.com/", StringComparison.OrdinalIgnoreCase)) return; capturing = true; e.Cancel = true; // страница playvalorant не нужна — нужны куки auth.riotgames.com await CaptureLogin(); } async Task>> ReadCookies() { var list = new List>(); List cookies = await web.CoreWebView2.CookieManager.GetCookiesAsync("https://auth.riotgames.com"); foreach (CoreWebView2Cookie c in cookies) { var d = new Dictionary(); d["name"] = c.Name; d["value"] = c.Value; d["domain"] = c.Domain; d["path"] = c.Path; d["session"] = c.IsSession; list.Add(d); } return list; } static Dictionary Find(List> list, string name) { foreach (var d in list) if ((string)d["name"] == name) return d; return null; } async Task CaptureLogin() { ShowOnly(resultPanel); SetResult("Вход пойман, отправляю боту…", Dark); moreButton.Enabled = doneButton.Enabled = false; List> list = null; Dictionary ssid = null; for (int attempt = 0; attempt < 10 && ssid == null; attempt++) { if (attempt > 0) await Task.Delay(400); list = await ReadCookies(); ssid = Find(list, "ssid"); } if (ssid == null) { SetResult("Не нашёл куку входа (ssid). Нажми «Добавить ещё аккаунт» и войди заново.", Accent); moreButton.Enabled = doneButton.Enabled = true; return; } var payload = new Dictionary(); payload["code"] = code; payload["cookies"] = list; payload["persistent"] = !(bool)ssid["session"]; payload["client"] = "ValoLogin/1.0"; Dictionary r = await Api.PostAsync(server + "/api/link", payload); try { web.CoreWebView2.CookieManager.DeleteAllCookies(); } catch { } bool ok = Api.Ok(r); if (ok) added++; string text = Api.Message(r, "Сервер ответил непонятно."); string warn = Api.Str(r, "warning"); if (warn.Length > 0) text += "\n\n" + warn; if (added > 0) text += "\n\nДобавлено за этот запуск: " + added + "."; SetResult(text, ok ? Good : Accent); moreButton.Enabled = doneButton.Enabled = true; ExitCode = added > 0 ? 0 : 1; } // Самопроверка без входа: распаковка WebView2, загрузка страницы Riot, запросы к серверу с заведомо неверным кодом. async Task SelfTest() { var log = new StringBuilder(); var json = new JavaScriptSerializer(); try { var p = new Dictionary(); p["code"] = "SELFTEST"; log.AppendLine("check: " + json.Serialize(await Api.PostAsync(server + "/api/check", p))); ShowOnly(web); await InitWebView(); log.AppendLine("webview2: " + web.CoreWebView2.Environment.BrowserVersionString); var done = new TaskCompletionSource(); web.CoreWebView2.NavigationCompleted += (s, e) => done.TrySetResult(e.IsSuccess); web.CoreWebView2.Navigate(AuthUrl); await Task.WhenAny(done.Task, Task.Delay(30000)); log.AppendLine("page: " + web.CoreWebView2.Source); List> list = await ReadCookies(); var names = new List(); foreach (var d in list) names.Add((string)d["name"] + ((bool)d["session"] ? "(session)" : "")); log.AppendLine("cookies: " + string.Join(", ", names)); var p2 = new Dictionary(); p2["code"] = "SELFTEST"; p2["cookies"] = new List(); log.AppendLine("link: " + json.Serialize(await Api.PostAsync(server + "/api/link", p2))); ExitCode = 0; } catch (Exception ex) { log.AppendLine("error: " + ex); } File.WriteAllText(selftest, log.ToString(), new UTF8Encoding(false)); Close(); } } static class Api { public static Task> PostAsync(string url, object payload) { return Task.Run(() => Post(url, payload)); } static Dictionary Post(string url, object payload) { var json = new JavaScriptSerializer(); try { byte[] body = Encoding.UTF8.GetBytes(json.Serialize(payload)); var req = (HttpWebRequest)WebRequest.Create(url); req.Method = "POST"; req.ContentType = "application/json; charset=utf-8"; req.UserAgent = "ValoLogin/1.0"; req.Timeout = 90000; using (Stream s = req.GetRequestStream()) s.Write(body, 0, body.Length); string text; try { using (var resp = (HttpWebResponse)req.GetResponse()) using (var rd = new StreamReader(resp.GetResponseStream(), Encoding.UTF8)) text = rd.ReadToEnd(); } catch (WebException ex) { if (ex.Response == null) throw; using (var rd = new StreamReader(ex.Response.GetResponseStream(), Encoding.UTF8)) text = rd.ReadToEnd(); } var r = json.Deserialize>(text); return r ?? Fail("Пустой ответ сервера бота."); } catch (Exception ex) { return Fail("Сервер бота не отвечает: " + ex.Message); } } static Dictionary Fail(string message) { var d = new Dictionary(); d["ok"] = false; d["message"] = message; return d; } public static bool Ok(Dictionary r) { object v; return r.TryGetValue("ok", out v) && v is bool && (bool)v; } public static string Str(Dictionary r, string key) { object v; return r.TryGetValue(key, out v) && v != null ? v.ToString() : ""; } public static string Message(Dictionary r, string fallback) { string m = Str(r, "message"); return m.Length > 0 ? m : fallback; } }