From 30cf049f23c6a34f194562e18b5d3b30d128d46c Mon Sep 17 00:00:00 2001 From: achiez Date: Tue, 12 May 2026 16:36:37 +0300 Subject: [PATCH 1/3] refactor(auth): redesign Steam audience and web domain authorization model - Rename AuthorizedDomains to WebDomains - Introduce explicit Steam audience constants and web audience mappings - Improve Steam token/domain resolution and cookie installation flow - Allow mobile-issued tokens with "web" audience to be used for web domains - Align authorization logic closer to actual Steam audience behavior - Rename cookie APIs from Add* to Set* where appropriate --- src/NebulaAuth/Model/MAAC/PortableMaClient.cs | 2 +- src/NebulaAuth/Model/MaClient.cs | 2 +- .../SessionHandler/SessionHandler_API.cs | 2 +- .../Authentication/AdmissionHelper.cs | 154 +++++++++--------- src/SteamLibForked/Core/SteamDomains.cs | 64 ++++---- .../Models/Core/Session/MobileSessionData.cs | 34 ++-- src/SteamLibForked/Web/ClientBuilder.cs | 2 +- 7 files changed, 136 insertions(+), 124 deletions(-) diff --git a/src/NebulaAuth/Model/MAAC/PortableMaClient.cs b/src/NebulaAuth/Model/MAAC/PortableMaClient.cs index 6395256..23555e6 100644 --- a/src/NebulaAuth/Model/MAAC/PortableMaClient.cs +++ b/src/NebulaAuth/Model/MAAC/PortableMaClient.cs @@ -62,7 +62,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable SetStatus(newStatus); ClientHandler.CookieContainer.ClearAllCookies(); - ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData); + ClientHandler.CookieContainer.SetSteamMobileCookies(sessionData); } diff --git a/src/NebulaAuth/Model/MaClient.cs b/src/NebulaAuth/Model/MaClient.cs index 115f5e3..74598a2 100644 --- a/src/NebulaAuth/Model/MaClient.cs +++ b/src/NebulaAuth/Model/MaClient.cs @@ -38,7 +38,7 @@ public static class MaClient { ClientHandler.CookieContainer.ClearAllCookies(); if (account == null) return; - ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData); + ClientHandler.CookieContainer.SetSteamMobileCookies(account.SessionData); Proxy.SetData(account.Proxy?.Data); } diff --git a/src/NebulaAuth/Model/SessionHandler/SessionHandler_API.cs b/src/NebulaAuth/Model/SessionHandler/SessionHandler_API.cs index b644462..fe40011 100644 --- a/src/NebulaAuth/Model/SessionHandler/SessionHandler_API.cs +++ b/src/NebulaAuth/Model/SessionHandler/SessionHandler_API.cs @@ -30,7 +30,7 @@ public partial class SessionHandler //API //Trigger PropertyChanged event for PortableMaClient handling session updated from MaClient mafile.SetSessionData(mafile.SessionData); await Storage.UpdateMafileAsync(mafile); - chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData); + chp.Handler.CookieContainer.SetSteamMobileCookies(mafile.SessionData); } public static async Task LoginAgain(HttpClientHandlerPair chp, Mafile mafile, string password, bool savePassword) diff --git a/src/SteamLibForked/Authentication/AdmissionHelper.cs b/src/SteamLibForked/Authentication/AdmissionHelper.cs index 30ab7b2..57ece1c 100644 --- a/src/SteamLibForked/Authentication/AdmissionHelper.cs +++ b/src/SteamLibForked/Authentication/AdmissionHelper.cs @@ -54,33 +54,20 @@ public static class AdmissionHelper return; } - - AddRefreshToken(container, sessionData.RefreshToken); + container.SetSteamRefreshToken(sessionData.RefreshToken); var community = SteamDomains.GetDomainUri(SteamDomain.Community); container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, sessionData.SessionId, "/")); container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage, "/")); TransferCommunityCookies(container); - foreach (var domain in SteamDomains.AuthDomains) + foreach (var domain in SteamDomains.WebDomains) { var token = sessionData.GetToken(domain); if (token == null) continue; - AddTokenCookie(container, token.Value); + container.SetSteamAccessToken(token.Value); } } - public static void SetDomainCookie(this CookieContainer container, SteamDomain domain, SteamAuthToken token) - { - var uri = SteamDomains.GetDomainUri(domain); - foreach (var cookie in container.GetCookies(uri) - .Where(c => !c.Expired && c.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME))) - { - cookie.Expired = true; - } - - AddTokenCookie(container, token); - } - /// /// Clear and set new session /// @@ -95,70 +82,22 @@ public static class AdmissionHelper return; } - AddRefreshToken(container, mobileSession.RefreshToken); + container.SetSteamRefreshToken(mobileSession.RefreshToken); var community = SteamDomains.GetDomainUri(SteamDomain.Community); container.Add(community, new Cookie("steamid", mobileSession.SteamId.Steam64.ToString())); container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, mobileSession.SessionId)); container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage)); TransferCommunityCookies(container); - foreach (var domain in SteamDomains.AuthDomains) + foreach (var domain in SteamDomains.WebDomains) { var token = mobileSession.GetToken(domain); if (token == null) continue; - AddTokenCookie(container, token.Value); + var domainUri = SteamDomains.GetDomainUri(domain); + container.SetSteamAccessTokenUnsafe(token.Value, domainUri); } } - /// - /// Clear and set new session. Not recommended. Uses for domain - /// instead of its own cookie. It's okay to use it only for confirmations. But - /// Market, Trading and other pages won't be authorized - /// - public static void SetSteamMobileCookiesWithMobileToken(this CookieContainer container, - IMobileSessionData? mobileSession, - string setLanguage = "english") - { - container.ClearSteamCookies(setLanguage); - container.AddMinimalMobileCookies(); - - if (mobileSession == null) - { - TransferCommunityCookies(container); - return; - } - - - AddRefreshToken(container, mobileSession.RefreshToken); - - var community = SteamDomains.GetDomainUri(SteamDomain.Community); - container.Add(community, new Cookie("steamid", mobileSession.SteamId.Steam64.ToString())); - container.Add(community, new Cookie(SESSION_ID_COOKIE_NAME, mobileSession.SessionId)); - container.Add(community, new Cookie(LANGUAGE_COOKIE_NAME, setLanguage)); - TransferCommunityCookies(container); - - var domainCookieSet = false; - foreach (var domain in SteamDomains.AllDomains) - { - var token = mobileSession.GetToken(domain); - if (token == null || token.Value.IsExpired) continue; - if (domain == SteamDomain.Community) - domainCookieSet = true; - AddTokenCookie(container, token.Value); - } - - var mobileToken = mobileSession.GetMobileToken(); - if (!domainCookieSet && mobileToken is {IsExpired: false}) - { - var domain = SteamDomains.GetDomainUri(SteamDomain.Community); - container.Add(domain, new Cookie(ACCESS_COOKIE_NAME, mobileToken.Value.SignedToken) - { - HttpOnly = true, - Secure = true, - Expires = mobileToken.Value.Expires.ToLocalDateTime() - }); - } - } public static void AddMinimalMobileCookies(this CookieContainer container) { @@ -229,27 +168,93 @@ public static class AdmissionHelper } } - public static void AddRefreshToken(CookieContainer container, SteamAuthToken token) + /// + /// Sets a Steam refresh token as a cookie to the specified cookie container. + /// + /// + /// The added cookie will have its expiration set according to the token's expiration time. This + /// method is typically used to enable authenticated requests to Steam services that require a refresh + /// token. + /// + /// The cookie container to which the Steam refresh token cookie will be added. Cannot be null. + /// + /// The Steam authentication token to add as a refresh cookie. Must be of type Refresh or + /// MobileRefresh. + /// + /// Thrown if the token is not of type Refresh or MobileRefresh. + public static void SetSteamRefreshToken(this CookieContainer container, SteamAuthToken token) { if (token.Type is not (SteamAccessTokenType.Refresh or SteamAccessTokenType.MobileRefresh)) throw new ArgumentException( $"Token must be of type Refresh or MobileRefresh. Provided token has type: {token.Type}", nameof(token)); - var refreshToken = token.SignedToken; - container.Add(SteamLoginUri, new Cookie(REFRESH_COOKIE_NAME, refreshToken) + SetSteamRefreshTokenUnsafe(container, token, SteamLoginUri); + } + + /// + /// Sets a Steam refresh token as a cookie to the specified cookie container for the given domain. + /// + /// + /// This method does not perform validation on the input parameters. Callers must ensure that the + /// provided values are valid and appropriate for use. + /// + /// The cookie container to which the refresh token cookie will be added. Cannot be null. + /// + /// The Steam authentication token containing the signed token value and expiration information. Cannot + /// be null. + /// + /// The URI of the domain for which the refresh token cookie should be set. Cannot be null. + public static void SetSteamRefreshTokenUnsafe(CookieContainer container, SteamAuthToken token, Uri domainUri) + { + container.Add(domainUri, new Cookie(REFRESH_COOKIE_NAME, token.SignedToken) { Expires = token.Expires.ToLocalDateTime() }); } - public static void AddTokenCookie(CookieContainer container, SteamAuthToken token) + /// + /// Sets a Steam access token to the specified cookie container for use with Steam web requests. + /// + /// + /// This method is intended for standard web access tokens bound to a specific Steam web domain. + /// Mobile tokens are intentionally rejected, since their audiences are capability-based rather + /// than domain-based and may grant access to multiple web domains. + /// + /// The cookie container to which the Steam access token will be added. Cannot be null. + /// + /// The Steam access token to add. Must be of type AccessToken and associated with a valid Steam + /// domain. + /// + /// Thrown if the token is not of type AccessToken. + public static void SetSteamAccessToken(this CookieContainer container, SteamAuthToken token) { + if (token.Type == SteamAccessTokenType.Mobile) + throw new ArgumentException( + $"Mobile access tokens cannot be added using this method. Use SetSteamAccessTokenUnsafe instead. Provided token has type: {token.Type}", + nameof(token)); if (token.Type is not SteamAccessTokenType.AccessToken) throw new ArgumentException($"Token must be of type AccessToken. Provided token has type: {token.Type}", nameof(token)); - var domain = SteamDomains.GetDomainUri(token.Domain); - container.Add(domain, new Cookie(ACCESS_COOKIE_NAME, token.SignedToken) + var domainUri = SteamDomains.GetDomainUri(token.Domain); + container.SetSteamAccessTokenUnsafe(token, domainUri); + } + + + /// + /// Sets a Steam access token as a secure, HTTP-only cookie to the specified cookie container for the given domain. + /// + /// + /// This method does not perform validation on the input parameters and should only be used when + /// input values are trusted. The added cookie is marked as secure and HTTP-only, and its expiration is set + /// according to the token's expiration time. + /// + /// The cookie container to which the Steam access token cookie will be added. Cannot be null. + /// The Steam access token to add as a cookie. Must contain a valid signed token and expiration. + /// The URI of the domain for which the cookie will be set. Cannot be null. + public static void SetSteamAccessTokenUnsafe(this CookieContainer container, SteamAuthToken token, Uri domainUri) + { + container.Add(domainUri, new Cookie(ACCESS_COOKIE_NAME, token.SignedToken) { HttpOnly = true, Secure = true, @@ -257,7 +262,6 @@ public static class AdmissionHelper }); } - public static bool IsSteamCookie(Cookie cookie) { return cookie.Domain.Contains("steamcommunity.com") || cookie.Domain.Contains("steampowered.com") || diff --git a/src/SteamLibForked/Core/SteamDomains.cs b/src/SteamLibForked/Core/SteamDomains.cs index a74737c..5be37e7 100644 --- a/src/SteamLibForked/Core/SteamDomains.cs +++ b/src/SteamLibForked/Core/SteamDomains.cs @@ -1,49 +1,43 @@ -using System.Collections.ObjectModel; -using SteamLibForked.Models.Core; +using SteamLibForked.Models.Core; +using System.Collections.Immutable; namespace SteamLib.Core; public static class SteamDomains { public static IReadOnlyDictionary Domains { get; } = - new ReadOnlyDictionary( - new Dictionary - { - {SteamDomain.Community, SteamConstants.STEAM_COMMUNITY}, - {SteamDomain.Store, SteamConstants.STEAM_STORE}, - {SteamDomain.Help, SteamConstants.STEAM_HELP}, - {SteamDomain.TV, SteamConstants.STEAM_TV}, - {SteamDomain.Checkout, SteamConstants.STEAM_CHECKOUT}, - {SteamDomain.Login, SteamConstants.STEAM_LOGIN}, - {SteamDomain.API, SteamConstants.STEAM_API} - }); + new Dictionary + { + {SteamDomain.Community, SteamConstants.STEAM_COMMUNITY}, + {SteamDomain.Store, SteamConstants.STEAM_STORE}, + {SteamDomain.Help, SteamConstants.STEAM_HELP}, + {SteamDomain.TV, SteamConstants.STEAM_TV}, + {SteamDomain.Checkout, SteamConstants.STEAM_CHECKOUT}, + {SteamDomain.Login, SteamConstants.STEAM_LOGIN}, + {SteamDomain.API, SteamConstants.STEAM_API} + }.ToImmutableDictionary(); public static IReadOnlyDictionary DomainUris { get; } - = new ReadOnlyDictionary( - Domains.ToDictionary(x => x.Key, x => new Uri(x.Value)) - ); + = Domains + .ToDictionary(x => x.Key, x => new Uri(x.Value)) + .ToImmutableDictionary(); - public static IEnumerable AllDomains { get; } = - [ - SteamDomain.Community, - SteamDomain.Store, - SteamDomain.Help, - SteamDomain.TV, - SteamDomain.Checkout, - SteamDomain.Login, - SteamDomain.API - ]; - - public static IEnumerable AuthDomains { get; } = - [ - SteamDomain.Community, - SteamDomain.Store, - SteamDomain.Help, - SteamDomain.TV, - SteamDomain.Checkout - ]; + /// + /// All known public Steam domains. + /// + public static IEnumerable AllDomains { get; } = ImmutableHashSet.Create(SteamDomain.Community, + SteamDomain.Store, SteamDomain.Help, SteamDomain.TV, SteamDomain.Checkout, SteamDomain.Login, SteamDomain.API); + /// + /// Steam web domains that participate in the standard login authorization flow. + /// + /// These domains use the web:* audience format and receive + /// authentication cookies/tokens during session initialization. + /// + /// + public static IReadOnlySet WebDomains { get; } = ImmutableHashSet.Create(SteamDomain.Community, + SteamDomain.Store, SteamDomain.Help, SteamDomain.TV, SteamDomain.Checkout); public static Uri GetDomainUri(SteamDomain domain) { diff --git a/src/SteamLibForked/Models/Core/Session/MobileSessionData.cs b/src/SteamLibForked/Models/Core/Session/MobileSessionData.cs index 5c228fc..e02b88c 100644 --- a/src/SteamLibForked/Models/Core/Session/MobileSessionData.cs +++ b/src/SteamLibForked/Models/Core/Session/MobileSessionData.cs @@ -1,13 +1,14 @@ -using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using Newtonsoft.Json; +using SteamLib.Core; using SteamLibForked.Abstractions; using SteamLibForked.Models.Core; +using System.Diagnostics.CodeAnalysis; namespace SteamLibForked.Models.Session; //WARNING: Any changes here should be reflected in MafileSerializer.cs -public sealed class MobileSessionData : SessionData, IMobileSessionData +public class MobileSessionData : SessionData, IMobileSessionData { public SteamAuthToken? MobileToken { get; private set; } @@ -31,26 +32,39 @@ public sealed class MobileSessionData : SessionData, IMobileSessionData return MobileToken; } + public override SteamAuthToken? GetToken(SteamDomain domain) + { + var isWeb = SteamDomains.WebDomains.Contains(domain); + if (isWeb) + { + // Mobile-issued tokens usually contain the "web" audience, + // so we assume they can also be used for all web:* domains. + // See: SteamAuthToken 'TODO' for more details + return MobileToken ?? base.GetToken(domain); + } + return base.GetToken(domain); + } + [MemberNotNull(nameof(MobileToken))] - public void SetMobileToken(SteamAuthToken token) + public virtual void SetMobileToken(SteamAuthToken token) { if (token.Type != SteamAccessTokenType.Mobile) throw new ArgumentException("Token must be of type MobileAccess", nameof(token)) { - Data = {{"ActualType", token.Type}} + Data = { { "ActualType", token.Type } } }; MobileToken = token; } - public override MobileSessionData Clone() - { - return (MobileSessionData) ((ISessionData) this).Clone(); - } - object ICloneable.Clone() { return new MobileSessionData(SessionId, SteamId, RefreshToken, MobileToken, Tokens); } + + public override MobileSessionData Clone() + { + return (MobileSessionData)((ISessionData)this).Clone(); + } } \ No newline at end of file diff --git a/src/SteamLibForked/Web/ClientBuilder.cs b/src/SteamLibForked/Web/ClientBuilder.cs index 9118d7a..28640fd 100644 --- a/src/SteamLibForked/Web/ClientBuilder.cs +++ b/src/SteamLibForked/Web/ClientBuilder.cs @@ -31,7 +31,7 @@ public static class ClientBuilder } else { - container.SetSteamMobileCookiesWithMobileToken(sessionData); + container.SetSteamMobileCookies(sessionData); } //Nebula tweak: From b5d3b4a8af49922474d05bf12862bd671f7ada7d Mon Sep 17 00:00:00 2001 From: achiez Date: Thu, 14 May 2026 16:44:14 +0300 Subject: [PATCH 2/3] feat(proxy): add bulk assignment and proxy usage stats - Implement bulk proxy assignment for accounts with flexible input (login:proxy, login:ID, or login) - Add UI for mass assignment with counters and behavior selection for unspecified proxies - Show assigned account count badges in proxy list for better visibility - Enable quick assignment of a free proxy to an account - Ensure fast and consistent proxy assignment state via in-memory cache - Perform ReSharper cleanup --- src/NebulaAuth/Converters/Converters.xaml | 1 + .../Converters/ProxyAccountCountConverter.cs | 25 ++ .../dialogs.proxymanager.loc.json | 110 ++++++ .../Localization/mainwindow.loc.json | 10 + src/NebulaAuth/MainWindow.xaml | 3 + src/NebulaAuth/Model/ProxyAssignmentCache.cs | 79 ++++ src/NebulaAuth/Model/Shell.cs | 1 + src/NebulaAuth/Theme/Brushes.xaml | 3 + src/NebulaAuth/View/ProxyManagerView.xaml | 364 ++++++++++++------ src/NebulaAuth/ViewModel/MainVM.cs | 2 +- src/NebulaAuth/ViewModel/MainVM_File.cs | 10 +- src/NebulaAuth/ViewModel/MainVM_Proxy.cs | 38 +- .../Other/ProxyManagerVM.Assignment.cs | 187 +++++++++ src/SteamLibForked/Core/SteamDomains.cs | 4 +- .../Models/Core/Session/MobileSessionData.cs | 9 +- .../MafileSerialization/LegacyMafile.cs | 9 +- 16 files changed, 710 insertions(+), 145 deletions(-) create mode 100644 src/NebulaAuth/Converters/ProxyAccountCountConverter.cs create mode 100644 src/NebulaAuth/Model/ProxyAssignmentCache.cs create mode 100644 src/NebulaAuth/ViewModel/Other/ProxyManagerVM.Assignment.cs diff --git a/src/NebulaAuth/Converters/Converters.xaml b/src/NebulaAuth/Converters/Converters.xaml index 97888f1..8ef5bd7 100644 --- a/src/NebulaAuth/Converters/Converters.xaml +++ b/src/NebulaAuth/Converters/Converters.xaml @@ -15,6 +15,7 @@ + diff --git a/src/NebulaAuth/Converters/ProxyAccountCountConverter.cs b/src/NebulaAuth/Converters/ProxyAccountCountConverter.cs new file mode 100644 index 0000000..f39d5b4 --- /dev/null +++ b/src/NebulaAuth/Converters/ProxyAccountCountConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using NebulaAuth.Model; + +namespace NebulaAuth.Converters; + +/// +/// Converts a proxy ID (int) to the number of accounts assigned to it. +/// Returns empty string when count is zero so the badge is invisible. +/// +public class ProxyAccountCountConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not int proxyId) return string.Empty; + var count = ProxyAssignmentCache.GetAccountCount(proxyId); + return count > 0 ? count.ToString() : string.Empty; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/NebulaAuth/Localization/dialogs.proxymanager.loc.json b/src/NebulaAuth/Localization/dialogs.proxymanager.loc.json index 07193f8..04a7baf 100644 --- a/src/NebulaAuth/Localization/dialogs.proxymanager.loc.json +++ b/src/NebulaAuth/Localization/dialogs.proxymanager.loc.json @@ -40,6 +40,26 @@ "es": "El proxy tiene un formato incorrecto", "tr": "Proxy yanlış formatta", "kk": "Прокси қате форматта" + }, + "AssignmentSuccess": { + "ru": "Прокси назначены: {0}", + "en": "Assigned: {0}", + "zh": "已分配:{0}", + "uk": "Проксі призначено: {0}", + "fr": "Attribués: {0}", + "es": "Asignados: {0}", + "tr": "Atandı: {0}", + "kk": "Тағайындалды: {0}" + }, + "AssignmentPartialSuccess": { + "ru": "Назначено: {0}, не найдено: {1}, ошибки: {2}", + "en": "Assigned: {0}, not found: {1}, errors: {2}", + "zh": "已分配:{0},未找到:{1},错误:{2}", + "uk": "Призначено: {0}, не знайдено: {1}, помилки: {2}", + "fr": "Attribués: {0}, non trouvés: {1}, erreurs: {2}", + "es": "Asignados: {0}, no encontrados: {1}, errores: {2}", + "tr": "Atandı: {0}, bulunamadı: {1}, hatalar: {2}", + "kk": "Тағайындалды: {0}, табылмады: {1}, қателер: {2}" } } }, @@ -93,6 +113,96 @@ "es": "Mostrar credenciales", "tr": "Kimlik bilgilerini göster", "kk": "Тіркелгі деректерін көрсету" + }, + "TabManager": { + "en": "Manager", + "ru": "Менеджер", + "zh": "管理", + "uk": "Менеджер", + "fr": "Gestionnaire", + "es": "Administrador", + "tr": "Yönetici", + "kk": "Менеджер" + }, + "TabAssignment": { + "en": "Assignment", + "ru": "Назначение", + "zh": "分配", + "uk": "Призначення", + "fr": "Attribution", + "es": "Asignación", + "tr": "Atama", + "kk": "Тағайындау" + }, + "TotalAccounts": { + "en": "Accounts", + "ru": "Аккаунтов", + "zh": "账户", + "uk": "Акаунтів", + "fr": "Comptes", + "es": "Cuentas", + "tr": "Hesaplar", + "kk": "Аккаунттар" + }, + "TotalProxies": { + "en": "Proxies", + "ru": "Прокси", + "zh": "代理", + "uk": "Проксі", + "fr": "Proxies", + "es": "Proxies", + "tr": "Proxy'ler", + "kk": "Проксилер" + }, + "AssignedProxies": { + "en": "Assigned", + "ru": "Назначено", + "zh": "已分配", + "uk": "Призначено", + "fr": "Attribués", + "es": "Asignados", + "tr": "Atandı", + "kk": "Тағайындалды" + }, + "UnspecifiedBehavior": { + "en": "Unspecified proxy:", + "ru": "Не указанный прокси:", + "zh": "未指定代理:", + "uk": "Не вказаний проксі:", + "fr": "Proxy non spécifié:", + "es": "Proxy no especificado:", + "tr": "Belirtilmemiş proxy:", + "kk": "Көрсетілмеген прокси:" + }, + "UnspecifiedRandom": { + "en": "Assign random", + "ru": "Назначить случайный", + "zh": "随机分配", + "uk": "Призначити випадковий", + "fr": "Attribuer aléatoirement", + "es": "Asignar aleatorio", + "tr": "Rastgele ata", + "kk": "Кездейсоқ тағайындау" + }, + "UnspecifiedRemove": { + "en": "Remove proxy", + "ru": "Удалить прокси", + "zh": "删除代理", + "uk": "Видалити проксі", + "fr": "Supprimer le proxy", + "es": "Eliminar proxy", + "tr": "Proxy'yi kaldır", + "kk": "Проксиді жою" + }, + "ApplyAssignment": { + "en": "Apply", + "ru": "Применить", + "zh": "应用", + "uk": "Застосувати", + "fr": "Appliquer", + "es": "Aplicar", + "tr": "Uygula", + "kk": "Қолдану" } } } \ No newline at end of file diff --git a/src/NebulaAuth/Localization/mainwindow.loc.json b/src/NebulaAuth/Localization/mainwindow.loc.json index c8b57e8..ca22364 100644 --- a/src/NebulaAuth/Localization/mainwindow.loc.json +++ b/src/NebulaAuth/Localization/mainwindow.loc.json @@ -904,6 +904,16 @@ "es": "Desvincular proxy", "tr": "Proxy bağlantısını kaldır", "kk": "Проксиді ажырату" + }, + "AssignFreeProxy": { + "en": "Assign free proxy", + "ru": "Назначить свободный прокси", + "zh": "分配空闲代理", + "uk": "Призначити вільний проксі", + "fr": "Attribuer un proxy libre", + "es": "Asignar proxy libre", + "tr": "Serbest proxy ata", + "kk": "Бос проксиді тағайындау" } }, "Proxy": { diff --git a/src/NebulaAuth/MainWindow.xaml b/src/NebulaAuth/MainWindow.xaml index 6eb2983..47654ed 100644 --- a/src/NebulaAuth/MainWindow.xaml +++ b/src/NebulaAuth/MainWindow.xaml @@ -420,6 +420,9 @@ + diff --git a/src/NebulaAuth/Model/ProxyAssignmentCache.cs b/src/NebulaAuth/Model/ProxyAssignmentCache.cs new file mode 100644 index 0000000..0a02f32 --- /dev/null +++ b/src/NebulaAuth/Model/ProxyAssignmentCache.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AchiesUtilities.Collections; +using AchiesUtilities.Web.Proxy; +using NebulaAuth.Model.Entities; + +namespace NebulaAuth.Model; + +/// +/// In-memory cache tracking which proxy IDs are currently assigned to accounts. +/// Provides fast lookup for free proxy selection without iterating all mafiles. +/// Initialized at startup and kept in sync whenever proxy assignments change. +/// +public static class ProxyAssignmentCache +{ + private static readonly Dictionary> _proxyToAccounts = new(); + + public static void Initialize(IEnumerable mafiles) + { + _proxyToAccounts.Clear(); + foreach (var maf in mafiles) + { + if (maf.Proxy != null) + AddInternal(maf.Proxy.Id, maf.AccountName); + } + } + + /// + /// Call whenever a proxy assignment on a mafile changes and is persisted to disk. + /// + public static void UpdateAssignment(string? accountName, int? oldProxyId, int? newProxyId) + { + if (accountName == null) return; + if (oldProxyId.HasValue) RemoveInternal(oldProxyId.Value, accountName); + if (newProxyId.HasValue) AddInternal(newProxyId.Value, accountName); + } + + public static bool IsProxyFree(int proxyId) + { + return !_proxyToAccounts.TryGetValue(proxyId, out var set) || set.Count == 0; + } + + public static int GetAccountCount(int proxyId) + { + return _proxyToAccounts.TryGetValue(proxyId, out var set) ? set.Count : 0; + } + + /// + /// Returns a random free proxy (not the default proxy), or null if none available. + /// + public static KeyValuePair? GetRandomFreeProxy( + ObservableDictionary proxies, + int? excludeDefaultProxyId) + { + var free = proxies + .Where(kvp => kvp.Key != excludeDefaultProxyId && IsProxyFree(kvp.Key)) + .ToList(); + if (free.Count == 0) return null; + return free[Random.Shared.Next(free.Count)]; + } + + private static void AddInternal(int proxyId, string accountName) + { + if (!_proxyToAccounts.TryGetValue(proxyId, out var set)) + { + set = new HashSet(StringComparer.OrdinalIgnoreCase); + _proxyToAccounts[proxyId] = set; + } + + set.Add(accountName); + } + + private static void RemoveInternal(int proxyId, string accountName) + { + if (_proxyToAccounts.TryGetValue(proxyId, out var set)) + set.Remove(accountName); + } +} \ No newline at end of file diff --git a/src/NebulaAuth/Model/Shell.cs b/src/NebulaAuth/Model/Shell.cs index 7a74a5d..f2ce3e5 100644 --- a/src/NebulaAuth/Model/Shell.cs +++ b/src/NebulaAuth/Model/Shell.cs @@ -45,6 +45,7 @@ public static class Shell var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1; await Storage.Initialize(threads); + ProxyAssignmentCache.Initialize(Storage.MaFiles); MAACStorage.Initialize(); MafileExporterStorage.Initialize(); diff --git a/src/NebulaAuth/Theme/Brushes.xaml b/src/NebulaAuth/Theme/Brushes.xaml index a75f635..5c13e96 100644 --- a/src/NebulaAuth/Theme/Brushes.xaml +++ b/src/NebulaAuth/Theme/Brushes.xaml @@ -89,4 +89,7 @@ Color="{StaticResource SecondaryColor}" /> + + \ No newline at end of file diff --git a/src/NebulaAuth/View/ProxyManagerView.xaml b/src/NebulaAuth/View/ProxyManagerView.xaml index 6eea83f..6dfadf9 100644 --- a/src/NebulaAuth/View/ProxyManagerView.xaml +++ b/src/NebulaAuth/View/ProxyManagerView.xaml @@ -20,12 +20,12 @@ FontSize="16"> - - + + @@ -45,90 +45,41 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - + + + + + + + - - + + + - + - - - + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + - + + + + + + + + + + + + - - - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +