Added Nightbot integration. Changed from client credentials flow to implicit code grant flow.

This commit is contained in:
Tom 2024-08-11 21:22:37 +00:00
parent 13bb6a9aa8
commit 0ad063cebd
9 changed files with 260 additions and 23 deletions

View File

@ -0,0 +1,123 @@
using Serilog;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands;
namespace TwitchChatTTS.Chat.Commands
{
public class NightbotCommand : IChatCommand
{
private readonly NightbotApiClient _api;
private readonly ILogger _logger;
public NightbotCommand(NightbotApiClient api, ILogger logger)
{
_api = api;
_logger = logger;
}
public string Name => "nightbot";
public void Build(ICommandBuilder builder)
{
builder.CreateCommandTree(Name, b =>
{
b.CreateStaticInputParameter("play", b =>
{
b.CreateCommand(new NightbotSongQueueCommand(_api, "play", _logger));
})
.CreateStaticInputParameter("pause", b =>
{
b.CreateCommand(new NightbotSongQueueCommand(_api, "pause", _logger));
})
.CreateStaticInputParameter("skip", b =>
{
b.CreateCommand(new NightbotSongQueueCommand(_api, "skip", _logger));
})
.CreateStaticInputParameter("volume", b =>
{
b.CreateUnvalidatedParameter("volume")
.CreateCommand(new NightbotSongQueueCommand(_api, "volume", _logger));
})
.CreateStaticInputParameter("clear_playlist", b =>
{
b.CreateCommand(new NightbotSongQueueCommand(_api, "clear_playlist", _logger));
})
.CreateStaticInputParameter("clear_queue", b =>
{
b.CreateCommand(new NightbotSongQueueCommand(_api, "volume", _logger));
})
.CreateStaticInputParameter("clear", b =>
{
b.CreateStaticInputParameter("playlist", b => b.CreateCommand(new NightbotSongQueueCommand(_api, "clear_playlist", _logger)))
.CreateStaticInputParameter("queue", b => b.CreateCommand(new NightbotSongQueueCommand(_api, "clear_queue", _logger)));
});
});
}
private sealed class NightbotSongQueueCommand : IChatPartialCommand
{
private readonly NightbotApiClient _api;
private readonly string _command;
private readonly ILogger _logger;
public bool AcceptCustomPermission { get => true; }
public NightbotSongQueueCommand(NightbotApiClient api, string command, ILogger logger)
{
_api = api;
_command = command.ToLower();
_logger = logger;
}
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{
try
{
if (_command == "play")
{
await _api.Play();
_logger.Information("Playing Nightbot song queue.");
}
else if (_command == "pause")
{
await _api.Pause();
_logger.Information("Playing Nightbot song queue.");
}
else if (_command == "skip")
{
await _api.Skip();
_logger.Information("Skipping Nightbot song queue.");
}
else if (_command == "volume")
{
int volume = int.Parse(values["volume"]);
await _api.Volume(volume);
_logger.Information($"Changed Nightbot volume to {volume}.");
}
else if (_command == "clear_playlist")
{
await _api.ClearPlaylist();
_logger.Information("Cleared Nightbot playlist.");
}
else if (_command == "clear_queue")
{
await _api.ClearQueue();
_logger.Information("Cleared Nightbot queue.");
}
}
catch (HttpRequestException e)
{
_logger.Warning("Ensure your Nightbot account is linked to your TTS account.");
}
catch (FormatException)
{
}
catch (OverflowException)
{
}
}
}
}
}

View File

