From e3eb7a8b4281c7d3b2170b10b2681cde8ee7f066 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Wed, 24 Jan 2024 21:33:34 +0100
Subject: [PATCH] Support verification via clicking link from e-mail

---
 osu.Game/Online/API/APIAccess.cs              | 50 ++++++++++++++++++-
 .../DevelopmentEndpointConfiguration.cs       |  1 +
 osu.Game/Online/EndpointConfiguration.cs      |  5 ++
 .../ExperimentalEndpointConfiguration.cs      |  1 +
 .../Online/ProductionEndpointConfiguration.cs |  1 +
 5 files changed, 57 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 389816fcf8..dabb2cc94c 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -11,8 +11,10 @@ using System.Net.Http;
 using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
+using JetBrains.Annotations;
 using Newtonsoft.Json.Linq;
 using osu.Framework.Bindables;
+using osu.Framework.Extensions;
 using osu.Framework.Extensions.ExceptionExtensions;
 using osu.Framework.Extensions.ObjectExtensions;
 using osu.Framework.Graphics;
@@ -76,6 +78,11 @@ namespace osu.Game.Online.API
 
         private readonly Logger log;
 
+        private string webSocketEndpointUrl;
+
+        [CanBeNull]
+        private OsuClientWebSocket webSocket;
+
         public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash)
         {
             this.game = game;
@@ -84,6 +91,7 @@ namespace osu.Game.Online.API
 
             APIEndpointUrl = endpointConfiguration.APIEndpointUrl;
             WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl;
+            webSocketEndpointUrl = endpointConfiguration.NotificationsWebSocketEndpointUrl;
 
             authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl);
             log = Logger.GetLogger(LoggingTarget.Network);
@@ -267,7 +275,10 @@ namespace osu.Game.Online.API
 
                 setLocalUser(me);
 
-                state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth;
+                if (me.SessionVerified)
+                    state.Value = APIState.Online;
+                else
+                    setUpSecondFactorAuthentication();
                 failureCount = 0;
             };
 
@@ -350,6 +361,42 @@ namespace osu.Game.Online.API
             this.password = password;
         }
 
+        private void setUpSecondFactorAuthentication()
+        {
+            if (state.Value == APIState.RequiresSecondFactorAuth)
+                return;
+
+            state.Value = APIState.RequiresSecondFactorAuth;
+
+            try
+            {
+                webSocket?.DisposeAsync().AsTask().WaitSafely();
+                var newSocket = new OsuClientWebSocket(this, webSocketEndpointUrl);
+                newSocket.MessageReceived += async msg =>
+                {
+                    if (msg.Event == @"verified")
+                    {
+                        state.Value = APIState.Online;
+                        await newSocket.DisposeAsync().ConfigureAwait(false);
+                        if (webSocket == newSocket)
+                            webSocket = null;
+                    }
+                };
+                newSocket.Closed += ex =>
+                {
+                    Logger.Error(ex, "Connection with account verification endpoint closed unexpectedly. Please supply account verification code manually.", LoggingTarget.Network);
+                    return Task.CompletedTask;
+                };
+                webSocket = newSocket;
+
+                webSocket.ConnectAsync(cancellationToken.Token).WaitSafely();
+            }
+            catch (Exception ex)
+            {
+                Logger.Error(ex, "Failed to set up connection with account verification endpoint. Please supply account verification code manually.", LoggingTarget.Network);
+            }
+        }
+
         public void AuthenticateSecondFactor(string code)
         {
             Debug.Assert(State.Value == APIState.RequiresSecondFactorAuth);
@@ -579,6 +626,7 @@ namespace osu.Game.Online.API
 
             flushQueue();
             cancellationToken.Cancel();
+            webSocket?.DisposeAsync().AsTask().WaitSafely();
         }
     }
 
diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs
index 5f3c353f4d..1c78c3c147 100644
--- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs
+++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs
@@ -13,6 +13,7 @@ namespace osu.Game.Online
             SpectatorEndpointUrl = $@"{APIEndpointUrl}/signalr/spectator";
             MultiplayerEndpointUrl = $@"{APIEndpointUrl}/signalr/multiplayer";
             MetadataEndpointUrl = $@"{APIEndpointUrl}/signalr/metadata";
+            NotificationsWebSocketEndpointUrl = "wss://dev.ppy.sh/home/notifications/feed";
         }
     }
 }
diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs
index f3bcced630..6187471b65 100644
--- a/osu.Game/Online/EndpointConfiguration.cs
+++ b/osu.Game/Online/EndpointConfiguration.cs
@@ -44,5 +44,10 @@ namespace osu.Game.Online
         /// The endpoint for the SignalR metadata server.
         /// </summary>
         public string MetadataEndpointUrl { get; set; }
+
+        /// <summary>
+        /// The endpoint for the notifications websocket.
+        /// </summary>
+        public string NotificationsWebSocketEndpointUrl { get; set; }
     }
 }
diff --git a/osu.Game/Online/ExperimentalEndpointConfiguration.cs b/osu.Game/Online/ExperimentalEndpointConfiguration.cs
index c3d0014c8b..bc65fd63f3 100644
--- a/osu.Game/Online/ExperimentalEndpointConfiguration.cs
+++ b/osu.Game/Online/ExperimentalEndpointConfiguration.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Online
             SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";
             MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer";
             MetadataEndpointUrl = "https://spectator.ppy.sh/metadata";
+            NotificationsWebSocketEndpointUrl = "wss://notify.ppy.sh";
         }
     }
 }
diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs
index 0244761b65..a26a25bce5 100644
--- a/osu.Game/Online/ProductionEndpointConfiguration.cs
+++ b/osu.Game/Online/ProductionEndpointConfiguration.cs
@@ -13,6 +13,7 @@ namespace osu.Game.Online
             SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";
             MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer";
             MetadataEndpointUrl = "https://spectator.ppy.sh/metadata";
+            NotificationsWebSocketEndpointUrl = "wss://notify.ppy.sh";
         }
     }
 }