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 0000000..b133bc3
Binary files /dev/null and b/Assets/glitch_flat_white.png differ
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 =
+@"
+
+