@ -1,3 +1,4 @@
using System.Net.Http.Formatting;
using System.Net.Http.Json;
using System.Text.Json;
@ -44,6 +45,11 @@ namespace TwitchChatTTS.Helpers
return await _client.PostAsJsonAsync(uri, new object(), Options);
}
public async Task<HttpResponseMessage> Put<T>(string uri, T data)
{
return await _client.PutAsJsonAsync(uri, data, Options);
}
public async Task<T?> Delete<T>(string uri)
{
return await _client.DeleteFromJsonAsync<T>(uri, Options);

View File

@ -8,12 +8,14 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
public class LoginAckHandler : IWebSocketHandler
{
private readonly User _user;
private readonly NightbotApiClient _nightbot;
private readonly ILogger _logger;
public int OperationCode { get; } = 2;
public LoginAckHandler(User user, ILogger logger)
public LoginAckHandler(User user, NightbotApiClient nightbot, ILogger logger)
{
_user = user;
_nightbot = nightbot;
_logger = logger;
}
@ -24,32 +26,48 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
if (sender is not HermesSocketClient client)
return;
if (message.AnotherClient && client.LoggedIn)
if (message.AnotherClient)
{
_logger.Warning("Another client has connected to the same account.");
if (client.LoggedIn)
_logger.Warning($"Another client has connected to the same account via {(message.WebLogin ? "web login" : "application")}.");
return;
}
if (client.LoggedIn)
{
_logger.Warning("Attempted to log in again while still logged in.");
_logger.Error("Attempted to log in again while still logged in.");
return;
}
_user.HermesUserId = message.UserId;
_user.OwnerId = message.OwnerId;
_user.DefaultTTSVoice = message.DefaultTTSVoice;
_user.VoicesAvailable = message.TTSVoicesAvailable;
_user.RegexFilters = message.WordFilters.ToArray();
_user.VoicesEnabled = new HashSet<string>(message.EnabledTTSVoices);
_user.TwitchConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "twitch");
_user.NightbotConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "nightbot");
client.LoggedIn = true;
_logger.Information($"Logged in as {_user.TwitchUsername} {(message.WebLogin ? "via web" : "via TTS app")}.");
await client.FetchTTSVoices();
await client.FetchEnabledTTSVoices();
await client.FetchTTSWordFilters();
await client.FetchTTSChatterVoices();
await client.FetchDefaultTTSVoice();
await client.FetchChatterIdentifiers();
await client.FetchEmotes();
await client.FetchRedemptions();
await client.FetchPermissions();
if (_user.NightbotConnection != null) {
_nightbot.Initialize(_user.NightbotConnection.ClientId, _user.NightbotConnection.AccessToken);
var span = DateTime.Now - _user.NightbotConnection.ExpiresAt;
var timeLeft = span.TotalDays >= 2 ? Math.Floor(span.TotalDays) + " days" : (span.TotalHours >= 2 ? Math.Floor(span.TotalHours) + " hours" : Math.Floor(span.TotalMinutes) + " minutes");
if (span.TotalDays >= 3)
_logger.Information($"Nightbot connection has {timeLeft} before it is revoked.");
else if (span.TotalMinutes >= 0)
_logger.Warning($"Nightbot connection has {timeLeft} before it is revoked. Refreshing the token is soon required.");
else
_logger.Error("Nightbot connection has its permissions revoked. Refresh the token. Anything related to Nightbot from this application will not work.");
}
_logger.Information("TTS is now ready.");
client.Ready = true;
}

View File

@ -0,0 +1,60 @@
using System.Text.Json;
using TwitchChatTTS.Helpers;
using Serilog;
using TwitchChatTTS;
public class NightbotApiClient
{
private readonly ILogger _logger;
private readonly WebClientWrap _web;
public NightbotApiClient(
ILogger logger
)
{
_logger = logger;
_web = new WebClientWrap(new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
});
}
public async Task Play()
{
await _web.Post("https://api.nightbot.tv/1/song_requests/queue/play");
}
public async Task Pause()
{
await _web.Post("https://api.nightbot.tv/1/song_requests/queue/pause");
}
public async Task Skip()
{
await _web.Post("https://api.nightbot.tv/1/song_requests/queue/skip");
}
public async Task Volume(int volume)
{
await _web.Put("https://api.nightbot.tv/1/song_requests", new Dictionary<string, object>() { { "volume", volume } });
}
public async Task ClearPlaylist()
{
await _web.Delete("https://api.nightbot.tv/1/song_requests/playlist");
}
public async Task ClearQueue()
{
await _web.Delete("https://api.nightbot.tv/1/song_requests/queue");
}
public void Initialize(string clientId, string accessToken)
{
_web.AddHeader("Authorization", "Bearer " + accessToken);
_web.AddHeader("Client-Id", clientId);
}
}

View File

