Skip to content
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# TwitchLib.Api
API component of TwitchLib.

For a general overview and example, refer to https://github.com/TwitchLib/TwitchLib/blob/master/README.md
For a general overview and examples, refer to https://github.com/TwitchLib/TwitchLib/blob/master/README.md

For Helix API Examples, refer to https://github.com/TwitchLib/TwitchLib.Api/blob/master/TwitchLib.Api.Helix/README.MD

```csharp
using System;
Expand Down
56 changes: 56 additions & 0 deletions TwitchLib.Api.Core.Interfaces/IApiSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,70 @@

namespace TwitchLib.Api.Core.Interfaces
{
/// <summary>
/// These are settings that are shared throughout the application. Define these before
/// creating an instance of TwitchAPI
/// </summary>
public interface IApiSettings
{
/// <summary>
/// The current client credential access token. Provides limited access to some of the TwitchAPI.
/// </summary>
string AccessToken { get; set; }
/// <summary>
/// Your application's Secret. Do not expose this to anyone else. This comes from the Twitch developer panel. https://dev.twitch.tv/console
/// </summary>
string Secret { get; set; }
/// <summary>
/// Your application's client ID. This comes from the Twitch developer panel. https://dev.twitch.tv/console
/// </summary>
string ClientId { get; set; }
/// <summary>
/// This does not appear to be used.
/// </summary>
bool SkipDynamicScopeValidation { get; set; }
/// <summary>
/// If AccessToken is null, and this is set to true, then Helix API calls will not attempt to use
/// the ClientID/Secret to generate a client_credential access token.
/// </summary>
bool SkipAutoServerTokenGeneration { get; set; }
/// <summary>
/// Add scopes that your application will be using to this collection before calling any Helix APIs.
/// A list of scopes can be found here: https://dev.twitch.tv/docs/authentication/scopes/
/// See the TwitchAPI reference for the scopes specific to each API.
/// Note: Do not add ALL the scopes, or your account may be banned (see warning here: https://dev.twitch.tv/docs/authentication/scopes/)
/// </summary>
List<AuthScopes> Scopes { get; set; }
/// <summary>
/// Set this value to another port if you have another application already listening to port 5000 on your machine.
/// Defaults to: 5000
/// </summary>
int OAuthResponsePort { get; set; }
/// <summary>
/// Set this value to a hostname or IP address if you have a multi-homed machine (more than one IP address)
/// and you would like to bind the OAuth response listener to a specific IP address. Defaults to 'localhost'
/// </summary>
string OAuthResponseHostname { get; set; }
/// <summary>
/// Storage for oAuth refresh token, expiration dates, etc. Defaults to %AppData%\\TwitchLib.API\\[ApplicationName].json
/// Set this if you will be running multiple instances of the same application that you would like to use with different
/// user tokens.
/// </summary>
string OAuthTokenFile { get; set; }
/// <summary>
/// Set this value to true to enable Helix calls that require an oAuth User Token. This requires you to also set
/// ApiSettings.ClientID and ApiSettings.Secret.
/// </summary>
bool UseUserTokenForHelixCalls { get; set; }
/// <summary>
/// Setting this value to true will enable storage of the oAuth refresh token and other data. This storage will be done in
/// an unencrypted, insecure local file. Anyone else with access to your computer could read this file and gain access to
/// your Twitch account in unexpected ways. Only set this value to true if you have properly secured your computer.
/// If you do not set this value to True, and UseUserTokenForHelixCalls = True, a browser window will always open on the
/// first call to any Helix API to perform the OAuth handshake.
/// Defaults to: False
/// </summary>
bool EnableInsecureTokenStorage { get; set; }

event PropertyChangedEventHandler PropertyChanged;
}
Expand Down
22 changes: 22 additions & 0 deletions TwitchLib.Api.Core.Interfaces/IUserAccessTokenManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using TwitchLib.Api.Core.Interfaces;

namespace TwitchLib.Api.Core.Interfaces
{
/// <summary>
/// Enables API calls to use user access tokens instead of client credentials. Most of the best parts of
/// the Twitch API are only available when using user access tokens.
/// </summary>
public interface IUserAccessTokenManager
{
/// <summary>
/// Uses the Authoization Grant flow to get an access code to get a token and refresh token.
/// https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#authorization-code-grant-flow
/// </summary>
/// <returns></returns>
Task<string> GetUserAccessToken();
}
}
36 changes: 32 additions & 4 deletions TwitchLib.Api.Core/ApiBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@

namespace TwitchLib.Api.Core
{
/// <summary>
/// A base class for any method calling the Twitch API. Provides authorization credentials and
/// abstracts for calling basic web methods.
/// </summary>
public class ApiBase
{
private readonly TwitchLibJsonSerializer _jsonSerializer;
private readonly IUserAccessTokenManager _userAccessTokenManager;
protected readonly IApiSettings Settings;
private readonly IRateLimiter _rateLimiter;
private readonly IHttpCallHandler _http;
Expand All @@ -25,20 +30,30 @@ public class ApiBase
private DateTime? _serverBasedAccessTokenExpiry;
private string _serverBasedAccessToken;

public ApiBase(IApiSettings settings, IRateLimiter rateLimiter, IHttpCallHandler http)
/// <summary>
/// Standard constructor for all derived API methods.
/// </summary>
/// <param name="settings">Can be null.</param>
/// <param name="rateLimiter">Can be null.</param>
/// <param name="http">Can be null.</param>
/// <param name="userAccessTokenManager">Can be null.</param>
public ApiBase(IApiSettings settings, IRateLimiter rateLimiter, IHttpCallHandler http, IUserAccessTokenManager userAccessTokenManager)
{
Settings = settings;
_rateLimiter = rateLimiter;
_http = http;
_jsonSerializer = new TwitchLibJsonSerializer();
_userAccessTokenManager = userAccessTokenManager;
}

public async ValueTask<string> GetAccessTokenAsync(string accessToken = null)
private async ValueTask<string> GetAccessTokenAsync(string accessToken = null)
{
if (!string.IsNullOrWhiteSpace(accessToken))
return accessToken;
if (!string.IsNullOrWhiteSpace(Settings.AccessToken))
return Settings.AccessToken;
if (Settings.UseUserTokenForHelixCalls && _userAccessTokenManager != null)
return await GenerateUserAccessToken();
if (!string.IsNullOrWhiteSpace(Settings.Secret) && !string.IsNullOrWhiteSpace(Settings.ClientId) && !Settings.SkipAutoServerTokenGeneration)
{
if (_serverBasedAccessTokenExpiry == null || _serverBasedAccessTokenExpiry - TimeSpan.FromMinutes(1) < DateTime.Now)
Expand All @@ -50,6 +65,11 @@ public async ValueTask<string> GetAccessTokenAsync(string accessToken = null)
return null;
}

private async Task<string> GenerateUserAccessToken()
{
return await _userAccessTokenManager.GetUserAccessToken();
}

internal async Task<string> GenerateServerBasedAccessToken()
{
var result = await _http.GeneralRequestAsync($"{BaseAuth}/token?client_id={Settings.ClientId}&client_secret={Settings.Secret}&grant_type=client_credentials", "POST", null, ApiVersion.Auth, Settings.ClientId, null).ConfigureAwait(false);
Expand Down Expand Up @@ -338,10 +358,18 @@ private string ConstructResourceUrl(string resource = null, List<KeyValuePair<st
{
for (var i = 0; i < getParams.Count; i++)
{
// When "after" is null, then Uri.EscapeDataString dies with null exception.
var value = "";

if (getParams[i].Value != null)
value = getParams[i].Value;

if (i == 0)
url += $"?{getParams[i].Key}={Uri.EscapeDataString(getParams[i].Value)}";
url += "?";
else
url += $"&{getParams[i].Key}={Uri.EscapeDataString(getParams[i].Value)}";
url += "&";

url += $"{getParams[i].Key}={Uri.EscapeDataString(value)}";
}
}
return url;
Expand Down
120 changes: 118 additions & 2 deletions TwitchLib.Api.Core/ApiSettings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using TwitchLib.Api.Core.Enums;
Expand All @@ -13,7 +14,15 @@ public class ApiSettings : IApiSettings, INotifyPropertyChanged
private string _accessToken;
private bool _skipDynamicScopeValidation;
private bool _skipAutoServerTokenGeneration;
private List<AuthScopes> _scopes;
private List<AuthScopes> _scopes = new List<AuthScopes>();
private int _oauthResponsePort = 5000;
private string _oAuthResponseHostname = "localhost";
private string _oauthTokenFile = System.Environment.ExpandEnvironmentVariables("%AppData%\\TwitchLib.API\\" + System.Diagnostics.Process.GetCurrentProcess().ProcessName + ".json");
private bool _useUserTokenForHelixCalls = false;
private bool _enableInsecureTokenStorage = false;
private IUserAccessTokenManager _userAccessTokenManager;


public string ClientId
{
get => _clientId;
Expand Down Expand Up @@ -74,6 +83,12 @@ public bool SkipAutoServerTokenGeneration
}
}
}
/// <summary>
/// Add scopes that your application will be using to this collection before calling any Helix APIs.
/// A list of scopes can be found here: https://dev.twitch.tv/docs/authentication/scopes/
/// See the TwitchAPI reference for the scopes specific to each API.
/// Note: Do not add ALL the scopes, or your account may be banned (see warning here: https://dev.twitch.tv/docs/authentication/scopes/)
/// </summary>
public List<AuthScopes> Scopes
{
get => _scopes;
Expand All @@ -87,6 +102,107 @@ public List<AuthScopes> Scopes
}
}

/// <summary>
/// If you are using TwitchLib.Api and make calls to API endpoints that require a user token, then you can use
/// your ClientSecret and ClientID to establish an OAuth token for your service. Part of this token generation
/// process requires Twitch to authenticate your application using your browser. Twitch will return your browser
/// back to this library for token storage so, this library needs to listen for your browser's request on a port.
/// By default, this is port 5000. If you have another application also on port 5000, set this to another open port.
/// </summary>
public int OAuthResponsePort
{
get => _oauthResponsePort;
set
{
if (value != _oauthResponsePort)
{
_oauthResponsePort = value;
NotifyPropertyChanged();
}
}
}

/// <summary>
/// Set this value to a hostname or IP address if you have a multi-homed machine (more than one IP address)
/// and you would like to bind the OAuth response listener to a specific IP address. Defaults to 'localhost'
/// </summary>
public string OAuthResponseHostname
{
get => _oAuthResponseHostname;
set
{
if (value != _oAuthResponseHostname)
{
_oAuthResponseHostname = value;
NotifyPropertyChanged();
}
}
}

/// <summary>
/// Storage for oAuth refresh token, expiration dates, etc. Defaults to %AppData%\\TwitchLib.API\\[ApplicationName].json
/// Set this if you will be running multiple instances of the same application that you would like to use with different
/// user tokens.
/// </summary>
public string OAuthTokenFile
{
get => _oauthTokenFile;
set
{
if (value != _oauthTokenFile)
{
_oauthTokenFile = value;
NotifyPropertyChanged();
}
}
}

/// <summary>
/// Set this value to true to enable Helix calls that require an oAuth User Token. This requires you to also set
/// ApiSettings.ClientID and ApiSettings.Secret.
/// </summary>
public bool UseUserTokenForHelixCalls
{
get => _useUserTokenForHelixCalls;
set
{
if (value != _useUserTokenForHelixCalls)
{
if (String.IsNullOrWhiteSpace(ClientId) == true || String.IsNullOrWhiteSpace(Secret) == true)
throw new Exception("You must set ApiSettings.ClientId and ApiSettings.Secret before you can enable this setting.");

_useUserTokenForHelixCalls = value;
NotifyPropertyChanged();
}
}
}

/// <summary>
/// Setting this value to true will enable storage of the oAuth refresh token and other data. This storage will be done in
/// an unencrypted, insecure local file. Anyone else with access to your computer could read this file and gain access to
/// your Twitch account in unexpected ways. Only set this value to true if you have properly secured your computer.
/// If you do not set this value to True, and UseUserTokenForHelixCalls = True, a browser window will always open on the
/// first call to any Helix API to perform the OAuth handshake.
/// Defaults to: False
/// </summary>
public bool EnableInsecureTokenStorage
{
get => _enableInsecureTokenStorage;
set
{
if (value != _enableInsecureTokenStorage)
{
_enableInsecureTokenStorage = value;
NotifyPropertyChanged();
}
}
}



/// <summary>
/// This event fires when ever a property is changed on the settings class.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;

private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
Expand Down
9 changes: 9 additions & 0 deletions TwitchLib.Api.Core/Exceptions/HttpResponseException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,19 @@ public class HttpResponseException : Exception
/// Null if using <see cref="TwitchLib.Api.Core.HttpCallHandlers.TwitchWebRequest"/> or <see cref="TwitchLib.Api.Core.Undocumented.Undocumented"/>
/// </summary>
public HttpResponseMessage HttpResponse { get; }
public string HttpResponseContent { get; }

public HttpResponseException(string apiData, HttpResponseMessage httpResponse) : base(apiData)
{
HttpResponse = httpResponse;

try
{
HttpResponseContent = httpResponse.Content.ReadAsStringAsync().Result;
} catch (Exception ex)
{
HttpResponseContent = $"Couldn't read response from server: {ex.ToString()}";
}
}
}
}
Loading