diff --git a/src/Nullinside.Api.Common.AspNetCore/Nullinside.Api.Common.AspNetCore.csproj b/src/Nullinside.Api.Common.AspNetCore/Nullinside.Api.Common.AspNetCore.csproj index 83838e7..1749a67 100644 --- a/src/Nullinside.Api.Common.AspNetCore/Nullinside.Api.Common.AspNetCore.csproj +++ b/src/Nullinside.Api.Common.AspNetCore/Nullinside.Api.Common.AspNetCore.csproj @@ -20,8 +20,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + diff --git a/src/Nullinside.Api.Common/Auth/AuthUtils.cs b/src/Nullinside.Api.Common/Auth/AuthUtils.cs index 58d3fd6..f5c343a 100644 --- a/src/Nullinside.Api.Common/Auth/AuthUtils.cs +++ b/src/Nullinside.Api.Common/Auth/AuthUtils.cs @@ -3,7 +3,7 @@ namespace Nullinside.Api.Common.Auth; /// -/// Random utilities for authentication. +/// Random utilities for authentication. /// public static class AuthUtils { /// diff --git a/src/Nullinside.Api.Common/Desktop/GitHubUpdateManager.cs b/src/Nullinside.Api.Common/Desktop/GitHubUpdateManager.cs index b3db17d..2b12728 100644 --- a/src/Nullinside.Api.Common/Desktop/GitHubUpdateManager.cs +++ b/src/Nullinside.Api.Common/Desktop/GitHubUpdateManager.cs @@ -7,7 +7,7 @@ namespace Nullinside.Api.Common.Desktop; /// /// Handles checking for updates to the application via GitHub releases. /// -public class GitHubUpdateManager { +public static class GitHubUpdateManager { /// /// Gets the latest version number of the release. /// diff --git a/src/Nullinside.Api.Common/Docker/DockerProxy.cs b/src/Nullinside.Api.Common/Docker/DockerProxy.cs index dbab87d..2113312 100644 --- a/src/Nullinside.Api.Common/Docker/DockerProxy.cs +++ b/src/Nullinside.Api.Common/Docker/DockerProxy.cs @@ -128,7 +128,14 @@ public async Task TurnOnOffDockerCompose(string name, bool turnOn, Cancell (!turnOn && output.error.Contains("Stopped", StringComparison.InvariantCultureIgnoreCase)); } - private async Task<(string output, string error)> ExecuteCommand(string command, CancellationToken token, + /// + /// Executes an SSH command in the given directory. + /// + /// The command to execute. + /// The cancellation token. + /// The directory to navigate to before executing the command. + /// The STDOUT and STDERR of the command's execution. + private async Task<(string output, string error)> ExecuteCommand(string command, CancellationToken token = new(), string? dir = null) { using SshClient client = new(_server!, _username!, _password!); await client.ConnectAsync(token); diff --git a/src/Nullinside.Api.Common/Nullinside.Api.Common.csproj b/src/Nullinside.Api.Common/Nullinside.Api.Common.csproj index 829e3f0..6f1bfc2 100644 --- a/src/Nullinside.Api.Common/Nullinside.Api.Common.csproj +++ b/src/Nullinside.Api.Common/Nullinside.Api.Common.csproj @@ -16,10 +16,10 @@ - - + + - + diff --git a/src/Nullinside.Api.Common/SqlScripts.cs b/src/Nullinside.Api.Common/SqlScripts.cs index 2236603..80299c8 100644 --- a/src/Nullinside.Api.Common/SqlScripts.cs +++ b/src/Nullinside.Api.Common/SqlScripts.cs @@ -71,7 +71,7 @@ public static class SqlScripts { """; /// - /// The inner function for the . Not meant to be called directly. + /// Removes . /// public const string LEVENSHTEIN_DISTANCE_SEARCH_INNER_FUNCTION_REMOVE = """ diff --git a/src/Nullinside.Api.Common/Twitch/ITwitchApiProxy.cs b/src/Nullinside.Api.Common/Twitch/ITwitchApiProxy.cs new file mode 100644 index 0000000..f5d651c --- /dev/null +++ b/src/Nullinside.Api.Common/Twitch/ITwitchApiProxy.cs @@ -0,0 +1,107 @@ +using Nullinside.Api.Common.Twitch.Json; + +using TwitchLib.Api.Helix.Models.Chat.GetChatters; +using TwitchLib.Api.Helix.Models.Moderation.BanUser; +using TwitchLib.Api.Helix.Models.Moderation.GetModerators; + +namespace Nullinside.Api.Common.Twitch; + +/// +/// The proxy for handling communication with Twitch. +/// +public interface ITwitchApiProxy { + /// + /// The Twitch access token. These are the credentials used for all requests. + /// + TwitchAccessToken? OAuth { get; set; } + + /// + /// Creates a new access token from a code using Twitch's OAuth workflow. + /// + /// The code from twitch to send back to twitch to generate a new access token. + /// The cancellation token. + /// The object will have its updated with the new settings for the token. + /// The OAuth details if successful, null otherwise. + Task CreateAccessToken(string code, CancellationToken token = new()); + + /// + /// Refreshes the access token. + /// + /// The cancellation token. + /// The object will have its updated with the new settings for the token. + /// The OAuth details if successful, null otherwise. + Task RefreshAccessToken(CancellationToken token = new()); + + /// + /// Determines if the is valid. + /// + /// The cancellation token. + /// True if valid, false otherwise. + Task GetAccessTokenIsValid(CancellationToken token = new()); + + /// + /// Gets the twitch id and username of the owner of the . + /// + /// The cancellation token. + /// The twitch username if successful, null otherwise. + Task<(string? id, string? username)> GetUser(CancellationToken token = new()); + + /// + /// Gets the email address of the owner of the . + /// + /// The cancellation token. + /// The email address if successful, null otherwise. + Task GetUserEmail(CancellationToken token = new()); + + /// + /// Gets the list of channels the user moderates for. + /// + /// The twitch id to scan. + /// The list of channels the user moderates for. + Task> GetUserModChannels(string userId); + + /// + /// Bans a list of users from a channel. + /// + /// The twitch id of the channel to ban the users from. + /// The twitch id of the bot user, the one banning the users. + /// The list of users to ban. + /// The reason for the ban. + /// The stopping token. + /// The users with confirmed bans. + Task> BanChannelUsers(string channelId, string botId, + IEnumerable<(string Id, string Username)> users, string reason, CancellationToken token = new()); + + /// + /// Gets the list of mods for the channel. + /// + /// The twitch id of the channel to get mods for. + /// The cancellation token. + /// The collection of moderators. + Task> GetChannelMods(string channelId, CancellationToken token = new()); + + /// + /// Gets the chatters currently in a channel. + /// + /// The twitch id of the channel that we are moderating. + /// The twitch id of the bot. + /// The cancellation token. + /// The collection of chatters. + Task> GetChannelUsers(string channelId, string botId, CancellationToken token = new()); + + /// + /// Checks if the supplied channels are live. + /// + /// The twitch ids of the channels. + /// The list of twitch channels that are currently live. + Task> GetChannelsLive(IEnumerable userIds); + + /// + /// Makes a user a moderator in a channel. + /// + /// The twitch id of the channel to add the mod to. + /// The twitch id to give the moderator role. + /// The cancellation token. + /// True if successful, false otherwise. + Task AddChannelMod(string channelId, string userId, CancellationToken token = new()); +} \ No newline at end of file diff --git a/src/Nullinside.Api.Common/Twitch/TwitchAccessToken.cs b/src/Nullinside.Api.Common/Twitch/TwitchAccessToken.cs new file mode 100644 index 0000000..30f317a --- /dev/null +++ b/src/Nullinside.Api.Common/Twitch/TwitchAccessToken.cs @@ -0,0 +1,21 @@ +namespace Nullinside.Api.Common.Twitch; + +/// +/// Represents an OAuth token in the Twitch workflow. +/// +public class TwitchAccessToken { + /// + /// The Twitch access token. + /// + public string? AccessToken { get; set; } + + /// + /// The refresh token. + /// + public string? RefreshToken { get; set; } + + /// + /// The UTC when the expires. + /// + public DateTime? ExpiresUtc { get; set; } +} \ No newline at end of file diff --git a/src/Nullinside.Api.Common/Twitch/TwitchApiProxy.cs b/src/Nullinside.Api.Common/Twitch/TwitchApiProxy.cs index e5a97cc..e720d30 100644 --- a/src/Nullinside.Api.Common/Twitch/TwitchApiProxy.cs +++ b/src/Nullinside.Api.Common/Twitch/TwitchApiProxy.cs @@ -20,209 +20,159 @@ namespace Nullinside.Api.Common.Twitch; /// -/// A proxy for making twitch requests. +/// The proxy for handling communication with Twitch. /// -public class TwitchApiProxy { +public class TwitchApiProxy : ITwitchApiProxy { /// /// The logger. /// private static readonly ILog Log = LogManager.GetLogger(typeof(TwitchApiProxy)); - /// - /// Initializes a new instance of the class. - /// - public TwitchApiProxy() { - } - - /// - /// Initializes a new instance of the class. - /// - /// The access token. - /// The refresh token. - /// When the token expires (utc). - public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires) { - AccessToken = token; - RefreshToken = refreshToken; - ExpiresUtc = tokenExpires; - } - /// /// The, public, twitch client id. /// - public static string ClientId { get; } = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_ID")!; + private static readonly string ClientId = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_ID")!; /// /// The, private, twitch client secret. /// - public static string ClientSecret { get; } = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_SECRET")!; + private static readonly string ClientSecret = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_SECRET")!; /// /// The redirect url. /// - public static string ClientRedirect { get; } = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_REDIRECT")!; - - /// - /// The Twitch access token. - /// - public string? AccessToken { get; set; } + private static readonly string ClientRedirect = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_REDIRECT")!; /// - /// The refresh token. + /// Initializes a new instance of the class. /// - public string? RefreshToken { get; set; } + public TwitchApiProxy() { + } /// - /// The UTC when the expires. + /// Initializes a new instance of the class. /// - public DateTime? ExpiresUtc { get; set; } + /// The access token. + /// The refresh token. + /// When the token expires (utc). + public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires) { + OAuth = new TwitchAccessToken { + AccessToken = token, + RefreshToken = refreshToken, + ExpiresUtc = tokenExpires + }; + } /// /// The number of times to retry queries before giving up. /// public int Retries { get; set; } = 3; - /// - /// Gets a new instance of the . - /// - /// A new instance of the . - private TwitchAPI GetApi() { - var api = new TwitchAPI { - Settings = { - ClientId = ClientId, - AccessToken = AccessToken - } + /// + public TwitchAccessToken? OAuth { get; set; } + + /// + public async Task CreateAccessToken(string code, CancellationToken token = new()) { + TwitchAPI api = GetApi(); + AuthCodeResponse? response = await api.Auth.GetAccessTokenFromCodeAsync(code, ClientSecret, ClientRedirect); + if (null == response) { + return null; + } + + OAuth = new TwitchAccessToken { + AccessToken = response.AccessToken, + RefreshToken = response.RefreshToken, + ExpiresUtc = DateTime.UtcNow + TimeSpan.FromSeconds(response.ExpiresIn) }; - return api; + return OAuth; } - /// - /// Refreshes the access token. - /// - /// The cancellation token. - /// The object will have it's properties updated with the new settings for the token. - /// True if successful, false otherwise. - public async Task RefreshTokenAsync(CancellationToken token = new()) { + /// + public async Task RefreshAccessToken(CancellationToken token = new()) { try { TwitchAPI api = GetApi(); - RefreshResponse? response = await api.Auth.RefreshAuthTokenAsync(RefreshToken, ClientSecret, ClientId); + RefreshResponse? response = await api.Auth.RefreshAuthTokenAsync(OAuth?.RefreshToken, ClientSecret, ClientId); if (null == response) { - return false; + return null; } - AccessToken = response.AccessToken; - RefreshToken = response.RefreshToken; - ExpiresUtc = DateTime.UtcNow + TimeSpan.FromSeconds(response.ExpiresIn); - return true; + OAuth = new TwitchAccessToken { + AccessToken = response.AccessToken, + RefreshToken = response.RefreshToken, + ExpiresUtc = DateTime.UtcNow + TimeSpan.FromSeconds(response.ExpiresIn) + }; + return OAuth; } catch (Exception) { - return false; + return null; } } - /// - /// Gets a new access token. - /// - /// The code to send to twitch to generate a new access token. - /// The cancellation token. - /// The object will have it's properties updated with the new settings for the token. - /// True if successful, false otherwise. - public async Task GetAccessToken(string code, CancellationToken token = new()) { - TwitchAPI api = GetApi(); - AuthCodeResponse? response = await api.Auth.GetAccessTokenFromCodeAsync(code, ClientSecret, ClientRedirect); - if (null == response) { - return false; - } - - AccessToken = response.AccessToken; - RefreshToken = response.RefreshToken; - ExpiresUtc = DateTime.UtcNow + TimeSpan.FromSeconds(response.ExpiresIn); - return true; + /// + public async Task GetAccessTokenIsValid(CancellationToken token = new()) { + return !string.IsNullOrWhiteSpace((await GetUser(token)).id); } - /// - /// Gets the email address of the owner of the . - /// - /// The cancellation token. - /// The email address if successful, null otherwise. - public async Task GetUserEmail(CancellationToken token = new()) { + /// + public async Task<(string? id, string? username)> GetUser(CancellationToken token = new()) { return await Retry.Execute(async () => { TwitchAPI api = GetApi(); GetUsersResponse? response = await api.Helix.Users.GetUsersAsync(); if (null == response) { - return null; + return (null, null); } - return response.Users.FirstOrDefault()?.Email; + User? user = response.Users.FirstOrDefault(); + return (user?.Id, user?.Login); }, Retries, token); } - /// - /// Gets the twitch username of the owner of the . - /// - /// The cancellation token. - /// The twitch username if successful, null otherwise. - public async Task<(string? id, string? username)> GetTwitchUser(CancellationToken token = new()) { + /// + public async Task GetUserEmail(CancellationToken token = new()) { return await Retry.Execute(async () => { TwitchAPI api = GetApi(); GetUsersResponse? response = await api.Helix.Users.GetUsersAsync(); if (null == response) { - return (null, null); + return null; } - User? user = response.Users.FirstOrDefault(); - return (user?.Id, user?.Login); + return response.Users.FirstOrDefault()?.Email; }, Retries, token); } - /// - /// Determines if the API has valid credentials. - /// - /// The cancellation token. - /// True if successful, null otherwise. - public async Task IsValid(CancellationToken token = new()) { - return !string.IsNullOrWhiteSpace((await GetTwitchUser(token)).id); - } + /// + public async Task> GetUserModChannels(string userId) { + using var client = new HttpClient(); - /// - /// Gets the chatters in a channel. - /// - /// The id of the channel that we are moderating. - /// The id of the bot channel. - /// The cancellation token. - /// The collection of chatters. - public async Task> GetChattersInChannel(string channelId, string botId, - CancellationToken token = new()) { - return await Retry.Execute(async () => { - TwitchAPI api = GetApi(); - var chatters = new List(); - string? cursor = null; - int total = 0; - do { - GetChattersResponse? response = await api.Helix.Chat.GetChattersAsync(channelId, botId, 1000, cursor); - if (null == response) { - break; - } + var ret = new List(); + string? cursor = null; + do { + string url = $"https://api.twitch.tv/helix/moderation/channels?user_id={userId}&first=100"; + if (null != cursor) { + url += $"&after={cursor}"; + } - chatters.AddRange(response.Data); - cursor = response.Pagination.Cursor; - total = response.Total; - } while (null != cursor); + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("Authorization", $"Bearer {OAuth?.AccessToken}"); + request.Headers.Add("Client-Id", ClientId); - Debug.Assert(chatters.Count == total); - return chatters; - }, Retries, token); + using HttpResponseMessage response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + string responseBody = await response.Content.ReadAsStringAsync(); + var moderatedChannels = JsonConvert.DeserializeObject(responseBody); + if (null == moderatedChannels) { + break; + } + + ret.AddRange(moderatedChannels.data); + cursor = moderatedChannels.pagination.cursor; + } while (null != cursor); + + return ret; } - /// - /// Bans a list of users. - /// - /// The twitch account id of the channel to ban the users from. - /// The twitch account id of the bot user. - /// The list of users to ban. - /// The reason for the ban. - /// The stopping token. - /// The users with confirmed bans. - public async Task> BanUsers(string channelId, string botId, + /// + public async Task> BanChannelUsers(string channelId, string botId, IEnumerable<(string Id, string Username)> users, string reason, CancellationToken token = new()) { return await Retry.Execute(async () => { TwitchAPI api = GetApi(); @@ -255,13 +205,56 @@ public async Task> BanUsers(string channelId, string bot }, Retries, token); } - /// - /// Gets the list of mods for the channel. - /// - /// The twitch id of the channel to get mods for. - /// The cancellation token. - /// The collection of moderators. - public async Task> GetMods(string channelId, CancellationToken token = new()) { + /// + public async Task> GetChannelUsers(string channelId, string botId, + CancellationToken token = new()) { + return await Retry.Execute(async () => { + TwitchAPI api = GetApi(); + var chatters = new List(); + string? cursor = null; + int total = 0; + do { + GetChattersResponse? response = await api.Helix.Chat.GetChattersAsync(channelId, botId, 1000, cursor); + if (null == response) { + break; + } + + chatters.AddRange(response.Data); + cursor = response.Pagination.Cursor; + total = response.Total; + } while (null != cursor); + + Debug.Assert(chatters.Count == total); + return chatters; + }, Retries, token); + } + + /// + public async Task> GetChannelsLive(IEnumerable userIds) { + TwitchAPI api = GetApi(); + + // We can only query 100 at a time, so throttle the search. + var liveUsers = new List(); + string[] twitchIdsArray = userIds.ToArray(); + for (int i = 0; i < twitchIdsArray.Length; i += 100) { + int lastIndex = i + 100; + if (lastIndex > twitchIdsArray.Length) { + lastIndex = twitchIdsArray.Length; + } + + GetStreamsResponse? response = + await api.Helix.Streams.GetStreamsAsync(userIds: twitchIdsArray[i..lastIndex].ToList()); + if (null != response) { + liveUsers.AddRange(response.Streams.Where(s => + "live".Equals(s.Type, StringComparison.InvariantCultureIgnoreCase))); + } + } + + return liveUsers.Select(l => l.UserId); + } + + /// + public async Task> GetChannelMods(string channelId, CancellationToken token = new()) { return await Retry.Execute(async () => { TwitchAPI api = GetApi(); @@ -287,14 +280,8 @@ public async Task> BanUsers(string channelId, string bot }, Retries, token); } - /// - /// Makes the bot account a mod. - /// - /// The twitch id of the channel to add the mod to. - /// The twitch user id to mod. - /// The cancellation token. - /// True if successful, false otherwise. - public async Task ModAccount(string channelId, string userId, CancellationToken token = new()) { + /// + public async Task AddChannelMod(string channelId, string userId, CancellationToken token = new()) { return await Retry.Execute(async () => { TwitchAPI api = GetApi(); await api.Helix.Moderation.AddChannelModeratorAsync(channelId, userId); @@ -303,65 +290,16 @@ public async Task> BanUsers(string channelId, string bot } /// - /// Gets the list of channels the supplied user moderates for. - /// - /// The user id to scan. - /// The list of channels the supplied user moderates for. - public async Task> GetChannelsWeMod(string userId) { - using var client = new HttpClient(); - - var ret = new List(); - string? cursor = null; - do { - string url = $"https://api.twitch.tv/helix/moderation/channels?user_id={userId}&first=100"; - if (null != cursor) { - url += $"&after={cursor}"; - } - - var request = new HttpRequestMessage(HttpMethod.Get, url); - request.Headers.Add("Authorization", $"Bearer {AccessToken}"); - request.Headers.Add("Client-Id", ClientId); - - using HttpResponseMessage response = await client.SendAsync(request); - response.EnsureSuccessStatusCode(); - string responseBody = await response.Content.ReadAsStringAsync(); - var moderatedChannels = JsonConvert.DeserializeObject(responseBody); - if (null == moderatedChannels) { - break; - } - - ret.AddRange(moderatedChannels.data); - cursor = moderatedChannels.pagination.cursor; - } while (null != cursor); - - return ret; - } - - /// - /// Checks if the supplied channels are live. + /// Gets a new instance of the . /// - /// The twitch user ids. - /// The list of twitch users that are currently live. - public async Task> GetLiveChannels(IEnumerable userIds) { - TwitchAPI api = GetApi(); - - // We can only query 100 at a time, so throttle the search. - var liveUsers = new List(); - string[] twitchIdsArray = userIds.ToArray(); - for (int i = 0; i < twitchIdsArray.Length; i += 100) { - int lastIndex = i + 100; - if (lastIndex > twitchIdsArray.Length) { - lastIndex = twitchIdsArray.Length; - } - - GetStreamsResponse? response = - await api.Helix.Streams.GetStreamsAsync(userIds: twitchIdsArray[i..lastIndex].ToList()); - if (null != response) { - liveUsers.AddRange(response.Streams.Where(s => - "live".Equals(s.Type, StringComparison.InvariantCultureIgnoreCase))); + /// A new instance of the . + private TwitchAPI GetApi() { + var api = new TwitchAPI { + Settings = { + ClientId = ClientId, + AccessToken = OAuth?.AccessToken } - } - - return liveUsers.Select(l => l.UserId); + }; + return api; } } \ No newline at end of file diff --git a/src/Nullinside.Api.Model/INullinsideContext.cs b/src/Nullinside.Api.Model/INullinsideContext.cs index c3a7d10..f75cacf 100644 --- a/src/Nullinside.Api.Model/INullinsideContext.cs +++ b/src/Nullinside.Api.Model/INullinsideContext.cs @@ -6,7 +6,7 @@ namespace Nullinside.Api.Model; /// -/// Represents the nullinside database. +/// Represents the nullinside database. /// public interface INullinsideContext : IAsyncDisposable { /// @@ -60,20 +60,20 @@ public interface INullinsideContext : IAsyncDisposable { DatabaseFacade Database { get; } /// - /// Saves all changes made in this context to the database. + /// Saves all changes made in this context to the database. /// /// A to observe while waiting for the task to complete. /// - /// A task that represents the asynchronous save operation. The task result contains the - /// number of state entries written to the database. + /// A task that represents the asynchronous save operation. The task result contains the + /// number of state entries written to the database. /// Task SaveChangesAsync(CancellationToken cancellationToken = default); - + /// - /// Saves all changes made in this context to the database. + /// Saves all changes made in this context to the database. /// /// - /// The number of state entries written to the database. + /// The number of state entries written to the database. /// int SaveChanges(); } \ No newline at end of file diff --git a/src/Nullinside.Api.Model/Shared/UserHelpers.cs b/src/Nullinside.Api.Model/Shared/UserHelpers.cs index e1a9f36..9a6eb70 100644 --- a/src/Nullinside.Api.Model/Shared/UserHelpers.cs +++ b/src/Nullinside.Api.Model/Shared/UserHelpers.cs @@ -1,5 +1,3 @@ -using System.Security.Cryptography; - using Microsoft.EntityFrameworkCore; using Nullinside.Api.Common; diff --git a/src/Nullinside.Api/Controllers/TwitchBotController.cs b/src/Nullinside.Api/Controllers/TwitchBotController.cs index 8219839..38b0a62 100644 --- a/src/Nullinside.Api/Controllers/TwitchBotController.cs +++ b/src/Nullinside.Api/Controllers/TwitchBotController.cs @@ -46,6 +46,7 @@ public TwitchBotController(IConfiguration configuration, INullinsideContext dbCo /// redirects users back to the nullinside website. /// /// The credentials provided by twitch. + /// The twitch api. /// The cancellation token. /// /// A redirect to the nullinside website. @@ -57,10 +58,10 @@ public TwitchBotController(IConfiguration configuration, INullinsideContext dbCo [AllowAnonymous] [HttpGet] [Route("login")] - public async Task TwitchLogin([FromQuery] string code, CancellationToken token) { + public async Task TwitchLogin([FromQuery] string code, [FromServices] ITwitchApiProxy api, + CancellationToken token) { string? siteUrl = _configuration.GetValue("Api:SiteUrl"); - var api = new TwitchApiProxy(); - if (!await api.GetAccessToken(code, token)) { + if (null == await api.CreateAccessToken(code, token)) { return Redirect($"{siteUrl}/twitch-bot/config?error={TwitchBotLoginErrors.TwitchErrorWithToken}"); } @@ -69,13 +70,13 @@ public async Task TwitchLogin([FromQuery] string code, Cancellati return Redirect($"{siteUrl}/twitch-bot/config?error={TwitchBotLoginErrors.TwitchAccountHasNoEmail}"); } - (string? id, string? username) user = await api.GetTwitchUser(token); + (string? id, string? username) user = await api.GetUser(token); if (string.IsNullOrWhiteSpace(user.username) || string.IsNullOrWhiteSpace(user.id)) { return Redirect($"{siteUrl}/twitch-bot/config?error={TwitchBotLoginErrors.InternalError}"); } - string? bearerToken = await UserHelpers.GetTokenAndSaveToDatabase(_dbContext, email, token, api.AccessToken, - api.RefreshToken, api.ExpiresUtc, user.username, user.id); + string? bearerToken = await UserHelpers.GetTokenAndSaveToDatabase(_dbContext, email, token, api.OAuth?.AccessToken, + api.OAuth?.RefreshToken, api.OAuth?.ExpiresUtc, user.username, user.id); if (string.IsNullOrWhiteSpace(bearerToken)) { return Redirect($"{siteUrl}/twitch-bot/config?error={TwitchBotLoginErrors.InternalError}"); } diff --git a/src/Nullinside.Api/Controllers/UserController.cs b/src/Nullinside.Api/Controllers/UserController.cs index b38fcc2..50e2780 100644 --- a/src/Nullinside.Api/Controllers/UserController.cs +++ b/src/Nullinside.Api/Controllers/UserController.cs @@ -83,6 +83,7 @@ public async Task Login([FromForm] GoogleOpenIdToken creds, Cance /// redirects users back to the nullinside website. /// /// The credentials provided by twitch. + /// The twitch api. /// The cancellation token. /// /// A redirect to the nullinside website. @@ -94,10 +95,10 @@ public async Task Login([FromForm] GoogleOpenIdToken creds, Cance [AllowAnonymous] [HttpGet] [Route("twitch-login")] - public async Task TwitchLogin([FromQuery] string code, CancellationToken token) { + public async Task TwitchLogin([FromQuery] string code, [FromServices] ITwitchApiProxy api, + CancellationToken token) { string? siteUrl = _configuration.GetValue("Api:SiteUrl"); - var api = new TwitchApiProxy(); - if (!await api.GetAccessToken(code, token)) { + if (null == await api.CreateAccessToken(code, token)) { return Redirect($"{siteUrl}/user/login?error=3"); } diff --git a/src/Nullinside.Api/Nullinside.Api.csproj b/src/Nullinside.Api/Nullinside.Api.csproj index 5533643..a5a12bb 100644 --- a/src/Nullinside.Api/Nullinside.Api.csproj +++ b/src/Nullinside.Api/Nullinside.Api.csproj @@ -21,16 +21,16 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + +