diff --git a/FoxTube.Core/FoxTube.Core.csproj b/FoxTube.Core/FoxTube.Core.csproj index afc0e11..8b40f20 100644 --- a/FoxTube.Core/FoxTube.Core.csproj +++ b/FoxTube.Core/FoxTube.Core.csproj @@ -130,7 +130,6 @@ PackageReference - @@ -157,7 +156,7 @@ 0.14.0 - 1.0.2 + 1.0.3 1.45.0 @@ -184,10 +183,10 @@ 10.1901.28001 - 2.3.200213001 + 2.4.0 - 5.0.2 + 5.0.3 diff --git a/FoxTube.Core/Models/SuspendedUser.cs b/FoxTube.Core/Models/SuspendedUser.cs deleted file mode 100644 index d986cf3..0000000 --- a/FoxTube.Core/Models/SuspendedUser.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace FoxTube.Models -{ - public class SuspendedUser - { - public string Name { get; set; } - public string Email { get; set; } - public string Avatar { get; set; } - public string RefreshToken { get; set; } - } -} \ No newline at end of file diff --git a/FoxTube.Core/Models/User.cs b/FoxTube.Core/Models/User.cs index b8b9f04..49f55b5 100644 --- a/FoxTube.Core/Models/User.cs +++ b/FoxTube.Core/Models/User.cs @@ -5,54 +5,59 @@ using Google.Apis.Services; using Google.Apis.YouTube.v3; using Google.Apis.YouTube.v3.Data; using System.Collections.Generic; +using System.Threading.Tasks; using YouTube; +using YoutubeExplode; namespace FoxTube.Models { public class User { - public Userinfoplus UserInfo { get; } - public UserCredential Credential { get; } + public Userinfoplus UserInfo { get; private set; } + public UserCredential Credential { get; private set; } public Channel Channel { get; private set; } public List Subscriptions { get; } = new List(); - public ExtendedYouTubeService Service { get; } - public User(UserCredential credential) + public ExtendedYouTubeService Service { get; private set; } + public YoutubeClient Client { get; private set; } + + public static async Task GetUser(UserCredential credential) { - Credential = credential; + User user = new User(); + + user.Credential = credential; BaseClientService.Initializer initializer = new BaseClientService.Initializer { ApplicationName = "FoxTube", - HttpClientInitializer = Credential + HttpClientInitializer = credential }; - Service = new ExtendedYouTubeService(initializer); + user.Service = new ExtendedYouTubeService(initializer); - UserInfo = new Oauth2Service(initializer).Userinfo.Get().Execute(); + user.UserInfo = await new Oauth2Service(initializer).Userinfo.Get().ExecuteAsync(); // TODO: Retrieve history and WL - SubscriptionsResource.ListRequest subRequest = Service.Subscriptions.List("snippet"); + SubscriptionsResource.ListRequest subRequest = user.Service.Subscriptions.List("snippet"); subRequest.Mine = true; subRequest.MaxResults = 50; subRequest.Order = SubscriptionsResource.ListRequest.OrderEnum.Relevance; SubscriptionListResponse subResponse; - string nextToken = null; - Subscriptions.Clear(); do { - subRequest.PageToken = nextToken; - subResponse = subRequest.Execute(); - foreach (Subscription s in subResponse.Items) - Subscriptions.Add(s); - nextToken = subResponse.NextPageToken; + subResponse = await subRequest.ExecuteAsync(); + subRequest.PageToken = subResponse.NextPageToken; - } while (!string.IsNullOrWhiteSpace(nextToken)); + user.Subscriptions.AddRange(subResponse.Items); - var request = Service.Channels.List("snippet,contentDetails,brandingSettings"); + } while (!string.IsNullOrWhiteSpace(subRequest.PageToken)); + + var request = user.Service.Channels.List("snippet,contentDetails,brandingSettings"); request.Mine = true; - Channel = request.Execute().Items[0]; + user.Channel = request.Execute().Items[0]; + + return user; } } } diff --git a/FoxTube.Core/Services/Inbox.cs b/FoxTube.Core/Services/Inbox.cs index 052ba36..8ba75b4 100644 --- a/FoxTube.Core/Services/Inbox.cs +++ b/FoxTube.Core/Services/Inbox.cs @@ -20,7 +20,7 @@ namespace FoxTube.Services try { // TODO: Add backend - HttpResponseMessage response = await client.GetAsync($"https://xfox111.net/FoxTube/Messages?toast=true&publishedAfter={storage.Values["Inbox.lastCheck"]}&lang={Settings.Language}&appVersion={Metrics.CurrentVersion}"); + HttpResponseMessage response = await client.GetAsync($"https://xfox111.net/FoxTube/Inbox?toast=true&publishedAfter={storage.Values["Inbox.lastCheck"]}&lang={Settings.Language}&appVersion={Metrics.CurrentVersion}"); storage.Values["Inbox.lastCheck"] = DateTime.UtcNow.Ticks; if (response.StatusCode == HttpStatusCode.NoContent) @@ -67,7 +67,7 @@ namespace FoxTube.Services // TODO: Add backend Settings.LastReviewedVersion = Metrics.CurrentVersion; - HttpResponseMessage response = await client.GetAsync($"https://xfox111.net/API/FoxTube/Changelogs?toast=true&lang={Settings.Language}&version={Metrics.CurrentVersion}"); + HttpResponseMessage response = await client.GetAsync($"https://xfox111.net/API/FoxTube/Changelog?lang={Settings.Language}&version={Metrics.CurrentVersion}"); if (response.StatusCode == HttpStatusCode.NoContent) return; diff --git a/FoxTube.Core/UserManagement.cs b/FoxTube.Core/UserManagement.cs index 87d3f2d..7ce647e 100644 --- a/FoxTube.Core/UserManagement.cs +++ b/FoxTube.Core/UserManagement.cs @@ -1,11 +1,9 @@ using Google.Apis.Auth.OAuth2; using Google.Apis.Oauth2.v2; using System; -using System.Collections.Generic; using System.Threading.Tasks; using Windows.Security.Authentication.Web; using YouTube.Authorization; -using System.Text.RegularExpressions; using Windows.Security.Credentials; using FoxTube.Models; using YouTube; @@ -13,58 +11,81 @@ using System.Threading; using Google.Apis.YouTube.v3; using Google.Apis.YouTube.v3.Data; using Windows.UI.Xaml.Controls; -using Windows.UI.Popups; using FoxTube.Utils; +using YoutubeExplode; +using System.Linq; +using Newtonsoft.Json; +using Windows.Storage; +using Google.Apis.Oauth2.v2.Data; namespace FoxTube { public static class UserManagement { - static ExtendedYouTubeService _defaultService = new ExtendedYouTubeService(new Google.Apis.Services.BaseClientService.Initializer + public const int MaxUsersCount = 2; + + #region Private members + private static readonly ApplicationDataContainer storage = ApplicationData.Current.LocalSettings; + + private static ExtendedYouTubeService _defaultService = new ExtendedYouTubeService(new Google.Apis.Services.BaseClientService.Initializer { ApplicationName = "FoxTube", - ApiKey = "AIzaSyBgHrCnrlzlVmk0cJKL8RqP9Y8x6XSuk_0" + ApiKey = "AIzaSyBgHrCnrlzlVmk0cJKL8RqP9Y8x6XSuk_0", //ApiKey = "AIzaSyD7tpbuvmYDv9h4udo9L_g3r0sLPFAnN00" }); + private static YoutubeClient _defaultYteClient = new YoutubeClient(); - static string[] Scopes { get; } = new string[] + private static string[] Scopes { get; } = new string[] { Oauth2Service.Scope.UserinfoProfile, Oauth2Service.Scope.UserinfoEmail, YouTubeService.Scope.YoutubeForceSsl }; - static ClientSecrets ClientSecrets { get; } = new ClientSecrets + private static ClientSecrets[] ClientSecrets { get; } = new ClientSecrets[MaxUsersCount] { - ClientId = "349735264870-2ekqlm0a4mkg3mmrfcv90s3qp3o15dq0.apps.googleusercontent.com", - ClientSecret = "BkVZOAaCU2Zclf0Zlicg6y2_" - //ClientId = "1096685398208-u95rcpkqb4e1ijfmb8jdq3jsg37l8igv.apps.googleusercontent.com", - //ClientSecret = "IU5bbdjwvmx8ttJoXQ7e6JWd" + new ClientSecrets + { + ClientId = "349735264870-2ekqlm0a4mkg3mmrfcv90s3qp3o15dq0.apps.googleusercontent.com", + ClientSecret = "BkVZOAaCU2Zclf0Zlicg6y2_" + }, + new ClientSecrets // DISABLED + { + ClientId = "1096685398208-u95rcpkqb4e1ijfmb8jdq3jsg37l8igv.apps.googleusercontent.com", + ClientSecret = "IU5bbdjwvmx8ttJoXQ7e6JWd" + } }; + #endregion + public static Userinfoplus[] Users { get; private set; } = new Userinfoplus[MaxUsersCount]; public static User CurrentUser { get; set; } public static bool Authorized => CurrentUser != null; public static ExtendedYouTubeService Service => CurrentUser?.Service ?? _defaultService; + public static YoutubeClient YoutubeClient => CurrentUser?.Client ?? _defaultYteClient; public static event EventHandler UserStateUpdated; public static event EventHandler SubscriptionsChanged; public static async Task AddUser() { - Uri requestString = AuthorizationHelpers.FormQueryString(ClientSecrets, Scopes); + int queueIndex = Users.ToList().FindIndex(i => i == null); + + if (queueIndex < 0) + throw new StackOverflowException("The maximum accounts limit is reached"); + + ClientSecrets secrets = ClientSecrets[queueIndex]; + + Uri requestString = AuthorizationHelpers.FormQueryString(secrets, Scopes); WebAuthenticationResult result = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.UseTitle, requestString, AuthorizationHelpers.Endpoint); switch (result.ResponseStatus) { case WebAuthenticationStatus.Success: - string successCode = new Regex(@"(?<=code=)(.*?)(?=&)").Match(result.ResponseData).Value; + string successCode = AuthorizationHelpers.ParseSuccessCode(result.ResponseData); - UserCredential credential = await AuthorizationHelpers.ExchangeToken(ClientSecrets, successCode); - CurrentUser = new User(credential); + YouTube.Authorization.UserCredential credential = await AuthorizationHelpers.ExchangeToken(secrets, successCode); - PasswordVault passwordVault = new PasswordVault(); - passwordVault.Add(new PasswordCredential("foxtube", CurrentUser.UserInfo.Id, credential.Token.RefreshToken)); - UserStateUpdated?.Invoke(null, true); + await LoadUser(credential, queueIndex); return true; case WebAuthenticationStatus.UserCancel: break; @@ -81,44 +102,104 @@ namespace FoxTube ("Error details", result.ResponseErrorDetail.ToString())); break; } + return false; } public static async Task Initialize() { - PasswordVault passwordVault = new PasswordVault(); - IReadOnlyList credentials; - credentials = passwordVault.RetrieveAll(); + Users = JsonConvert.DeserializeObject(storage.Values["UserManagement.Users"] as string); + int? lastUserIndex = storage.Values["UserManagement.LastUser"] as int?; - if (credentials.Count == 0) - return; + if (lastUserIndex.HasValue && Users[lastUserIndex.Value] != null || + (lastUserIndex = Users.ToList().FindIndex(i => i != null)) > -1) + await SwitchUser(lastUserIndex.Value); + } + + public static async Task Logout() + { + if (CurrentUser?.UserInfo?.Id == null) + return false; try { - credentials[0].RetrievePassword(); - UserCredential credential = await AuthorizationHelpers.RestoreUser(ClientSecrets, credentials[0].Password); - credentials[0].Password = credential.Token.RefreshToken; - CurrentUser = new User(credential); + string userId = CurrentUser.UserInfo.Id; + + PasswordVault passwordVault = new PasswordVault(); + PasswordCredential credential = passwordVault.Retrieve("foxtube", userId); + passwordVault.Remove(credential); + + await CurrentUser.Credential.RevokeTokenAsync(CancellationToken.None); + + CurrentUser = null; + Users[Users.ToList().FindIndex(i => i.Id == userId)] = null; + + storage.Values["UserManagement.Users"] = JsonConvert.SerializeObject(Users); + storage.Values["UserManagement.LastUser"] = null; + + if (Users.Any(i => i != null)) + await SwitchUser(Users.ToList().FindIndex(i => i != null)); + else + UserStateUpdated?.Invoke(Users, false); + + return true; } catch (Exception e) { - await new MessageDialog("It may be a bug or temporary server issues. Please, try again later", "Something went wrong...").ShowAsync(); + Metrics.SendReport(new Exception("Failed to logout", e)); - Metrics.SendReport(new Exception("Refresh token exchange failed", e)); + await new ContentDialog + { + Title = "Something went wrong...", + Content = "It may be a bug or temporary server issues. Please, try again later" + }.ShowAsync(); - foreach (PasswordCredential i in credentials) - passwordVault.Remove(i); + return false; } } - public static async Task Logout() + public static async Task SwitchUser(int userIndex) + { + Userinfoplus userInfo = Users[userIndex]; + + PasswordVault valut = new PasswordVault(); + PasswordCredential vaultCredential = valut.Retrieve("foxtube", userInfo.Id); + if (vaultCredential == null) + throw new NullReferenceException("No user found to switch on"); + + vaultCredential.RetrievePassword(); + string token = vaultCredential.Password; + YouTube.Authorization.UserCredential credential = await AuthorizationHelpers.RestoreUser(ClientSecrets[userIndex], token); + + await LoadUser(credential, userIndex); + } + + private static async Task LoadUser(YouTube.Authorization.UserCredential credential, int userIndex) + { + CurrentUser = await User.GetUser(credential); + Users[userIndex] = CurrentUser.UserInfo; + + storage.Values["UserManagement.Users"] = JsonConvert.SerializeObject(Users); + storage.Values["UserManagement.LastUser"] = userIndex; + + credential.RefreshTokenUpdated += (s, e) => UpdateToken(CurrentUser.UserInfo.Id, credential.Token.RefreshToken); + UpdateToken(CurrentUser.UserInfo.Id, credential.Token.RefreshToken); + + UserStateUpdated?.Invoke(Users, true); + } + + private static void UpdateToken(string id, string refreshToken) { PasswordVault passwordVault = new PasswordVault(); - PasswordCredential credential = passwordVault.Retrieve("foxtube", CurrentUser.UserInfo.Id); - passwordVault.Remove(credential); - await CurrentUser.Credential.RevokeTokenAsync(CancellationToken.None); + PasswordCredential vaultCredential = passwordVault.Retrieve("foxtube", id); - UserStateUpdated?.Invoke(null, false); + if (vaultCredential == null) + { + vaultCredential = new PasswordCredential("foxtube", CurrentUser.UserInfo.Id, refreshToken); + passwordVault.Add(vaultCredential); + } + else + vaultCredential.Password = refreshToken; } } -} +} \ No newline at end of file