diff --git a/KfChatDotNetBot/Models/YouTubeApiModels.cs b/KfChatDotNetBot/Models/YouTubeApiModels.cs new file mode 100644 index 0000000..fde5ae3 --- /dev/null +++ b/KfChatDotNetBot/Models/YouTubeApiModels.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; + +namespace KfChatDotNetBot.Models; + + public class YouTubeApiModels + { + public class ContentDetailsRoot + { + [JsonPropertyName("kind")] + public required string Kind { get; set; } + [JsonPropertyName("items")] + public required List Items { get; set; } + } + + public class SnippetModel + { + [JsonPropertyName("publishedAt")] + public required DateTime PublishedAt { get; set; } + [JsonPropertyName("channelId")] + public required string ChannelId { get; set; } + [JsonPropertyName("title")] + public required string Title { get; set; } + [JsonPropertyName("description")] + public required string Description { get; set; } + [JsonPropertyName("channelTitle")] + public required string ChannelTitle { get; set; } + // "none", "live", "upcoming" + [JsonPropertyName("liveBroadcastContent")] + public required string LiveBroadcastContent { get; set; } + } + + public class ItemModel + { + [JsonPropertyName("kind")] + public required string Kind { get; set; } + [JsonPropertyName("id")] + public required string Id { get; set; } + [JsonPropertyName("snippet")] + public required SnippetModel Snippet { get; set; } + } + } \ No newline at end of file diff --git a/KfChatDotNetBot/Services/BotServices.cs b/KfChatDotNetBot/Services/BotServices.cs index 64a5b3c..d473ff4 100644 --- a/KfChatDotNetBot/Services/BotServices.cs +++ b/KfChatDotNetBot/Services/BotServices.cs @@ -39,6 +39,7 @@ public class BotServices private PeerTube? _peerTubeStatusCheck; private Owncast? _owncastStatusCheck; private ShuffleDotUs? _shuffleDotUs; + private YouTubePubSub? _youTubePubSub; public OpenRouter? OpenRouter; private Task? _websocketWatchdog; @@ -130,6 +131,20 @@ public class BotServices _shuffleDotUs.OnLatestBetUpdated += ShuffleOnLatestBetUpdated; await _shuffleDotUs.StartWsClient(); } + + private async Task BuildYouTubePubSub() + { + var settings = await SettingsProvider.GetMultipleValuesAsync([BuiltIn.Keys.YouTubePubSubEnabled]); + if (!settings[BuiltIn.Keys.YouTubePubSubEnabled].ToBoolean()) + { + _logger.Debug("YouTube PubSub is disabled"); + return; + } + _youTubePubSub = new YouTubePubSub(_cancellationToken); + _youTubePubSub.OnNewVideo += YouTubePubSubOnNewVideo; + await _youTubePubSub.Connect(); + _logger.Info("Built YouTube PubSub"); + } private async Task BuildDiscord() { @@ -393,7 +408,8 @@ public class BotServices var settings = await SettingsProvider.GetMultipleValuesAsync([ BuiltIn.Keys.KickEnabled, BuiltIn.Keys.HowlggEnabled, BuiltIn.Keys.ChipsggEnabled, BuiltIn.Keys.ClashggEnabled, BuiltIn.Keys.BetBoltEnabled, BuiltIn.Keys.YeetEnabled, - BuiltIn.Keys.RainbetEnabled, BuiltIn.Keys.PartiEnabled, BuiltIn.Keys.JackpotEnabled + BuiltIn.Keys.RainbetEnabled, BuiltIn.Keys.PartiEnabled, BuiltIn.Keys.JackpotEnabled, + BuiltIn.Keys.YouTubePubSubEnabled ]); try { @@ -508,6 +524,15 @@ public class BotServices _shuffleDotUs = null!; await BuildShuffleDotUs(); } + + if (settings[BuiltIn.Keys.YouTubePubSubEnabled].ToBoolean() && _youTubePubSub != null && + !_youTubePubSub.IsConnected()) + { + _logger.Error("YouTube PubSub died, recreating it"); + _youTubePubSub.Dispose(); + _youTubePubSub = null; + await BuildYouTubePubSub(); + } } catch (Exception e) { @@ -1253,4 +1278,31 @@ public class BotServices }; await SettingsProvider.SetValueAsJsonObjectAsync(BuiltIn.Keys.BossmanLastSighting, sighting); } + + private void YouTubePubSubOnNewVideo(object sender, YouTubePubSubNotificationModel data) + { + var video = YouTubeApi.GetVideoDetails(data.Id).Result; + if (video == null) + { + _logger.Error($"Caught a null when getting video details for {data.Id} which we were just notified about"); + return; + } + + if (video.Snippet.LiveBroadcastContent == "live") + { + _chatBot.SendChatMessageAsync($"@{video.Snippet.ChannelTitle} has gone live! {data.Title} {data.Url}", true).Wait(_cancellationToken); + } + else if (video.Snippet.LiveBroadcastContent == "upcoming") + { + _chatBot.SendChatMessageAsync($"@{video.Snippet.ChannelTitle} has scheduled a live stream! {data.Title} {data.Url}", true).Wait(_cancellationToken); + } + else if (video.Snippet.LiveBroadcastContent == "none") + { + _chatBot.SendChatMessageAsync($"@{video.Snippet.ChannelTitle} has uploaded a new video! {data.Title} {data.Url}", true).Wait(_cancellationToken); + } + else + { + _logger.Error($"YouTube live broadcast content '{video.Snippet.LiveBroadcastContent}' was unhandled for {data.Id}"); + } + } } \ No newline at end of file diff --git a/KfChatDotNetBot/Services/YouTubeApi.cs b/KfChatDotNetBot/Services/YouTubeApi.cs index 8bfd7b5..4a28489 100644 --- a/KfChatDotNetBot/Services/YouTubeApi.cs +++ b/KfChatDotNetBot/Services/YouTubeApi.cs @@ -1,6 +1,42 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using KfChatDotNetBot.Models; +using KfChatDotNetBot.Settings; + namespace KfChatDotNetBot.Services; -public class YouTubeApi +public static class YouTubeApi { - + private static async Task GetHttpClient() + { + var settings = + await SettingsProvider.GetMultipleValuesAsync([BuiltIn.Keys.YouTubeApiKey, BuiltIn.Keys.Proxy]); + if (settings[BuiltIn.Keys.YouTubeApiKey].Value == null) + throw new InvalidOperationException("YouTube API key has not been configured"); + var handler = new HttpClientHandler(); + if (settings[BuiltIn.Keys.Proxy].Value != null) + { + handler.Proxy = new WebProxy(settings[BuiltIn.Keys.Proxy].Value); + handler.UseProxy = true; + } + handler.AutomaticDecompression = DecompressionMethods.All; + var client = new HttpClient(handler); + client.DefaultRequestHeaders.UserAgent.Clear(); + client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("KenoGPT", "1.0")); + client.DefaultRequestHeaders.Add("X-Goog-Api-Key", settings[BuiltIn.Keys.YouTubeApiKey].Value); + client.BaseAddress = new Uri("https://www.googleapis.com/youtube/v3/"); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + return client; + } + + public static async Task GetVideoDetails(string id) + { + var client = await GetHttpClient(); + var response = + await client.GetFromJsonAsync($"videos?part=snippet&id={id}"); + if (response?.Items.Count == 0) return null; + return response?.Items[0]; + } } \ No newline at end of file diff --git a/KfChatDotNetBot/Services/YouTubePubSub.cs b/KfChatDotNetBot/Services/YouTubePubSub.cs index d97fecf..2ae1b97 100644 --- a/KfChatDotNetBot/Services/YouTubePubSub.cs +++ b/KfChatDotNetBot/Services/YouTubePubSub.cs @@ -60,6 +60,7 @@ public class YouTubePubSub : IDisposable _logger.Error(e); _logger.Error("--- Payload ---"); _logger.Error(message.ToString()); + _logger.Error("---------------"); } } @@ -79,14 +80,4 @@ public class YouTubePubSubNotificationModel public required string Title { get; set; } [JsonPropertyName("url")] public required string Url { get; set; } - [JsonPropertyName("channel")] - public required YouTubePubSubNotificationChannelModel Channel { get; set; } -} - -public class YouTubePubSubNotificationChannelModel -{ - [JsonPropertyName("id")] - public required string Id { get; set; } - [JsonPropertyName("name")] - public required string Name { get; set; } } \ No newline at end of file diff --git a/KfChatDotNetBot/Settings/BuiltIn.cs b/KfChatDotNetBot/Settings/BuiltIn.cs index 01e6283..ce79bf1 100644 --- a/KfChatDotNetBot/Settings/BuiltIn.cs +++ b/KfChatDotNetBot/Settings/BuiltIn.cs @@ -453,6 +453,10 @@ public static class BuiltIn public static string KasinoDiceCleanupDelay = "Kasino.Dice.CleanupDelay"; [BuiltInSetting("Delay in milliseconds before cleaning up wheel", SettingValueType.Text, "30000", WholeNumberRegex)] public static string KasinoWheelCleanupDelay = "Kasino.Wheel.CleanupDelay"; + [BuiltInSetting("Whether the YouTube PubSub Redis client is enabled", SettingValueType.Boolean, "true", BooleanRegex)] + public static string YouTubePubSubEnabled = "YouTube.PubSub.Enabled"; + [BuiltInSetting("YouTube API Key", SettingValueType.Text, isSecret: true)] + public static string YouTubeApiKey = "YouTube.ApiKey"; [BuiltInSetting("Openrouter API key for hostess command", SettingValueType.Text, isSecret: true)] public static string OpenrouterApiKey = "Openrouter.ApiKey"; }