From baa2a15d02bf94d39cec23493851a481d9f72967 Mon Sep 17 00:00:00 2001 From: Enrico Ludwig Date: Wed, 28 Aug 2024 17:27:52 +0200 Subject: [PATCH] Completely replaced the previous static auth flow with Twitch OAuth to enable users to login within the application --- App.axaml.cs | 135 +++++++++++++----------- Assets/glitch_flat_white.png | Bin 0 -> 24118 bytes BetterRaid.generated.sln | 10 +- Misc/Tools.cs | 170 ++++++++++++++++++++++++++++++ ViewModels/MainWindowViewModel.cs | 17 ++- ViewModels/RaidButtonViewModel.cs | 13 +-- Views/MainWindow.axaml | 35 ++++++ Views/MainWindow.axaml.cs | 19 ++++ 8 files changed, 324 insertions(+), 75 deletions(-) create mode 100644 Assets/glitch_flat_white.png create mode 100644 Misc/Tools.cs diff --git a/App.axaml.cs b/App.axaml.cs index b454913..9f2bbbe 100644 --- a/App.axaml.cs +++ b/App.axaml.cs @@ -8,88 +8,97 @@ using Avalonia.Markup.Xaml; using BetterRaid.ViewModels; using BetterRaid.Views; using TwitchLib.Api; -using TwitchLib.Client; -using TwitchLib.Client.Events; -using TwitchLib.Client.Models; -using TwitchLib.Communication.Clients; -using TwitchLib.Communication.Models; namespace BetterRaid; public partial class App : Application { - public static int AutoUpdateDelay = 10_000; - public static string TwitchBroadcasterId = ""; - public static string TwitchChannelName = ""; - public static string TokenClientId = ""; - public static string TokenClientSecret = ""; - public static string TokenClientAccess = ""; - public static TwitchClient? TwitchClient = null; - public static TwitchAPI? TwitchAPI = null; + internal static TwitchAPI? TwitchApi = null; + internal static int AutoUpdateDelay = 10_000; + internal static string TwitchOAuthAccessToken = ""; + internal static string TwitchOAuthAccessTokenFilePath = ""; + internal static string TokenClientId = "kkxu4jorjrrc5jch1ito5i61hbev2o"; + internal static readonly string TwitchOAuthRedirectUrl = "http://localhost:9900"; + internal static readonly string TwitchOAuthResponseType = "token"; + internal static readonly string[] TwitchOAuthScopes = [ "channel:manage:raids", "user:read:chat" ]; + internal static readonly string TwitchOAuthUrl = $"https://id.twitch.tv/oauth2/authorize" + + $"?client_id={TokenClientId}" + + "&redirect_uri=http://localhost:9900" + + $"&response_type={TwitchOAuthResponseType}" + + $"&scope={string.Join("+", TwitchOAuthScopes)}"; public override void Initialize() { - try + var userHomeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var betterRaidDir = ""; + + switch (Environment.OSVersion.Platform) { - var tokenFile = "zn_twitch.secret"; - var profilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var tokenFilePath = Path.Combine(profilePath, tokenFile); - var tokenFileLines = File.ReadAllLines(tokenFilePath); - TwitchChannelName = tokenFileLines[0].Split('=')[1]; - TokenClientId = tokenFileLines[1].Split('=')[1]; - TokenClientSecret = tokenFileLines[2].Split('=')[1]; - TokenClientAccess = tokenFileLines[3].Split('=')[1]; - } - catch (Exception) - { - Console.WriteLine("[ERROR] Failed to read token from secret file!"); - Environment.Exit(1); + case PlatformID.Win32NT: + betterRaidDir = Path.Combine(userHomeDir, "AppData", "Roaming", "BetterRaid"); + break; + case PlatformID.Unix: + betterRaidDir = Path.Combine(userHomeDir, ".config", "BetterRaid"); + break; + case PlatformID.MacOSX: + betterRaidDir = Path.Combine(userHomeDir, "Library", "Application Support", "BetterRaid"); + break; } - var creds = new ConnectionCredentials(TwitchChannelName, TokenClientAccess); - var clientOptions = new ClientOptions + if (!Directory.Exists(betterRaidDir)) + Directory.CreateDirectory(betterRaidDir); + + TwitchOAuthAccessTokenFilePath = Path.Combine(betterRaidDir, ".access_token"); + + if (File.Exists(TwitchOAuthAccessTokenFilePath)) { - MessagesAllowedInPeriod = 750, - ThrottlingPeriod = TimeSpan.FromSeconds(30) - }; - - var customClient = new WebSocketClient(clientOptions); - TwitchClient = new TwitchClient(customClient); - - TwitchClient.Initialize(creds, TwitchChannelName); - TwitchClient.OnConnected += OnConnected; - TwitchClient.OnConnectionError += OnConnectionError; - - TwitchClient.Connect(); - - TwitchAPI = new TwitchAPI(); - TwitchAPI.Settings.ClientId = TokenClientId; - TwitchAPI.Settings.AccessToken = TokenClientAccess; - - var channels = TwitchAPI.Helix.Search.SearchChannelsAsync(TwitchChannelName).Result; - var exactChannel = channels.Channels.FirstOrDefault(c => c.BroadcasterLogin == TwitchChannelName); - - if (exactChannel != null) - { - TwitchBroadcasterId = exactChannel.Id; - Console.WriteLine($"TWITCH BROADCASTER ID SET TO {TwitchBroadcasterId}"); - } - else - { - Console.WriteLine("FAILED TO SET BROADCASTER ID!"); + TwitchOAuthAccessToken = File.ReadAllText(TwitchOAuthAccessTokenFilePath); + InitTwitchClient(); } + AvaloniaXamlLoader.Load(this); } - private void OnConnectionError(object? sender, OnConnectionErrorArgs e) + public static void InitTwitchClient(bool overrideToken = false) { - Console.WriteLine("[ERROR] Twitch Client failed to connect!"); - } + Console.WriteLine("[INFO] Initializing Twitch Client..."); - private void OnConnected(object? sender, OnConnectedArgs e) - { - Console.WriteLine("[INFO] Twitch Client connected!"); + TwitchApi = new TwitchAPI(); + TwitchApi.Settings.ClientId = TokenClientId; + TwitchApi.Settings.AccessToken = TwitchOAuthAccessToken; + + Console.WriteLine("[INFO] Testing Twitch API connection..."); + + var user = TwitchApi.Helix.Users.GetUsersAsync().Result.Users.FirstOrDefault(); + if (user == null) + { + TwitchApi = null; + Console.WriteLine("[ERROR] Failed to connect to Twitch API!"); + return; + } + + Console.WriteLine("[INFO] Connected to Twitch API as '{0}'!", user.DisplayName); + + if (overrideToken) + { + File.WriteAllText(TwitchOAuthAccessTokenFilePath, TwitchOAuthAccessToken); + + switch (Environment.OSVersion.Platform) + { + case PlatformID.Win32NT: + File.SetAttributes(TwitchOAuthAccessTokenFilePath, File.GetAttributes(TwitchOAuthAccessTokenFilePath) | FileAttributes.Hidden); + break; + case PlatformID.Unix: +#pragma warning disable CA1416 // Validate platform compatibility + File.SetUnixFileMode(TwitchOAuthAccessTokenFilePath, UnixFileMode.UserRead); +#pragma warning restore CA1416 // Validate platform compatibility + break; + case PlatformID.MacOSX: + File.SetAttributes(TwitchOAuthAccessTokenFilePath, File.GetAttributes(TwitchOAuthAccessTokenFilePath) | FileAttributes.Hidden); + break; + } + } } public override void OnFrameworkInitializationCompleted() diff --git a/Assets/glitch_flat_white.png b/Assets/glitch_flat_white.png new file mode 100644 index 0000000000000000000000000000000000000000..b133bc32835940bfebf2b0453eb07252a801534a GIT binary patch literal 24118 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 literal 0 HcmV?d00001 diff --git a/BetterRaid.generated.sln b/BetterRaid.generated.sln index 9354cb2..e4ffe1e 100644 --- a/BetterRaid.generated.sln +++ b/BetterRaid.generated.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.002.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterRaid", "BetterRaid.csproj", "{6BE742A5-079D-4617-BDC1-2933274CDCB7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BetterRaid", "BetterRaid.csproj", "{EDFD12AB-9E05-4D87-9139-C220A703CFDB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +11,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6BE742A5-079D-4617-BDC1-2933274CDCB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6BE742A5-079D-4617-BDC1-2933274CDCB7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6BE742A5-079D-4617-BDC1-2933274CDCB7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6BE742A5-079D-4617-BDC1-2933274CDCB7}.Release|Any CPU.Build.0 = Release|Any CPU + {EDFD12AB-9E05-4D87-9139-C220A703CFDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDFD12AB-9E05-4D87-9139-C220A703CFDB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDFD12AB-9E05-4D87-9139-C220A703CFDB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDFD12AB-9E05-4D87-9139-C220A703CFDB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Misc/Tools.cs b/Misc/Tools.cs new file mode 100644 index 0000000..b7cf484 --- /dev/null +++ b/Misc/Tools.cs @@ -0,0 +1,170 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; + +namespace BetterRaid.Misc; + +public static class Tools +{ + private static HttpListener? _oauthListener; + private static Task? _oauthWaiterTask; + + // Source: https://stackoverflow.com/a/43232486 + public static void StartOAuthLogin(string url, Action? callback = null, CancellationToken? token = null) + { + if (_oauthListener != null) + return; + + var _token = token ?? CancellationToken.None; + + _oauthListener = new HttpListener(); + _oauthListener.Prefixes.Add("http://localhost:9900/"); + _oauthListener.Start(); + + _oauthWaiterTask = WaitForCallback(callback, _token); + + try + { + Process.Start(url); + } + catch + { + // hack because of this: https://github.com/dotnet/corefx/issues/10361 + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + url = url.Replace("&", "^&"); + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Process.Start("xdg-open", url); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", url); + } + else + { + throw; + } + } + } + + private static async Task WaitForCallback(Action? callback, CancellationToken token) + { + if (_oauthListener == null) + return; + + if (token.IsCancellationRequested) + return; + + Console.WriteLine("Starting token listener"); + + while (!token.IsCancellationRequested) + { + var ctx = await _oauthListener.GetContextAsync(); + var req = ctx.Request; + var res = ctx.Response; + + if (req.Url == null) + continue; + + Console.WriteLine("{0} {1}", req.HttpMethod, req.Url); + + // Response, that may contain the access token as fragment + // It must be extracted client-side in browser + if (req.Url.LocalPath == "/") + { + var buf = new byte[1024]; + var data = new StringBuilder(); + int bytesRead; + while ((bytesRead = await req.InputStream.ReadAsync(buf, token)) > 0) + { + data.Append(Encoding.UTF8.GetString(buf, 0, bytesRead)); + } + + req.InputStream.Close(); + + Console.WriteLine(data.ToString()); + + res.StatusCode = 200; + await res.OutputStream.WriteAsync(Encoding.UTF8.GetBytes(OAUTH_CLIENT_DOCUMENT).AsMemory(0, OAUTH_CLIENT_DOCUMENT.Length), token); + res.Close(); + } + + if (req.Url.LocalPath == "/login") + { + var buf = new byte[1024]; + var data = new StringBuilder(); + int bytesRead; + while ((bytesRead = await req.InputStream.ReadAsync(buf, token)) > 0) + { + data.Append(Encoding.UTF8.GetString(buf, 0, bytesRead)); + } + + req.InputStream.Close(); + + var json = data.ToString(); + var jsonData = JsonObject.Parse(json); + + if (jsonData == null) + { + Console.WriteLine("[ERROR] Failed to parse JSON data:"); + Console.WriteLine(json); + + res.StatusCode = 400; + res.Close(); + continue; + } + + if (jsonData["access_token"] == null) + { + Console.WriteLine("[ERROR] Missing access_token in JSON data."); + + res.StatusCode = 400; + res.Close(); + continue; + } + + var accessToken = jsonData["access_token"]?.ToString(); + App.TwitchOAuthAccessToken = accessToken!; + + res.StatusCode = 200; + res.Close(); + + Console.WriteLine("[INFO] Received access token!"); + + callback?.Invoke(); + + _oauthListener.Stop(); + return; + } + } + } + + private const string OAUTH_CLIENT_DOCUMENT = +@" + +
+ BetterRaid Twitch Login +
+ +

Successfully logged in!

+ + + +"; +} \ No newline at end of file diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 051c68d..22deb9e 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -1,7 +1,8 @@ using System; -using Avalonia; +using System.Threading; using Avalonia.Controls; using BetterRaid.Extensions; +using BetterRaid.Misc; using BetterRaid.Models; using BetterRaid.Views; @@ -25,6 +26,8 @@ public partial class MainWindowViewModel : ViewModelBase set => SetProperty(ref _filter, value); } + public bool IsLoggedIn => App.TwitchApi != null; + public void ExitApplication() { //TODO polish later @@ -37,4 +40,16 @@ public partial class MainWindowViewModel : ViewModelBase about.ShowDialog(owner); about.CenterToOwner(); } + + public void LoginWithTwitch() + { + Tools.StartOAuthLogin(App.TwitchOAuthUrl, OnTwitchLoginCallback, CancellationToken.None); + } + + public void OnTwitchLoginCallback() + { + App.InitTwitchClient(overrideToken: true); + + OnPropertyChanged(nameof(IsLoggedIn)); + } } diff --git a/ViewModels/RaidButtonViewModel.cs b/ViewModels/RaidButtonViewModel.cs index 61985b2..5204ceb 100644 --- a/ViewModels/RaidButtonViewModel.cs +++ b/ViewModels/RaidButtonViewModel.cs @@ -94,13 +94,13 @@ public class RaidButtonViewModel : ViewModelBase private async Task GetChannelAsync(string channelName) { - if (App.TwitchAPI == null) + if (App.TwitchApi == null) return null; if (string.IsNullOrEmpty(channelName)) return null; - var channels = await App.TwitchAPI.Helix.Search.SearchChannelsAsync(channelName); + var channels = await App.TwitchApi.Helix.Search.SearchChannelsAsync(channelName); var exactChannel = channels.Channels.FirstOrDefault(c => c.BroadcasterLogin.Equals(channelName, StringComparison.CurrentCultureIgnoreCase)); return exactChannel; @@ -108,13 +108,13 @@ public class RaidButtonViewModel : ViewModelBase private async Task GetStreamAsync(Channel currentChannelData) { - if (App.TwitchAPI == null) + if (App.TwitchApi == null) return null; if (currentChannelData == null) return null; - var streams = await App.TwitchAPI.Helix.Streams.GetStreamsAsync(userLogins: [currentChannelData.BroadcasterLogin]); + var streams = await App.TwitchApi.Helix.Streams.GetStreamsAsync(userLogins: [currentChannelData.BroadcasterLogin]); var exactStream = streams.Streams.FirstOrDefault(s => s.UserLogin == currentChannelData.BroadcasterLogin); return exactStream; @@ -122,7 +122,7 @@ public class RaidButtonViewModel : ViewModelBase public async Task RaidChannel() { - if (App.TwitchAPI == null) + if (App.TwitchApi == null) return; if (Channel == null) @@ -132,7 +132,8 @@ public class RaidButtonViewModel : ViewModelBase try { - raid = await App.TwitchAPI.Helix.Raids.StartRaidAsync(App.TwitchBroadcasterId, Channel.BroadcasterId); + // TODO: Get own broadcaster id + raid = await App.TwitchApi.Helix.Raids.StartRaidAsync("", Channel.BroadcasterId); } catch (Exception e) { diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml index 97d8b43..2cd6989 100644 --- a/Views/MainWindow.axaml +++ b/Views/MainWindow.axaml @@ -48,6 +48,29 @@ Orientation="Horizontal" Spacing="5"> + + @@ -61,9 +84,21 @@ + + diff --git a/Views/MainWindow.axaml.cs b/Views/MainWindow.axaml.cs index f7cc400..2fa2732 100644 --- a/Views/MainWindow.axaml.cs +++ b/Views/MainWindow.axaml.cs @@ -58,10 +58,19 @@ public partial class MainWindow : Window { GenerateRaidGrid(); } + + if (e.PropertyName == nameof(MainWindowViewModel.IsLoggedIn) && DataContext is MainWindowViewModel { IsLoggedIn: true }) + { + InitializeRaidChannels(); + GenerateRaidGrid(); + } } private void InitializeRaidChannels() { + if (DataContext is MainWindowViewModel { IsLoggedIn: false }) + return; + if (_autoUpdater?.IsBusy == false) { _autoUpdater?.CancelAsync(); @@ -100,6 +109,9 @@ public partial class MainWindow : Window private void GenerateRaidGrid() { + if (DataContext is MainWindowViewModel { IsLoggedIn: false }) + return; + foreach (var child in raidGrid.Children) { if (child is Button btn) @@ -216,6 +228,13 @@ public partial class MainWindow : Window public void UpdateChannelData() { + var loggedIn = Dispatcher.UIThread.Invoke(() => { + return (DataContext as MainWindowViewModel)?.IsLoggedIn ?? false; + }); + + if (loggedIn == false) + return; + foreach (var vm in _raidButtonVMs) { Task.Run(vm.GetOrUpdateChannelAsync);