diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index a9ae90b..1726a99 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Application/Features/ProcessMessageCommand.cs b/src/Application/Features/ProcessMessageCommand.cs index 1a2f107..f27f133 100644 --- a/src/Application/Features/ProcessMessageCommand.cs +++ b/src/Application/Features/ProcessMessageCommand.cs @@ -1,13 +1,13 @@ using System.Text.Json; -using Application.Infrastructure.Pubg; -using TwitchLib.Client.Models; + namespace Application.Features; -public record ProcessMessageCommand(ChatMessage ChatMessage) : IRequest; +public record ProcessMessageCommand(ChatMessage ChatMessage, string ThreadId) : IRequest; public class ProcessMessageCommandHandler( IChatService chatService, IAIClient aiClient, + IAssistantClient assistantClient, IPubgAIClient aiPubgAIClient, IModerationClient moderationClient, IMediator mediator, @@ -19,6 +19,7 @@ ILoggerFactory loggerFactory { private readonly IChatService chatService = chatService; private readonly IAIClient aiClient = aiClient; + private readonly IAssistantClient assistantClient = assistantClient; private readonly IPubgAIClient aiPubgAIClient = aiPubgAIClient; private readonly IModerationClient moderationClient = moderationClient; private readonly IMediator mediator = mediator; @@ -84,6 +85,8 @@ public async Task Handle(ProcessMessageCommand request, CancellationToken cancel logger.LogTrace("Messages: {Messages}", string.Join(", ", messages)); logger.LogTrace("Summary: {Summary}", messages.GetExtractiveSummary(request.ChatMessage.Channel)); + await assistantClient.AddMessage(request.ThreadId, request.ChatMessage.Username, string.Empty, request.ChatMessage.Message); + // Ignore messages from some users (like yourself) if ( request.ChatMessage.Username.Equals(options.Username, StringComparison.CurrentCultureIgnoreCase) @@ -96,10 +99,10 @@ public async Task Handle(ProcessMessageCommand request, CancellationToken cancel var randomResponseChance = random.NextDouble(); logger.LogDebug("Random value: {RandomValue}", randomResponseChance); - if (request.ChatMessage.Message.Contains(options.Username, StringComparison.CurrentCultureIgnoreCase)) + if (request.ChatMessage.Message.Contains(options.Username, StringComparison.CurrentCultureIgnoreCase) || randomResponseChance > RandomResponseChance) { // If the message contains the bot's name, use only that message as a prompt - string completion = await aiClient.GetCompletion(request.ChatMessage.Message); + string completion = await assistantClient.RunAndWait(request.ThreadId); logger.LogDebug("Completion ({Channel}): {Completion}", request.ChatMessage.Channel, completion); @@ -116,21 +119,6 @@ public async Task Handle(ProcessMessageCommand request, CancellationToken cancel } } } - else if (randomResponseChance > RandomResponseChance) - { - // If this is a random response, use some of the chat history to generate a response - // This also empties the history queue - - // Get the messages into a list - var historyMessages = new List(); - while (messages.TryDequeue(out var message)) - { - historyMessages.Add(message.Message); - } - string completion = await aiClient.GetAwareCompletion(historyMessages); - logger.LogInformation("History aware completion ({Channel}): {Completion}", request.ChatMessage.Channel, completion); - await chatService.SendMessage(request.ChatMessage.Channel, completion, cancellationToken); - } } } @@ -196,3 +184,5 @@ public string GetExtractiveSummary(string channel, int numSentences = 3) } public record HistoryMessage(string Channel, string Username, string Message, DateTime Timestamp); + +#pragma warning restore OPENAI001 diff --git a/src/Application/GlobalUsings.cs b/src/Application/GlobalUsings.cs index 936ffa3..1d62526 100644 --- a/src/Application/GlobalUsings.cs +++ b/src/Application/GlobalUsings.cs @@ -10,6 +10,7 @@ global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Options; global using OpenAI; +global using OpenAI.Assistants; global using OpenAI.Audio; global using System.Text.RegularExpressions; global using TwitchLib.Client.Models; diff --git a/src/Application/Infrastructure/DependencyInjection.cs b/src/Application/Infrastructure/DependencyInjection.cs index cc95fe4..c9aa374 100644 --- a/src/Application/Infrastructure/DependencyInjection.cs +++ b/src/Application/Infrastructure/DependencyInjection.cs @@ -29,6 +29,7 @@ private static IServiceCollection AddOpenAI(this IServiceCollection services, IC services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/Application/Infrastructure/OpenAI/AIClient.cs b/src/Application/Infrastructure/OpenAI/AIClient.cs index d4d9acf..99e38ac 100644 --- a/src/Application/Infrastructure/OpenAI/AIClient.cs +++ b/src/Application/Infrastructure/OpenAI/AIClient.cs @@ -48,4 +48,5 @@ public class OpenAIClientOptions : IConfigurationOptions public Guid SoundOutDeviceGuid { get; set; } = Guid.Empty; public string AudioOutputPath { get; set; } = string.Empty; public string GeneralSystemPrompt { get; set; } = string.Empty; + public string Assistant { get; set; } = string.Empty; } \ No newline at end of file diff --git a/src/Application/Infrastructure/OpenAI/AssisstantClient.cs b/src/Application/Infrastructure/OpenAI/AssisstantClient.cs new file mode 100644 index 0000000..c2df397 --- /dev/null +++ b/src/Application/Infrastructure/OpenAI/AssisstantClient.cs @@ -0,0 +1,61 @@ +namespace Application.Infrastructure.OpenAI; + +#pragma warning disable OPENAI001 + +public interface IAssistantClient +{ + Task NewThread(ThreadCreationOptions threadCreationOptions); + Task AddMessage(string threadId, string nick, string role, string message); + Task RunAndWait(string threadId); +} + +public class AssistantClient(IOptionsMonitor optionsMonitor, ILogger logger) : IAssistantClient +{ + readonly OpenAIClient openAIClient = new(optionsMonitor.CurrentValue.ApiKey); + private readonly ILogger logger = logger; + + public async Task NewThread(ThreadCreationOptions threadCreationOptions) + { + var thread = await openAIClient.GetAssistantClient().CreateThreadAsync(threadCreationOptions); + return thread.Value.Id; + } + + public async Task AddMessage(string threadId, string nick, string role, string message) + { + var assistantClient = openAIClient.GetAssistantClient(); + var messageCreationOptions = new MessageCreationOptions(); + messageCreationOptions.Metadata.Add("nick", nick); + messageCreationOptions.Metadata.Add("role", role); + + var chatMessage = $"{nick} ({role}): {message}"; + + await assistantClient.CreateMessageAsync(threadId, MessageRole.User, [MessageContent.FromText(chatMessage)], messageCreationOptions); + } + + public async Task RunAndWait(string threadId) + { + var assistantClient = openAIClient.GetAssistantClient(); + var createRunClientResult = await assistantClient.CreateRunAsync(threadId, optionsMonitor.CurrentValue.Assistant); + var threadRun = createRunClientResult.Value; + + do + { + Thread.Sleep(100); + threadRun = await assistantClient.GetRunAsync(threadRun.ThreadId, threadRun.Id); + } while (!threadRun.Status.IsTerminal); + + var messages = assistantClient.GetMessages(threadRun.ThreadId, new MessageCollectionOptions() { Order = MessageCollectionOrder.Descending }); + + foreach (var message in messages) + { + foreach (var contentItem in message.Content) + { + logger.LogInformation("{Role}: {Message}", message.Role.ToString().ToUpper(), contentItem.Text); + } + } + + return messages.First().Content.First().Text; + } +} + +#pragma warning restore OPENAI001 diff --git a/src/Application/Infrastructure/Twitch/ChatService.cs b/src/Application/Infrastructure/Twitch/ChatService.cs index 584d3eb..feee79a 100644 --- a/src/Application/Infrastructure/Twitch/ChatService.cs +++ b/src/Application/Infrastructure/Twitch/ChatService.cs @@ -4,6 +4,8 @@ namespace Application.Infrastructure.Twitch; +#pragma warning disable OPENAI001 + public interface IChatService { public Task StartAsync(string accessToken, CancellationToken cancellationToken); @@ -13,12 +15,15 @@ public interface IChatService public IReadOnlyList JoinedChannels { get; } } -public partial class ChatService(ILoggerFactory loggerFactory, ILogger logger, ChatOptions options, IMediator mediator) : IChatService +public partial class ChatService(ILoggerFactory loggerFactory, ILogger logger, ChatOptions options, IMediator mediator, IAssistantClient assistantClient) : IChatService { readonly TwitchClient client = new(loggerFactory: loggerFactory); private readonly ILogger logger = logger; private readonly ChatOptions options = options; private readonly IMediator mediator = mediator; + private readonly IAssistantClient assistantClient = assistantClient; + private string threadId = string.Empty; + private Task Client_OnConnected(object? sender, OnConnectedEventArgs e) { @@ -40,7 +45,7 @@ private async Task Client_OnWhisperReceived(object? sender, OnWhisperReceivedArg private async Task Client_OnMessageReceived(object? sender, OnMessageReceivedArgs e) { - var processMessageCommand = new ProcessMessageCommand(e.ChatMessage); + var processMessageCommand = new ProcessMessageCommand(e.ChatMessage, threadId); await mediator.Send(processMessageCommand); } @@ -52,6 +57,8 @@ private Task Client_OnJoinedChannel(object? sender, OnJoinedChannelArgs e) public async Task StartAsync(string accessToken, CancellationToken cancellationToken) { + threadId = await assistantClient.NewThread(new ThreadCreationOptions()); + ConnectionCredentials credentials = new(options.Username, accessToken); client.Initialize(credentials, options.Channel); @@ -91,3 +98,5 @@ public async Task JoinChannel(string channel, CancellationToken cancellationToke public IReadOnlyList JoinedChannels => client.JoinedChannels; } + +#pragma warning restore OPENAI001 \ No newline at end of file diff --git a/src/Application/Infrastructure/Twitch/WebSocketService.cs b/src/Application/Infrastructure/Twitch/WebSocketService.cs index f4dbdfd..4218436 100644 --- a/src/Application/Infrastructure/Twitch/WebSocketService.cs +++ b/src/Application/Infrastructure/Twitch/WebSocketService.cs @@ -1,5 +1,6 @@ using TwitchLib.Api; using TwitchLib.Api.Core.Enums; +using TwitchLib.EventSub.Core.SubscriptionTypes.Channel; using TwitchLib.EventSub.Websockets.Core.EventArgs.Stream; namespace Application.Infrastructure.Twitch; @@ -17,6 +18,7 @@ public class WebsocketService : IWebsocketService private readonly TwitchAPI twitchApi = new(); private string userId = string.Empty; private string broadcasterId = string.Empty; + private string botUserId = string.Empty; public WebsocketService( ILogger logger, @@ -37,14 +39,295 @@ IMediator mediator this.eventSubWebsocketClient.StreamOnline += OnStreamOnline; this.eventSubWebsocketClient.StreamOffline += OnStreamOffline; - this.eventSubWebsocketClient.ChannelFollow += OnChannelFollow; - this.eventSubWebsocketClient.ChannelVipAdd += OnChannelVipAdd; - this.eventSubWebsocketClient.ChannelVipRemove += OnChannelVipRemove; this.eventSubWebsocketClient.ChannelAdBreakBegin += OnChannelAdBreakBegin; + this.eventSubWebsocketClient.ChannelBan += OnChannelBan; + this.eventSubWebsocketClient.ChannelCharityCampaignDonate += OnChannelCharityCampaignDonate; + this.eventSubWebsocketClient.ChannelCharityCampaignProgress += OnChannelCharityCampaignProgress; + this.eventSubWebsocketClient.ChannelCharityCampaignStart += OnChannelCharityCampaignStart; + this.eventSubWebsocketClient.ChannelChatMessage += OnChannelChatMessage; + this.eventSubWebsocketClient.ChannelCheer += OnChannelCheer; + this.eventSubWebsocketClient.ChannelFollow += OnChannelFollow; + this.eventSubWebsocketClient.ChannelGoalBegin += OnChannelGoalBegin; + this.eventSubWebsocketClient.ChannelGoalEnd += OnChannelGoalEnd; + this.eventSubWebsocketClient.ChannelGoalProgress += OnChannelGoalProgress; + this.eventSubWebsocketClient.ChannelGuestStarGuestUpdate += OnChannelGuestStarGuestUpdate; + this.eventSubWebsocketClient.ChannelGuestStarSessionBegin += OnChannelGuestStarSessionBegin; + this.eventSubWebsocketClient.ChannelGuestStarSessionEnd += OnChannelGuestStarSessionEnd; + this.eventSubWebsocketClient.ChannelGuestStarSettingsUpdate += OnChannelGuestStarSettingsUpdate; + this.eventSubWebsocketClient.ChannelGuestStarSlotUpdate += OnChannelGuestStarSlotUpdate; + this.eventSubWebsocketClient.ChannelHypeTrainBegin += OnChannelHypeTrainBegin; + this.eventSubWebsocketClient.ChannelHypeTrainEnd += OnChannelHypeTrainEnd; + this.eventSubWebsocketClient.ChannelModeratorAdd += OnChannelModeratorAdd; + this.eventSubWebsocketClient.ChannelModeratorRemove += OnChannelModeratorRemove; + this.eventSubWebsocketClient.ChannelPointsAutomaticRewardRedemptionAdd += OnChannelPointsAutomaticRewardRedemption; + this.eventSubWebsocketClient.ChannelPointsCustomRewardAdd += OnChannelPointsCustomRewardAdd; + this.eventSubWebsocketClient.ChannelPointsCustomRewardRedemptionAdd += OnChannelPointsCustomRewardRedemption; + this.eventSubWebsocketClient.ChannelPointsCustomRewardRedemptionUpdate += OnChannelPointsCustomRewardRedemptionUpdate; + this.eventSubWebsocketClient.ChannelPointsCustomRewardRemove += OnChannelPointsCustomRewardRemove; + this.eventSubWebsocketClient.ChannelPointsCustomRewardUpdate += OnChannelPointsCustomRewardUpdate; + this.eventSubWebsocketClient.ChannelPollBegin += OnChannelPollBegin; + this.eventSubWebsocketClient.ChannelPollEnd += OnChannelPollEnd; + this.eventSubWebsocketClient.ChannelPollProgress += OnChannelPollProgress; + this.eventSubWebsocketClient.ChannelPredictionBegin += OnChannelPredictionBegin; + this.eventSubWebsocketClient.ChannelPredictionEnd += OnChannelPredictionEnd; + this.eventSubWebsocketClient.ChannelPredictionLock += OnChannelPredictionLock; + this.eventSubWebsocketClient.ChannelPredictionProgress += OnChannelPredictionProgress; + this.eventSubWebsocketClient.ChannelRaid += OnChannelRaid; + this.eventSubWebsocketClient.ChannelShieldModeBegin += OnChannelShieldModeBegin; + this.eventSubWebsocketClient.ChannelShieldModeEnd += OnChannelShieldModeEnd; + this.eventSubWebsocketClient.ChannelShoutoutCreate += OnChannelShoutoutCreate; + this.eventSubWebsocketClient.ChannelShoutoutReceive += OnChannelShoutoutReceive; this.eventSubWebsocketClient.ChannelSubscribe += OnChannelSubscribe; + this.eventSubWebsocketClient.ChannelSubscriptionEnd += OnChannelSubscriptionEnd; + this.eventSubWebsocketClient.ChannelSubscriptionGift += OnChannelSubscriptionGift; this.eventSubWebsocketClient.ChannelSubscriptionMessage += OnChannelSubscriptionMessage; + this.eventSubWebsocketClient.ChannelSuspiciousUserMessage += OnChannelSuspiciousUserMessage; + this.eventSubWebsocketClient.ChannelSuspiciousUserUpdate += OnChannelSuspiciousUserUpdate; + this.eventSubWebsocketClient.ChannelUnban += OnChannelUnban; + this.eventSubWebsocketClient.ChannelUpdate += OnChannelUpdate; + this.eventSubWebsocketClient.ChannelVipAdd += OnChannelVipAdd; + this.eventSubWebsocketClient.ChannelVipRemove += OnChannelVipRemove; + this.eventSubWebsocketClient.ChannelWarningAcknowledge += OnChannelWarningAcknowledge; + this.eventSubWebsocketClient.ChannelWarningSend += OnChannelWarningSend; + } + + private async Task OnChannelAdBreakBegin(object sender, ChannelAdBreakBeginArgs args) + { + var adDurationSeconds = args.Notification.Payload.Event.DurationSeconds; + var broadcasterUserId = args.Notification.Payload.Event.BroadcasterUserLogin; + logger.LogInformation("WebSocket ChannelAdBreakBegin: {DurationSeconds} seconds", adDurationSeconds); + + var processInstructionCommand = new ProcessInstructionCommand( + $@"Tell the chat an AD started, it will be over in {adDurationSeconds} seconds. + Give chat a suggestion what to do in the meantime. + If nothing else you can invite them to join our Discord server with this link {options.DiscordJoinLink} + Remind the chat that they can use their Prime Sub to sub to the channel to avoid ads.", + broadcasterUserId + ); + + await mediator.Send(processInstructionCommand); } + private Task OnChannelBan(object sender, ChannelBanArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelCharityCampaignDonate(object sender, ChannelCharityCampaignDonateArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelCharityCampaignProgress(object sender, ChannelCharityCampaignProgressArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelCharityCampaignStart(object sender, ChannelCharityCampaignStartArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelCheer(object sender, ChannelCheerArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private async Task OnChannelFollow(object? sender, ChannelFollowArgs e) + { + var newFollower = e.Notification.Payload.Event.UserName; + var broadcasterUserId = e.Notification.Payload.Event.BroadcasterUserLogin; + + logger.LogInformation("{UserName} followed {BroadcasterUserName} at {FollowedAt}", newFollower, e.Notification.Payload.Event.BroadcasterUserName, e.Notification.Payload.Event.FollowedAt); + + var processInstructionCommand = new ProcessInstructionCommand($"Welcome {newFollower} as a new follower! Post some hype in chat for the new follower!", broadcasterUserId); + await mediator.Send(processInstructionCommand); + } + + private Task OnChannelGoalBegin(object sender, ChannelGoalBeginArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelGoalEnd(object sender, ChannelGoalEndArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelGoalProgress(object sender, ChannelGoalProgressArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelGuestStarGuestUpdate(object sender, ChannelGuestStarGuestUpdateArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelGuestStarSessionBegin(object sender, ChannelGuestStarSessionBegin args) + { + logger.LogDebug("OnChannelUpdate: {@ChannelGuestStarSessionBegin}", args); + return Task.CompletedTask; + } + + private Task OnChannelGuestStarSessionEnd(object sender, ChannelGuestStarSessionEnd args) + { + logger.LogDebug("OnChannelUpdate: {@ChannelGuestStarSessionEnd}", args); + return Task.CompletedTask; + } + + private Task OnChannelGuestStarSettingsUpdate(object sender, ChannelGuestStarSettingsUpdateArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelGuestStarSlotUpdate(object sender, ChannelGuestStarSlotUpdateArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelHypeTrainBegin(object sender, ChannelHypeTrainBeginArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelHypeTrainEnd(object sender, ChannelHypeTrainEndArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelModeratorAdd(object sender, ChannelModeratorArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelModeratorRemove(object sender, ChannelModeratorArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelPointsAutomaticRewardRedemption(object sender, ChannelPointsAutomaticRewardRedemptionArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelPointsCustomRewardAdd(object sender, ChannelPointsCustomRewardArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelPointsCustomRewardRedemption(object sender, ChannelPointsCustomRewardRedemptionArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelPointsCustomRewardRedemptionUpdate(object sender, ChannelPointsCustomRewardRedemptionArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelPointsCustomRewardRemove(object sender, ChannelPointsCustomRewardArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelPointsCustomRewardUpdate(object sender, ChannelPointsCustomRewardArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelPollBegin(object sender, ChannelPollBeginArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelPollEnd(object sender, ChannelPollEndArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelPollProgress(object sender, ChannelPollProgressArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelPredictionBegin(object sender, ChannelPredictionBeginArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelPredictionEnd(object sender, ChannelPredictionEndArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelPredictionLock(object sender, ChannelPredictionLockArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelPredictionProgress(object sender, ChannelPredictionProgressArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelRaid(object sender, ChannelRaidArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelShieldModeBegin(object sender, ChannelShieldModeBeginArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelShieldModeEnd(object sender, ChannelShieldModeEndArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelShoutoutCreate(object sender, ChannelShoutoutCreateArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelShoutoutReceive(object sender, ChannelShoutoutReceiveArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } // The channel.subscribe subscription type sends a notification when a specified channel receives a subscriber. This does not include resubscribes. private async Task OnChannelSubscribe(object sender, ChannelSubscribeArgs args) @@ -64,6 +347,18 @@ private async Task OnChannelSubscribe(object sender, ChannelSubscribeArgs args) await mediator.Send(processInstructionCommand); } + private Task OnChannelSubscriptionEnd(object sender, ChannelSubscriptionEndArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + + private Task OnChannelSubscriptionGift(object sender, ChannelSubscriptionGiftArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } + private async Task OnChannelSubscriptionMessage(object sender, ChannelSubscriptionMessageArgs args) { var userName = args.Notification.Payload.Event.BroadcasterUserName; @@ -81,33 +376,28 @@ private async Task OnChannelSubscriptionMessage(object sender, ChannelSubscripti await mediator.Send(processInstructionCommand); } - private Task OnStreamOnline(object sender, StreamOnlineArgs args) + private Task OnChannelSuspiciousUserMessage(object sender, ChannelSuspiciousUserMessageArgs args) { - var userId = args.Notification.Payload.Event.BroadcasterUserId; - var userName = args.Notification.Payload.Event.BroadcasterUserName; - var userLogin = args.Notification.Payload.Event.BroadcasterUserLogin; - logger.LogInformation("Websocket OnStreamOnline: {BroadcasterUserName} is now online! {BroadcasterUserLogin}/{BroadcasterUserId}", userName, userLogin, userId); + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); return Task.CompletedTask; } - private Task OnStreamOffline(object sender, StreamOfflineArgs args) + private Task OnChannelSuspiciousUserUpdate(object sender, ChannelSuspiciousUserUpdateArgs args) { - var userId = args.Notification.Payload.Event.BroadcasterUserId; - var userName = args.Notification.Payload.Event.BroadcasterUserName; - var userLogin = args.Notification.Payload.Event.BroadcasterUserLogin; - logger.LogInformation("Websocket OnStreamOffline: {BroadcasterUserName} is now offline! {BroadcasterUserLogin}/{BroadcasterUserId}", userName, userLogin, userId); + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); return Task.CompletedTask; } - private async Task OnChannelFollow(object? sender, ChannelFollowArgs e) + private Task OnChannelUnban(object sender, ChannelUnbanArgs args) { - var newFollower = e.Notification.Payload.Event.UserName; - var broadcasterUserId = e.Notification.Payload.Event.BroadcasterUserLogin; - - logger.LogInformation("{UserName} followed {BroadcasterUserName} at {FollowedAt}", newFollower, e.Notification.Payload.Event.BroadcasterUserName, e.Notification.Payload.Event.FollowedAt); + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; + } - var processInstructionCommand = new ProcessInstructionCommand($"Welcome {newFollower} as a new follower! Post some hype in chat for the new follower!", broadcasterUserId); - await mediator.Send(processInstructionCommand); + private Task OnChannelUpdate(object sender, ChannelUpdateArgs args) + { + logger.LogDebug("OnChannelUpdate: {@Notification}", args.Notification); + return Task.CompletedTask; } private async Task OnChannelVipAdd(object sender, ChannelVipArgs args) @@ -121,27 +411,46 @@ private async Task OnChannelVipAdd(object sender, ChannelVipArgs args) await mediator.Send(processInstructionCommand); } - private Task OnChannelVipRemove(object sender, ChannelVipArgs args) + private Task OnChannelWarningAcknowledge(object sender, ChannelWarningAcknowledgeArgs args) { - logger.LogInformation("WebSocket ChannelVipRemove"); + logger.LogDebug("OnChannelWarningAcknowledge: {@Notification}", args.Notification); return Task.CompletedTask; } - private async Task OnChannelAdBreakBegin(object sender, ChannelAdBreakBeginArgs args) + private Task OnChannelWarningSend(object sender, ChannelWarningSendArgs args) { - var adDurationSeconds = args.Notification.Payload.Event.DurationSeconds; - var broadcasterUserId = args.Notification.Payload.Event.BroadcasterUserLogin; - logger.LogInformation("WebSocket ChannelAdBreakBegin: {DurationSeconds} seconds", adDurationSeconds); + logger.LogDebug("OnChannelWarningSend: {@Notification}", args.Notification); + return Task.CompletedTask; + } - var processInstructionCommand = new ProcessInstructionCommand( - $@"Tell the chat an AD started, it will be over in {adDurationSeconds} seconds. - Give chat a suggestion what to do in the meantime. - If nothing else you can invite them to join our Discord server with this link {options.DiscordJoinLink} - Remind the chat that they can use their Prime Sub to sub to the channel to avoid ads.", - broadcasterUserId - ); + private Task OnChannelChatMessage(object sender, ChannelChatMessageArgs args) + { + logger.LogDebug("OnChannelChatMessage: {@Notification}", args.Notification); + return Task.CompletedTask; + } - await mediator.Send(processInstructionCommand); + private Task OnStreamOnline(object sender, StreamOnlineArgs args) + { + var userId = args.Notification.Payload.Event.BroadcasterUserId; + var userName = args.Notification.Payload.Event.BroadcasterUserName; + var userLogin = args.Notification.Payload.Event.BroadcasterUserLogin; + logger.LogInformation("Websocket OnStreamOnline: {BroadcasterUserName} is now online! {BroadcasterUserLogin}/{BroadcasterUserId}", userName, userLogin, userId); + return Task.CompletedTask; + } + + private Task OnStreamOffline(object sender, StreamOfflineArgs args) + { + var userId = args.Notification.Payload.Event.BroadcasterUserId; + var userName = args.Notification.Payload.Event.BroadcasterUserName; + var userLogin = args.Notification.Payload.Event.BroadcasterUserLogin; + logger.LogInformation("Websocket OnStreamOffline: {BroadcasterUserName} is now offline! {BroadcasterUserLogin}/{BroadcasterUserId}", userName, userLogin, userId); + return Task.CompletedTask; + } + + private Task OnChannelVipRemove(object sender, ChannelVipArgs args) + { + logger.LogInformation("WebSocket ChannelVipRemove"); + return Task.CompletedTask; } public async Task StartAsync(string accessToken, CancellationToken cancellationToken = default) @@ -156,6 +465,9 @@ public async Task StartAsync(string accessToken, CancellationToken cancellationT twitchApi.Settings.ClientId = options.ClientId; twitchApi.Settings.AccessToken = accessToken; + var usersResponse = await twitchApi.Helix.Users.GetUsersAsync(ids: null, logins: ["andibanterbot"], accessToken); + botUserId = usersResponse.Users.First().Id; + await eventSubWebsocketClient.ConnectAsync(); } @@ -170,143 +482,33 @@ private async Task OnWebsocketConnected(object? sender, WebsocketConnectedArgs e if (!e.IsRequestedReconnect) { - /* - Subscribe to topics via the TwitchApi.Helix.EventSub object, this example shows how to subscribe - to the channel follow event used in the example above. - - var conditions = new Dictionary() - { - { "broadcaster_user_id", someUserId } - }; - var subscriptionResponse = await TwitchApi.Helix.EventSub.CreateEventSubSubscriptionAsync("channel.follow", "2", conditions, - EventSubTransportMethod.Websocket, _eventSubWebsocketClient.SessionId); - - You can find more examples on the subscription types and their requirements here https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types/ - Prerequisite: Twitchlib.Api nuget package installed (included in the Twitchlib package automatically) - */ - - // channel.follow - try - { - var conditions = new Dictionary() - { - { "broadcaster_user_id", "165699060" }, - { "moderator_user_id", "165699060" } - }; - - var subscriptionResponse = await twitchApi.Helix.EventSub.CreateEventSubSubscriptionAsync( - "channel.follow", - "2", - conditions, - EventSubTransportMethod.Websocket, - websocketSessionId: eventSubWebsocketClient.SessionId - ); - } - catch (Exception ex) + var conditions = new Dictionary() { - logger.LogError(ex, "channel.follow"); - } + { "broadcaster_user_id", broadcasterId }, + }; - // channel.vip.add/remove - try - { - var conditions = new Dictionary() + await SubscribeToEvent("channel.follow", "2", new Dictionary() { - { "broadcaster_user_id", "165699060" } - }; - await twitchApi.Helix.EventSub.CreateEventSubSubscriptionAsync( - "channel.vip.add", - "1", - conditions, - EventSubTransportMethod.Websocket, - websocketSessionId: eventSubWebsocketClient.SessionId - ); - await twitchApi.Helix.EventSub.CreateEventSubSubscriptionAsync( - "channel.vip.remove", - "1", - conditions, - EventSubTransportMethod.Websocket, - eventSubWebsocketClient.SessionId - ); - } - catch (Exception ex) - { - logger.LogError(ex, "channel.vip.add/remove"); - } + { "broadcaster_user_id", broadcasterId }, + { "moderator_user_id", broadcasterId } + }); - // channel.ad_break.begin - try - { - var conditions = new Dictionary() - { - { "broadcaster_user_id", "165699060" } - }; - await twitchApi.Helix.EventSub.CreateEventSubSubscriptionAsync( - "channel.ad_break.begin", - "1", - conditions, - EventSubTransportMethod.Websocket, - websocketSessionId: eventSubWebsocketClient.SessionId - ); - } - catch (System.Exception ex) - { - logger.LogError(ex, "channel.ad_break.begin"); - } + await SubscribeToEvent("channel.vip.add", "1", conditions); + await SubscribeToEvent("channel.vip.remove", "1", conditions); - // stream.online/offline - try - { - var conditions = new Dictionary() - { - { "broadcaster_user_id", "165699060" } - }; - await twitchApi.Helix.EventSub.CreateEventSubSubscriptionAsync( - "stream.online", - "1", - conditions, - EventSubTransportMethod.Websocket, - websocketSessionId: eventSubWebsocketClient.SessionId - ); - await twitchApi.Helix.EventSub.CreateEventSubSubscriptionAsync( - "stream.offline", - "1", - conditions, - EventSubTransportMethod.Websocket, - websocketSessionId: eventSubWebsocketClient.SessionId - ); - } - catch (System.Exception ex) - { - logger.LogError(ex, "stream.online/offline"); - } + await SubscribeToEvent("channel.ad_break.begin", "1", conditions); - // channel.subscribe / channel.subscription.message - try - { - var conditions = new Dictionary() + await SubscribeToEvent("stream.online", "1", conditions); + await SubscribeToEvent("stream.offline", "1", conditions); + + await SubscribeToEvent("channel.subscribe", "1", conditions); + await SubscribeToEvent("channel.subscription.message", "1", conditions); + + await SubscribeToEvent("channel.chat.message", "1", new Dictionary() { - { "broadcaster_user_id", "165699060" } - }; - await twitchApi.Helix.EventSub.CreateEventSubSubscriptionAsync( - "channel.subscribe", - "1", - conditions, - EventSubTransportMethod.Websocket, - websocketSessionId: eventSubWebsocketClient.SessionId - ); - await twitchApi.Helix.EventSub.CreateEventSubSubscriptionAsync( - "channel.subscription.message", - "1", - conditions, - EventSubTransportMethod.Websocket, - websocketSessionId: eventSubWebsocketClient.SessionId - ); - } - catch (System.Exception ex) - { - logger.LogError(ex, "channel.subscribe/subscription.message"); - } + { "broadcaster_user_id", broadcasterId }, + { "user_id", broadcasterId } + }); } } @@ -333,4 +535,22 @@ private Task OnErrorOccurred(object? sender, ErrorOccuredArgs e) logger.LogError(e.Exception, "Websocket {SessionId} - Error occurred!", eventSubWebsocketClient.SessionId); return Task.CompletedTask; } + + private async Task SubscribeToEvent(string eventType, string version, Dictionary conditions) + { + try + { + await twitchApi.Helix.EventSub.CreateEventSubSubscriptionAsync( + eventType, + version, + conditions, + EventSubTransportMethod.Websocket, + websocketSessionId: eventSubWebsocketClient.SessionId + ); + } + catch (Exception ex) + { + logger.LogError(ex, "{EventType} subscription failed", eventType); + } + } } diff --git a/src/Host/Host.csproj b/src/Host/Host.csproj index 6d4afe5..e527a88 100644 --- a/src/Host/Host.csproj +++ b/src/Host/Host.csproj @@ -6,7 +6,7 @@ - +