Skip to content

Commit 3739daa

Browse files
fix: crash on startup
Fixing crash on startup from failure to refresh access token.
1 parent c4101f0 commit 3739daa

File tree

6 files changed

+89
-100
lines changed

6 files changed

+89
-100
lines changed

src/TwitchStreamingTools/Services/AccountManager.cs

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Nullinside.Api.Common.Twitch;
77

88
using TwitchStreamingTools.Models;
9+
using TwitchStreamingTools.Utilities;
910

1011
namespace TwitchStreamingTools.Services;
1112

@@ -18,11 +19,6 @@ public class AccountManager : IAccountManager {
1819
/// </summary>
1920
private readonly DispatcherTimer _timer;
2021

21-
/// <summary>
22-
/// The twitch chat api.
23-
/// </summary>
24-
private readonly ITwitchApiProxy _twitchApi;
25-
2622
/// <summary>
2723
/// The twitch chat client.
2824
/// </summary>
@@ -32,10 +28,8 @@ public class AccountManager : IAccountManager {
3228
/// Initializes a new instance of the <see cref="AccountManager" /> class.
3329
/// </summary>
3430
/// <param name="twitchClient">The twitch chat client.</param>
35-
/// <param name="twitchApi">The twitch chat api.</param>
36-
public AccountManager(ITwitchClientProxy twitchClient, ITwitchApiProxy twitchApi) {
31+
public AccountManager(ITwitchClientProxy twitchClient) {
3732
_twitchClient = twitchClient;
38-
_twitchApi = twitchApi;
3933
_timer = new DispatcherTimer {
4034
Interval = TimeSpan.FromSeconds(5)
4135
};
@@ -58,15 +52,17 @@ public AccountManager(ITwitchClientProxy twitchClient, ITwitchApiProxy twitchApi
5852

5953
/// <inheritdoc />
6054
public async Task UpdateCredentials(string bearer, string refresh, DateTime expires) {
61-
_twitchApi.OAuth = new TwitchAccessToken {
62-
AccessToken = bearer,
63-
RefreshToken = refresh,
64-
ExpiresUtc = expires
55+
var twitchApi = new TwitchApiWrapper {
56+
OAuth = new TwitchAccessToken {
57+
AccessToken = bearer,
58+
RefreshToken = refresh,
59+
ExpiresUtc = expires
60+
}
6561
};
6662

6763
(string? id, string? username)? user = null;
6864
try {
69-
user = await _twitchApi.GetUser();
65+
user = await twitchApi.GetUser();
7066
}
7167
catch {
7268
// Do nothing
@@ -89,7 +85,6 @@ public async Task UpdateCredentials(string bearer, string refresh, DateTime expi
8985

9086
/// <inheritdoc />
9187
public void DeleteCredentials() {
92-
_twitchApi.OAuth = null;
9388
Configuration.Instance.OAuth = null;
9489
Configuration.Instance.TwitchUsername = null;
9590
_twitchClient.TwitchOAuthToken = null;
@@ -109,7 +104,11 @@ private async Task OnCheckCredentials() {
109104
_timer.Stop();
110105
try {
111106
bool previousValue = CredentialsAreValid;
112-
string? username = (await _twitchApi.GetUser()).username;
107+
108+
await DoTokenRefreshIfNearExpiration();
109+
110+
var twitchApi = new TwitchApiWrapper();
111+
string? username = (await twitchApi.GetUser()).username;
113112
CredentialsAreValid = !string.IsNullOrWhiteSpace(username);
114113
TwitchUsername = username;
115114

@@ -124,4 +123,30 @@ private async Task OnCheckCredentials() {
124123
_timer.Start();
125124
}
126125
}
126+
127+
/// <summary>
128+
/// Checks the expiration of the OAuth token and refreshes if it's within 1 hour of the time.
129+
/// </summary>
130+
private async Task DoTokenRefreshIfNearExpiration() {
131+
var twitchApi = new TwitchApiWrapper();
132+
DateTime expiration = twitchApi.OAuth?.ExpiresUtc ?? DateTime.MaxValue;
133+
TimeSpan timeUntil = expiration - (DateTime.UtcNow + TimeSpan.FromHours(1));
134+
if (timeUntil.Ticks >= 0) {
135+
return;
136+
}
137+
138+
if (null == twitchApi.OAuth || string.IsNullOrWhiteSpace(twitchApi.OAuth.AccessToken) ||
139+
string.IsNullOrWhiteSpace(twitchApi.OAuth.RefreshToken)) {
140+
return;
141+
}
142+
143+
await twitchApi.RefreshAccessToken();
144+
145+
Configuration.Instance.OAuth = new OAuthResponse {
146+
Bearer = twitchApi.OAuth.AccessToken,
147+
Refresh = twitchApi.OAuth.RefreshToken,
148+
ExpiresUtc = twitchApi.OAuth.ExpiresUtc ?? DateTime.MinValue
149+
};
150+
Configuration.Instance.WriteConfiguration();
151+
}
127152
}
Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
using System;
2-
using System.Threading.Tasks;
3-
4-
using Microsoft.Extensions.DependencyInjection;
1+
using Microsoft.Extensions.DependencyInjection;
52

63
using Nullinside.Api.Common.Twitch;
74

8-
using TwitchStreamingTools.Utilities;
95
using TwitchStreamingTools.ViewModels;
106
using TwitchStreamingTools.ViewModels.Pages;
117

@@ -20,26 +16,12 @@ public static class ServiceCollectionExtensions {
2016
/// </summary>
2117
/// <param name="collection">The services collection to initialize.</param>
2218
public static void AddCommonServices(this IServiceCollection collection) {
23-
// collection.AddSingleton<IRepository, Repository>();
24-
// collection.AddTransient<BusinessService>();
2519
collection.AddSingleton<IAccountManager, AccountManager>();
2620
collection.AddSingleton<ITwitchClientProxy, TwitchClientProxy>(_ => TwitchClientProxy.Instance);
2721

2822
collection.AddTransient<MainWindowViewModel>();
2923
collection.AddTransient<AccountViewModel>();
3024
collection.AddTransient<ChatViewModel>();
3125
collection.AddTransient<NewVersionWindowViewModel>();
32-
collection.AddTransient<ITwitchApiProxy, TwitchApiWrapper>(TwitchApiWrapperFactory);
33-
}
34-
35-
/// <summary>
36-
/// A factory for generating twitch apis.
37-
/// </summary>
38-
/// <param name="_">not used.</param>
39-
/// <returns>A new instance of the <see cref="TwitchApiWrapper" /> class.</returns>
40-
private static TwitchApiWrapper TwitchApiWrapperFactory(IServiceProvider _) {
41-
Task<TwitchApiWrapper> task = TwitchApiWrapper.CreateApi();
42-
Task.WaitAll(task);
43-
return task.Result;
4426
}
4527
}

src/TwitchStreamingTools/TwitchStreamingTools.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.0"/>
3232
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.0"/>
3333
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
34-
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.8"/>
34+
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.0"/>
3535
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.0"/>
3636
<PackageReference Include="Material.Avalonia" Version="3.11.0"/>
3737
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4"/>

src/TwitchStreamingTools/Utilities/TwitchApiWrapper.cs

Lines changed: 32 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Net.Http;
3-
using System.Net.Http.Json;
44
using System.Threading;
55
using System.Threading.Tasks;
66

7+
using log4net;
8+
79
using Newtonsoft.Json;
810

911
using Nullinside.Api.Common.Twitch;
10-
using Nullinside.Api.Common.Twitch.Json;
1112

1213
using TwitchStreamingTools.Models;
1314

@@ -18,14 +19,14 @@ namespace TwitchStreamingTools.Utilities;
1819
/// </summary>
1920
public class TwitchApiWrapper : TwitchApiProxy {
2021
/// <summary>
21-
/// Lock to prevent the creation of more than one API at a time.
22+
/// The logger.
2223
/// </summary>
23-
private static readonly SemaphoreSlim _lock = new(1);
24+
private static readonly ILog Log = LogManager.GetLogger(typeof(TwitchApiWrapper));
2425

2526
/// <summary>
2627
/// Initializes a new instance of the <see cref="TwitchApiWrapper" /> class.
2728
/// </summary>
28-
protected TwitchApiWrapper() : base(
29+
public TwitchApiWrapper() : base(
2930
Configuration.Instance.OAuth?.Bearer ?? "",
3031
Configuration.Instance.OAuth?.Refresh ?? "",
3132
Configuration.Instance.OAuth?.ExpiresUtc ?? DateTime.MinValue,
@@ -35,55 +36,38 @@ protected TwitchApiWrapper() : base(
3536
}
3637

3738
/// <summary>
38-
/// Creates a new instance of the API.
39+
/// Handles refreshing the twitch oauth token either through a local application or through the website.
3940
/// </summary>
40-
/// <returns>A new API instance.</returns>
41-
public static async Task<TwitchApiWrapper> CreateApi() {
42-
await _lock.WaitAsync();
41+
/// <param name="token">The refresh token.</param>
42+
/// <returns>The new OAuth token information if successful, null otherwise.</returns>
43+
public override async Task<TwitchAccessToken?> RefreshAccessToken(CancellationToken token = new()) {
4344
try {
44-
var api = new TwitchApiWrapper();
45-
DateTime expiration = api.OAuth?.ExpiresUtc ?? DateTime.MaxValue;
46-
TimeSpan timeUntil = expiration - (DateTime.UtcNow + TimeSpan.FromHours(1));
47-
if (timeUntil.Ticks < 0) {
48-
if (null != api.OAuth && !string.IsNullOrWhiteSpace(api.OAuth.AccessToken) &&
49-
!string.IsNullOrWhiteSpace(api.OAuth.RefreshToken)) {
50-
await api.RefreshAccessToken();
51-
(string? id, string? username) userInfo = await api.GetUser();
52-
Configuration.Instance.OAuth = new OAuthResponse {
53-
Bearer = api.OAuth.AccessToken,
54-
Refresh = api.OAuth.RefreshToken,
55-
ExpiresUtc = api.OAuth.ExpiresUtc ?? DateTime.MinValue
56-
};
57-
}
45+
// If the secret is specified, then this isn't using our API to authenticate, it's using the twitch api directly.
46+
if (!string.IsNullOrWhiteSpace(TwitchAppConfig?.ClientSecret)) {
47+
await base.RefreshAccessToken(token);
5848
}
5949

60-
return api;
61-
}
62-
finally {
63-
_lock.Release();
64-
}
65-
}
50+
using var client = new HttpClient();
51+
string url = $"{Constants.API_SITE_DOMAIN}/api/v1/user/twitch-login/twitch-streaming-tools";
52+
var request = new HttpRequestMessage(HttpMethod.Post, url);
53+
var values = new Dictionary<string, string> { { "refreshToken", OAuth?.RefreshToken ?? string.Empty } };
54+
var content = new FormUrlEncodedContent(values);
55+
using HttpResponseMessage response = await client.PostAsync(request.RequestUri, content, token);
56+
response.EnsureSuccessStatusCode();
57+
string responseBody = await response.Content.ReadAsStringAsync(token);
58+
var oauthResp = JsonConvert.DeserializeObject<OAuthResponse>(responseBody);
59+
if (null == oauthResp) {
60+
return null;
61+
}
6662

67-
/// <summary>
68-
/// Handles refreshing the twitch oauth token eithe through a local application or through the website.
69-
/// </summary>
70-
/// <param name="token">The refresh token.</param>
71-
/// <returns>The new OAuth token information if successful, null otherwise.</returns>
72-
public override async Task<TwitchAccessToken?> RefreshAccessToken(CancellationToken token = new()) {
73-
if (!string.IsNullOrWhiteSpace(TwitchAppConfig?.ClientSecret)) {
74-
await base.RefreshAccessToken(token);
63+
return new TwitchAccessToken {
64+
AccessToken = oauthResp.Bearer,
65+
ExpiresUtc = oauthResp.ExpiresUtc,
66+
RefreshToken = oauthResp.Refresh
67+
};
7568
}
76-
77-
using var client = new HttpClient();
78-
string url = $"{Constants.API_SITE_DOMAIN}/api/v1/user/twitch-login/twitch-streaming-tools";
79-
var request = new HttpRequestMessage(HttpMethod.Post, url);
80-
using HttpResponseMessage response =
81-
await client.PostAsJsonAsync(request.RequestUri, $"{{\"refreshToken\":\"{OAuth?.RefreshToken}\"}}");
82-
response.EnsureSuccessStatusCode();
83-
string responseBody = await response.Content.ReadAsStringAsync();
84-
var moderatedChannels = JsonConvert.DeserializeObject<TwitchModeratedChannelsResponse>(responseBody);
85-
if (null == moderatedChannels) {
86-
return null;
69+
catch (Exception e) {
70+
Log.Error("Failed to refresh access token", e);
8771
}
8872

8973
return null;

src/TwitchStreamingTools/ViewModels/Pages/ChatViewModel.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
using System;
22
using System.Collections.ObjectModel;
33
using System.Reactive;
4-
using System.Threading.Tasks;
54

65
using Nullinside.Api.Common.Twitch;
76

87
using ReactiveUI;
98

109
using TwitchLib.Client.Events;
11-
using TwitchLib.Client.Interfaces;
1210

1311
using TwitchStreamingTools.Models;
1412

@@ -19,14 +17,14 @@ namespace TwitchStreamingTools.ViewModels.Pages;
1917
/// </summary>
2018
public class ChatViewModel : PageViewModelBase, IDisposable {
2119
/// <summary>
22-
/// The list of chat names selected in the list.
20+
/// The twitch chat client.
2321
/// </summary>
24-
private ObservableCollection<string> _selectedTwitchChatNames = [];
25-
22+
private readonly ITwitchClientProxy _twitchClient;
23+
2624
/// <summary>
27-
/// The twitch chat client.
25+
/// The list of chat names selected in the list.
2826
/// </summary>
29-
private ITwitchClientProxy _twitchClient;
27+
private ObservableCollection<string> _selectedTwitchChatNames = [];
3028

3129
/// <summary>
3230
/// The current position of the cursor for the text box showing our chat logs, increment to move down.

src/TwitchStreamingTools/Views/MainWindow.axaml.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
using System.Reflection;
33
using System.Threading.Tasks;
44

5-
using Avalonia;
65
using Avalonia.Controls;
7-
using Avalonia.Threading;
8-
9-
using Microsoft.Extensions.DependencyInjection;
106

117
using Nullinside.Api.Common.Desktop;
128
#if !DEBUG
9+
using Avalonia.Threading;
10+
using Microsoft.Extensions.DependencyInjection;
1311
using TwitchStreamingTools.ViewModels;
12+
#else
13+
using Avalonia;
1414
#endif
1515

1616
namespace TwitchStreamingTools.Views;
@@ -19,11 +19,6 @@ namespace TwitchStreamingTools.Views;
1919
/// The main application window.
2020
/// </summary>
2121
public partial class MainWindow : Window {
22-
/// <summary>
23-
/// The service provider for DI.
24-
/// </summary>
25-
public IServiceProvider? ServiceProvider { get; set; }
26-
2722
/// <summary>
2823
/// Initializes a new instance of the <see cref="MainWindow" /> class.
2924
/// </summary>
@@ -35,6 +30,11 @@ public MainWindow() {
3530
#endif
3631
}
3732

33+
/// <summary>
34+
/// The service provider for DI.
35+
/// </summary>
36+
public IServiceProvider? ServiceProvider { get; set; }
37+
3838
/// <summary>
3939
/// Checks for a new version number of the application.
4040
/// </summary>
@@ -64,7 +64,7 @@ protected override void OnInitialized() {
6464
if (null == vm) {
6565
return;
6666
}
67-
67+
6868
vm.LocalVersion = localVersion;
6969
Dispatcher.UIThread.Post(async () => {
7070
var versionWindow = new NewVersionWindow {

0 commit comments

Comments
 (0)