@ -69,6 +69,7 @@ s.AddSingleton(new JsonSerializerOptions()
s.AddSingleton<IChatCommand, SkipCommand>();
s.AddSingleton<IChatCommand, VoiceCommand>();
s.AddSingleton<IChatCommand, RefreshCommand>();
s.AddSingleton<IChatCommand, NightbotCommand>();
s.AddSingleton<IChatCommand, OBSCommand>();
s.AddSingleton<IChatCommand, TTSCommand>();
s.AddSingleton<IChatCommand, VersionCommand>();
@ -107,6 +108,9 @@ s.AddKeyedSingleton<IWebSocketHandler, EndOfStreamHandler>("7tv");
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, SevenMessageTypeManager>("7tv");
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv");
// Nightbot
s.AddSingleton<NightbotApiClient>();
// twitch websocket
s.AddKeyedSingleton<IBackoff>("twitch", new ExponentialBackoff(1000, 120 * 1000));
s.AddSingleton<ITwitchConnectionManager, TwitchConnectionManager>();

View File

@ -17,6 +17,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
private readonly User _user;
private readonly OBSSocketClient _obs;
private readonly HermesSocketClient _hermes;
private readonly NightbotApiClient _nightbot;
private readonly AudioPlaybackEngine _playback;
private readonly ILogger _logger;
private readonly Random _random;
@ -27,6 +28,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
User user,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
NightbotApiClient nightbot,
AudioPlaybackEngine playback,
ILogger logger)
{
@ -34,6 +36,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
_user = user;
_obs = (obs as OBSSocketClient)!;
_hermes = (hermes as HermesSocketClient)!;
_nightbot = nightbot;
_playback = playback;
_logger = logger;
_random = new Random();
@ -191,6 +194,21 @@ namespace TwitchChatTTS.Twitch.Redemptions
_playback.PlaySound(action.Data["file_path"]);
_logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
case "NIGHTBOT_PLAY":
await _nightbot.Play();
break;
case "NIGHTBOT_PAUSE":
await _nightbot.Pause();
break;
case "NIGHTBOT_SKIP":
await _nightbot.Skip();
break;
case "NIGHTBOT_CLEAR_PLAYLIST":
await _nightbot.ClearPlaylist();
break;
case "NIGHTBOT_CLEAR_QUEUE":
await _nightbot.ClearQueue();
break;
default:
_logger.Warning($"Unknown redeemable action has occured. Update needed? [type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;

View File

@ -7,14 +7,12 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public string Name => "session_welcome";
private readonly HermesApiClient _hermes;
private readonly TwitchApiClient _api;
private readonly User _user;
private readonly ILogger _logger;
public SessionWelcomeHandler(HermesApiClient hermes, TwitchApiClient api, User user, ILogger logger)
public SessionWelcomeHandler(TwitchApiClient api, User user, ILogger logger)
{
_hermes = hermes;
_api = api;
_user = user;
_logger = logger;
@ -32,18 +30,24 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
}
int waited = 0;
while (_user.TwitchUserId <= 0 && ++waited < 3)
while ((_user.TwitchUserId <= 0 || _user.TwitchConnection == null) && ++waited < 5)
await Task.Delay(TimeSpan.FromSeconds(1));
try
if (_user.TwitchConnection == null)
{
await _hermes.AuthorizeTwitch();
var token = await _hermes.FetchTwitchBotToken();
_api.Initialize(token);
_logger.Error("Ensure you have linked either your Twitch account or TTS' bot to your TTS account. Twitch client will not be connecting.");
return;
}
catch (Exception)
{
_logger.Error("Ensure you have your Twitch account linked on TTS. Restart application once you do.");
_api.Initialize(_user.TwitchConnection.ClientId, _user.TwitchConnection.AccessToken);
var span = DateTime.Now - _user.TwitchConnection.ExpiresAt;
var timeLeft = span.TotalDays >= 2 ? Math.Floor(span.TotalDays) + " days" : (span.TotalHours >= 2 ? Math.Floor(span.TotalHours) + " hours" : Math.Floor(span.TotalMinutes) + " minutes");
if (span.TotalDays >= 3)
_logger.Information($"Twitch connection has {timeLeft} before it is revoked.");
else if (span.TotalMinutes >= 0)
_logger.Warning($"Twitch connection has {timeLeft} before it is revoked. Refreshing the token is soon required.");
else {
_logger.Error("Twitch connection has its permissions revoked. Refresh the token. Twith client will not be connecting.");
return;
}
@ -88,7 +92,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
await Subscribe(sender, subscription, message.Session.Id, broadcasterId, "1");
foreach (var subscription in subscriptionsv2)
await Subscribe(sender, subscription, message.Session.Id, broadcasterId, "2");
await Subscribe(sender, "channel.raid", broadcasterId, async () => await _api.CreateChannelRaidEventSubscription("1", message.Session.Id, to: broadcasterId));
sender.Identify(message.Session.Id);

View File

@ -95,9 +95,9 @@ public class TwitchApiClient
return await _web.GetJson<EventResponse<NotificationInfo>>("https://api.twitch.tv/helix/eventsub/subscriptions" + query);
}
public void Initialize(TwitchBotToken token)
public void Initialize(string clientId, string accessToken)
{
_web.AddHeader("Authorization", "Bearer " + token.AccessToken);
_web.AddHeader("Client-Id", token.ClientId);
_web.AddHeader("Authorization", "Bearer " + accessToken);
_web.AddHeader("Client-Id", clientId);
}
}

View File

@ -1,6 +1,7 @@
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using HermesSocketLibrary.Requests.Messages;
using HermesSocketLibrary.Socket.Data;
using TwitchChatTTS.Twitch.Socket.Handlers;
namespace TwitchChatTTS
@ -15,6 +16,9 @@ namespace TwitchChatTTS
public string SevenEmoteSetId { get; set; }
public long? OwnerId { get; set; }
public Connection? TwitchConnection { get; set; }
public Connection? NightbotConnection { get; set; }
public string DefaultTTSVoice { get; set; }
// voice id -> voice name
public IDictionary<string, string> VoicesAvailable { get => _voicesAvailable; set { _voicesAvailable = value; VoiceNameRegex = GenerateEnabledVoicesRegex(); } }