forked from JohnnyCrazy/SpotifyAPI-NET
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTokenSwapWebAPIFactory.cs
More file actions
280 lines (249 loc) · 10.8 KB
/
TokenSwapWebAPIFactory.cs
File metadata and controls
280 lines (249 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using SpotifyAPI.Web.Enums;
using SpotifyAPI.Web.Models;
namespace SpotifyAPI.Web.Auth
{
/// <summary>
/// Returns a <see cref="SpotifyWebAPI"/> using the TokenSwapAuth process.
/// </summary>
public class TokenSwapWebAPIFactory
{
/// <summary>
/// Access provided by Spotify expires after 1 hour. If true, <see cref="TokenSwapAuth"/> will time the access tokens, and access will attempt to be silently (without opening a browser) refreshed automatically. This will not make <see cref="OnAccessTokenExpired"/> fire, see <see cref="TimeAccessExpiry"/> for that.
/// </summary>
public bool AutoRefresh { get; set; }
/// <summary>
/// If true when calling <see cref="GetWebApiAsync"/>, will time how long it takes for access to Spotify to expire. The event <see cref="OnAccessTokenExpired"/> fires when the timer elapses.
/// </summary>
public bool TimeAccessExpiry { get; set; }
/// <summary>
/// The maximum time in seconds to wait for a SpotifyWebAPI to be returned. The timeout is cancelled early regardless if an auth success or failure occured.
/// </summary>
public int Timeout { get; set; }
public Scope Scope { get; set; }
/// <summary>
/// The URI (or URL) of the exchange server which exchanges the auth code for access and refresh tokens.
/// </summary>
public string ExchangeServerUri { get; set; }
/// <summary>
/// The URI (or URL) of where a callback server to receive the auth code will be hosted. e.g. http://localhost:4002
/// </summary>
public string HostServerUri { get; set; }
/// <summary>
/// Opens the user's browser and visits the exchange server for you, triggering the key exchange. This should be true unless you want to handle the key exchange in a nicer way.
/// </summary>
public bool OpenBrowser { get; set; }
/// <summary>
/// The HTML to respond with when the callback server has been reached. By default, it is set to close the window on arrival.
/// </summary>
public string HtmlResponse { get; set; }
/// <summary>
/// Whether or not to show a dialog saying "Is this you?" during the initial key exchange. It should be noted that this would allow a user the opportunity to change accounts.
/// </summary>
public bool ShowDialog { get; set; }
/// <summary>
/// The maximum amount of times to retry getting a token.
/// <para/>
/// A token get is attempted every time you <see cref="GetWebApiAsync"/> and <see cref="RefreshAuthAsync"/>. Increasing this may improve how often these actions succeed - although it won't solve any underlying problems causing a get token failure.
/// </summary>
public int MaxGetTokenRetries { get; set; } = 10;
/// <summary>
/// Returns a SpotifyWebAPI using the TokenSwapAuth process.
/// </summary>
/// <param name="exchangeServerUri">The URI (or URL) of the exchange server which exchanges the auth code for access and refresh tokens.</param>
/// <param name="scope"></param>
/// <param name="hostServerUri">The URI (or URL) of where a callback server to receive the auth code will be hosted. e.g. http://localhost:4002</param>
/// <param name="timeout">The maximum time in seconds to wait for a SpotifyWebAPI to be returned. The timeout is cancelled early regardless if an auth success or failure occured.</param>
/// <param name="autoRefresh">Access provided by Spotify expires after 1 hour. If true, access will attempt to be silently (without opening a browser) refreshed automatically.</param>
/// <param name="openBrowser">Opens the user's browser and visits the exchange server for you, triggering the key exchange. This should be true unless you want to handle the key exchange in a nicer way.</param>
public TokenSwapWebAPIFactory(string exchangeServerUri, Scope scope = Scope.None, string hostServerUri = "http://localhost:4002", int timeout = 10, bool autoRefresh = false, bool openBrowser = true)
{
AutoRefresh = autoRefresh;
Timeout = timeout;
Scope = scope;
ExchangeServerUri = exchangeServerUri;
HostServerUri = hostServerUri;
OpenBrowser = openBrowser;
OnAccessTokenExpired += async(sender, e) =>
{
if (AutoRefresh)
{
await RefreshAuthAsync();
}
};
}
Token lastToken;
SpotifyWebAPI lastWebApi;
TokenSwapAuth lastAuth;
public class ExchangeReadyEventArgs : EventArgs
{
public string ExchangeUri { get; set; }
}
/// <summary>
/// When the URI to get an authorization code is ready to be used to be visited. Not required if <see cref="OpenBrowser"/> is true as the exchange URI will automatically be visited for you.
/// </summary>
public event EventHandler<ExchangeReadyEventArgs> OnExchangeReady;
/// <summary>
/// Refreshes the access for a SpotifyWebAPI returned by this factory.
/// </summary>
/// <returns></returns>
public async Task RefreshAuthAsync()
{
Token token = await lastAuth.RefreshAuthAsync(lastToken.RefreshToken);
if (token == null)
{
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs($"Token not returned by server."));
}
else if (token.HasError())
{
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs($"{token.Error} {token.ErrorDescription}"));
}
else if (string.IsNullOrEmpty(token.AccessToken))
{
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs("Token had no access token attached."));
}
else
{
lastWebApi.AccessToken = token.AccessToken;
OnAuthSuccess?.Invoke(this, new AuthSuccessEventArgs());
}
}
// By defining empty EventArgs objects, you can specify additional information later on as you see fit and it won't
// be considered a breaking change to consumers of this API.
//
// They don't even need to be constructed for their associated events to be invoked - just pass the static Empty property.
public class AccessTokenExpiredEventArgs : EventArgs
{
public static new AccessTokenExpiredEventArgs Empty { get; } = new AccessTokenExpiredEventArgs();
public AccessTokenExpiredEventArgs()
{ }
}
/// <summary>
/// When the authorization from Spotify expires. This will only occur if <see cref="AutoRefresh"/> is true.
/// </summary>
public event EventHandler<AccessTokenExpiredEventArgs> OnAccessTokenExpired;
public class AuthSuccessEventArgs : EventArgs
{
public static new AuthSuccessEventArgs Empty { get; } = new AuthSuccessEventArgs();
public AuthSuccessEventArgs()
{ }
}
/// <summary>
/// When an authorization attempt succeeds and gains authorization.
/// </summary>
public event EventHandler<AuthSuccessEventArgs> OnAuthSuccess;
public class AuthFailureEventArgs : EventArgs
{
public static new AuthFailureEventArgs Empty { get; } = new AuthFailureEventArgs("");
public string Error { get; }
public AuthFailureEventArgs(string error)
{
Error = error;
}
}
/// <summary>
/// When an authorization attempt fails to gain authorization.
/// </summary>
public event EventHandler<AuthFailureEventArgs> OnAuthFailure;
/// <summary>
/// Manually triggers the timeout for any ongoing get web API request.
/// </summary>
public void CancelGetWebApiRequest()
{
if (webApiTimeoutTimer == null) return;
// The while loop in GetWebApiSync() will react and trigger the timeout.
webApiTimeoutTimer.Stop();
webApiTimeoutTimer.Dispose();
webApiTimeoutTimer = null;
}
System.Timers.Timer webApiTimeoutTimer;
/// <summary>
/// Gets an authorized and ready to use SpotifyWebAPI by following the SecureAuthorizationCodeAuth process with its current settings.
/// </summary>
/// <returns></returns>
public async Task<SpotifyWebAPI> GetWebApiAsync()
{
return await Task<SpotifyWebAPI>.Factory.StartNew(() =>
{
bool currentlyAuthorizing = true;
// Cancel any ongoing get web API requests
CancelGetWebApiRequest();
lastAuth = new TokenSwapAuth(
exchangeServerUri: ExchangeServerUri,
serverUri: HostServerUri,
scope: Scope,
htmlResponse: HtmlResponse)
{
ShowDialog = ShowDialog,
MaxGetTokenRetries = MaxGetTokenRetries,
TimeAccessExpiry = AutoRefresh || TimeAccessExpiry
};
lastAuth.AuthReceived += async(sender, response) =>
{
if (!string.IsNullOrEmpty(response.Error) || string.IsNullOrEmpty(response.Code))
{
// We only want one auth failure to be fired, if the request timed out then don't bother.
if (!webApiTimeoutTimer.Enabled) return;
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs(response.Error));
currentlyAuthorizing = false;
return;
}
lastToken = await lastAuth.ExchangeCodeAsync(response.Code);
if (lastToken == null || lastToken.HasError() || string.IsNullOrEmpty(lastToken.AccessToken))
{
// We only want one auth failure to be fired, if the request timed out then don't bother.
if (!webApiTimeoutTimer.Enabled) return;
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs("Exchange token not returned by server."));
currentlyAuthorizing = false;
return;
}
if (lastWebApi != null)
{
lastWebApi.Dispose();
}
lastWebApi = new SpotifyWebAPI()
{
TokenType = lastToken.TokenType,
AccessToken = lastToken.AccessToken
};
lastAuth.Stop();
OnAuthSuccess?.Invoke(this, AuthSuccessEventArgs.Empty);
currentlyAuthorizing = false;
};
lastAuth.OnAccessTokenExpired += async(sender, e) =>
{
if (TimeAccessExpiry)
{
OnAccessTokenExpired?.Invoke(sender, AccessTokenExpiredEventArgs.Empty);
}
if (AutoRefresh)
{
await RefreshAuthAsync();
}
};
lastAuth.Start();
OnExchangeReady?.Invoke(this, new ExchangeReadyEventArgs { ExchangeUri = lastAuth.GetUri() });
if (OpenBrowser)
{
lastAuth.OpenBrowser();
}
webApiTimeoutTimer = new System.Timers.Timer
{
AutoReset = false,
Enabled = true,
Interval = Timeout * 1000
};
while (currentlyAuthorizing && webApiTimeoutTimer.Enabled);
// If a timeout occurred
if (lastWebApi == null && currentlyAuthorizing)
{
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs("Authorization request has timed out."));
}
return lastWebApi;
});
}
}
}