From a8de49cee3de35f4c61fe1095e46977434a144cf Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 25 May 2025 18:38:52 -0500 Subject: [PATCH 01/31] refactor: reorganize solution for better multi-project support/management --- src/Directory.Build.props | 1 - src/StevesBot.sln | 90 +++++++++++++------ src/{StevesBot.Worker => src}/.editorconfig | 0 src/src/Directory.Build.props | 11 +++ src/src/Directory.Packages.props | 19 ++++ src/src/StevesBot.Webhook/Program.cs | 14 +++ .../Properties/launchSettings.json | 23 +++++ .../StevesBot.Webhook.csproj | 13 +++ .../StevesBot.Worker/Discord/Extensions.cs | 0 .../Discord/Gateway/DiscordCloseCodes.cs | 0 .../Discord/Gateway/DiscordGatewayClient.cs | 0 .../Gateway/DiscordGatewayClientException.cs | 0 .../Discord/Gateway/DiscordIntents.cs | 0 .../Discord/Gateway/Events/Data/Activity.cs | 0 .../Gateway/Events/Data/ActivityType.cs | 0 .../Discord/Gateway/Events/Data/HelloData.cs | 0 .../Gateway/Events/Data/IdentifyData.cs | 0 .../Gateway/Events/Data/IdentifyProperties.cs | 0 .../Gateway/Events/Data/PresenceStatus.cs | 0 .../Discord/Gateway/Events/Data/ReadyData.cs | 0 .../Discord/Gateway/Events/Data/ResumeData.cs | 0 .../Gateway/Events/Data/UpdatePresenceData.cs | 0 .../Discord/Gateway/Events/DiscordEvent.cs | 0 .../Gateway/Events/DiscordEventConverter.cs | 0 .../Gateway/Events/DiscordEventTypes.cs | 0 .../Gateway/Events/DiscordMessageTypes.cs | 0 .../Discord/Gateway/Events/DiscordOpCodes.cs | 0 .../Gateway/Events/DispatchDiscordEvent.cs | 0 .../Events/HeartbeatAckDiscordEvent.cs | 0 .../Gateway/Events/HeartbeatDiscordEvent.cs | 0 .../Gateway/Events/HelloDiscordEvent.cs | 0 .../Gateway/Events/IdentifyDiscordEvent.cs | 0 .../Events/InvalidSessionDiscordEvent.cs | 0 .../Events/MessageCreateDiscordEvent.cs | 0 .../Gateway/Events/ReadyDiscordEvent.cs | 0 .../Gateway/Events/ReconnectDiscordEvent.cs | 0 .../Gateway/Events/ResumeDiscordEvent.cs | 0 .../Events/UpdatePresenceDiscordEvent.cs | 0 .../Discord/Gateway/IDiscordGatewayClient.cs | 0 .../Discord/Rest/DiscordRestClient.cs | 0 .../Rest/DiscordRestClientException.cs | 0 .../Discord/Rest/IDiscordRestClient.cs | 0 .../Rest/Requests/CreateMessageRequest.cs | 0 .../Discord/Rest/Responses/GatewayResponse.cs | 0 .../Discord/Shared/DiscordClientOptions.cs | 0 .../Discord/Shared/DiscordMessage.cs | 0 .../Discord/Shared/DiscordMessageReference.cs | 0 .../Shared/DiscordMessageReferenceTypes.cs | 0 .../Discord/Shared/DiscordUser.cs | 0 .../Handlers/WelcomeMessageHandler.cs | 0 src/{ => src}/StevesBot.Worker/Program.cs | 0 .../Properties/launchSettings.json | 0 .../StevesBot.Worker/StevesBot.Worker.csproj | 16 ++-- .../StevesBot.Worker/Telemetry/Extensions.cs | 0 .../Telemetry/Instrumentation.cs | 0 .../StevesBot.Worker/Telemetry/SeqOptions.cs | 0 .../StevesBot.Worker/Threading/AsyncLock.cs | 0 .../StevesBot.Worker/Threading/IAsyncLock.cs | 0 .../Threading/LockReleaser.cs | 0 src/{ => src}/StevesBot.Worker/Usings.cs | 0 .../StevesBot.Worker/WebSockets/IWebSocket.cs | 0 .../WebSockets/IWebSocketFactory.cs | 0 .../StevesBot.Worker/WebSockets/WebSocket.cs | 0 .../WebSockets/WebSocketFactory.cs | 0 src/{ => src}/StevesBot.Worker/Worker.cs | 0 .../StevesBot.Worker/appsettings.Example.json | 0 .../.editorconfig | 0 src/tests/Directory.Build.props | 15 ++++ src/tests/Directory.packages.props | 19 ++++ .../StevesBot.Webhook.Tests.csproj} | 39 ++++---- .../StevesBot.Webhook.Tests/UnitTest1.cs | 10 +++ .../Infrastructure/TestWebSocketServer.cs | 0 .../Integration/WebSocketTests.cs | 0 .../StevesBot.Worker.Tests.csproj | 39 ++++++++ .../Unit/ActivityTests.cs | 0 .../Unit/ActivityTypeTests.cs | 0 .../Unit/AsyncLockTests.cs | 0 .../Unit/CreateMessageRequestTests.cs | 0 .../Unit/DiscordClientOptionsTests.cs | 0 .../Unit/DiscordCloseCodesTests.cs | 0 .../Unit/DiscordEventConverterTests.cs | 0 .../Unit/DiscordEventTests.cs | 0 .../Unit/DiscordEventTypesTests.cs | 0 .../DiscordGatewayClientExceptionTests.cs | 0 .../Unit/DiscordGatewayClientTests.cs | 0 .../Unit/DiscordIntentsTests.cs | 0 .../Unit/DiscordMessageReferenceTests.cs | 0 .../Unit/DiscordMessageReferenceTypesTests.cs | 0 .../Unit/DiscordOpCodesTests.cs | 0 .../Unit/DiscordRestClientExceptionTests.cs | 0 .../Unit/DiscordRestClientTests.cs | 0 .../Unit/ExtensionsTests.cs | 0 .../Unit/HeartbeatAckDiscordEventTests.cs | 0 .../Unit/HeartbeatDiscordEventTests.cs | 0 .../Unit/HelloDiscordEventTests.cs | 0 .../Unit/IdentifyDataTests.cs | 0 .../Unit/IdentifyDiscordEventTests.cs | 0 .../Unit/InvalidSessionDiscordEventTests.cs | 0 .../Unit/LockReleaserTests.cs | 0 .../Unit/MessageCreateDiscordEventTests.cs | 0 .../Unit/PresenceStatusTests.cs | 0 .../Unit/ReadyDiscordEventTests.cs | 0 .../Unit/ReconnectDiscordEventTests.cs | 0 .../Unit/ResumeDataTests.cs | 0 .../Unit/ResumeDiscordEventTests.cs | 0 .../Unit/UpdatePresenceDataTests.cs | 0 .../Unit/UpdatePresenceDiscordEventTests.cs | 0 .../Unit/WebSocketFactoryTests.cs | 0 .../Unit/WebSocketTests.cs | 0 .../Unit/WelcomeMessageHandlerTests.cs | 0 .../StevesBot.Worker.Tests/Usings.cs | 0 111 files changed, 250 insertions(+), 59 deletions(-) rename src/{StevesBot.Worker => src}/.editorconfig (100%) create mode 100644 src/src/Directory.Build.props create mode 100644 src/src/Directory.Packages.props create mode 100644 src/src/StevesBot.Webhook/Program.cs create mode 100644 src/src/StevesBot.Webhook/Properties/launchSettings.json create mode 100644 src/src/StevesBot.Webhook/StevesBot.Webhook.csproj rename src/{ => src}/StevesBot.Worker/Discord/Extensions.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/DiscordCloseCodes.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/DiscordGatewayClient.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/DiscordGatewayClientException.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/DiscordIntents.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/Data/Activity.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/Data/ActivityType.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/Data/HelloData.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/Data/IdentifyData.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/Data/IdentifyProperties.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/Data/PresenceStatus.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/Data/ReadyData.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/Data/ResumeData.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/Data/UpdatePresenceData.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/DiscordEvent.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/DiscordEventConverter.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/DiscordEventTypes.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/DiscordMessageTypes.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/DiscordOpCodes.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/DispatchDiscordEvent.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/HeartbeatAckDiscordEvent.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/HeartbeatDiscordEvent.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/HelloDiscordEvent.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/IdentifyDiscordEvent.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/InvalidSessionDiscordEvent.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/MessageCreateDiscordEvent.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/ReadyDiscordEvent.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/ReconnectDiscordEvent.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/ResumeDiscordEvent.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/Events/UpdatePresenceDiscordEvent.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Gateway/IDiscordGatewayClient.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Rest/DiscordRestClientException.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Rest/Responses/GatewayResponse.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Shared/DiscordClientOptions.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Shared/DiscordMessage.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Shared/DiscordMessageReferenceTypes.cs (100%) rename src/{ => src}/StevesBot.Worker/Discord/Shared/DiscordUser.cs (100%) rename src/{ => src}/StevesBot.Worker/Handlers/WelcomeMessageHandler.cs (100%) rename src/{ => src}/StevesBot.Worker/Program.cs (100%) rename src/{ => src}/StevesBot.Worker/Properties/launchSettings.json (100%) rename src/{ => src}/StevesBot.Worker/StevesBot.Worker.csproj (64%) rename src/{ => src}/StevesBot.Worker/Telemetry/Extensions.cs (100%) rename src/{ => src}/StevesBot.Worker/Telemetry/Instrumentation.cs (100%) rename src/{ => src}/StevesBot.Worker/Telemetry/SeqOptions.cs (100%) rename src/{ => src}/StevesBot.Worker/Threading/AsyncLock.cs (100%) rename src/{ => src}/StevesBot.Worker/Threading/IAsyncLock.cs (100%) rename src/{ => src}/StevesBot.Worker/Threading/LockReleaser.cs (100%) rename src/{ => src}/StevesBot.Worker/Usings.cs (100%) rename src/{ => src}/StevesBot.Worker/WebSockets/IWebSocket.cs (100%) rename src/{ => src}/StevesBot.Worker/WebSockets/IWebSocketFactory.cs (100%) rename src/{ => src}/StevesBot.Worker/WebSockets/WebSocket.cs (100%) rename src/{ => src}/StevesBot.Worker/WebSockets/WebSocketFactory.cs (100%) rename src/{ => src}/StevesBot.Worker/Worker.cs (100%) rename src/{ => src}/StevesBot.Worker/appsettings.Example.json (100%) rename src/{StevesBot.Worker.Tests => tests}/.editorconfig (100%) create mode 100644 src/tests/Directory.Build.props create mode 100644 src/tests/Directory.packages.props rename src/{StevesBot.Worker.Tests/StevesBot.Worker.Tests.csproj => tests/StevesBot.Webhook.Tests/StevesBot.Webhook.Tests.csproj} (52%) create mode 100644 src/tests/StevesBot.Webhook.Tests/UnitTest1.cs rename src/{ => tests}/StevesBot.Worker.Tests/Integration/Infrastructure/TestWebSocketServer.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Integration/WebSocketTests.cs (100%) create mode 100644 src/tests/StevesBot.Worker.Tests/StevesBot.Worker.Tests.csproj rename src/{ => tests}/StevesBot.Worker.Tests/Unit/ActivityTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/ActivityTypeTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/AsyncLockTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/CreateMessageRequestTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/DiscordClientOptionsTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/DiscordCloseCodesTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/DiscordEventConverterTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/DiscordEventTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/DiscordEventTypesTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/DiscordGatewayClientExceptionTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/DiscordIntentsTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTypesTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/DiscordOpCodesTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/DiscordRestClientExceptionTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/DiscordRestClientTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/ExtensionsTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/HeartbeatAckDiscordEventTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/HeartbeatDiscordEventTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/HelloDiscordEventTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/IdentifyDataTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/IdentifyDiscordEventTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/InvalidSessionDiscordEventTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/LockReleaserTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/MessageCreateDiscordEventTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/PresenceStatusTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/ReadyDiscordEventTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/ReconnectDiscordEventTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/ResumeDataTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/ResumeDiscordEventTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/UpdatePresenceDataTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/UpdatePresenceDiscordEventTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/WebSocketFactoryTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/WebSocketTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Unit/WelcomeMessageHandlerTests.cs (100%) rename src/{ => tests}/StevesBot.Worker.Tests/Usings.cs (100%) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c78f721..807df2c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,6 @@ - $(MSBuildThisFileDirectory) net9.0 enable enable diff --git a/src/StevesBot.sln b/src/StevesBot.sln index c4dc140..927b98f 100644 --- a/src/StevesBot.sln +++ b/src/StevesBot.sln @@ -3,9 +3,17 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StevesBot.Worker", "StevesBot.Worker\StevesBot.Worker.csproj", "{C45EE62D-8222-46D7-B4FF-22B155B17561}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StevesBot.Worker.Tests", "StevesBot.Worker.Tests\StevesBot.Worker.Tests.csproj", "{FFB90DB8-1B9A-47E3-8252-746F1445CA02}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StevesBot.Worker", "src\StevesBot.Worker\StevesBot.Worker.csproj", "{51B85887-7083-47A0-B83F-4E1561B14AA9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StevesBot.Webhook", "src\StevesBot.Webhook\StevesBot.Webhook.csproj", "{7C243CA8-55A0-4D73-9FF6-F5E801B7146D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StevesBot.Worker.Tests", "tests\StevesBot.Worker.Tests\StevesBot.Worker.Tests.csproj", "{388EAD53-CCA4-4532-8DEC-5E9177776117}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StevesBot.Webhook.Tests", "tests\StevesBot.Webhook.Tests\StevesBot.Webhook.Tests.csproj", "{3875E45D-8400-46D9-89CE-485BADED0736}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -17,32 +25,62 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C45EE62D-8222-46D7-B4FF-22B155B17561}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C45EE62D-8222-46D7-B4FF-22B155B17561}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C45EE62D-8222-46D7-B4FF-22B155B17561}.Debug|x64.ActiveCfg = Debug|Any CPU - {C45EE62D-8222-46D7-B4FF-22B155B17561}.Debug|x64.Build.0 = Debug|Any CPU - {C45EE62D-8222-46D7-B4FF-22B155B17561}.Debug|x86.ActiveCfg = Debug|Any CPU - {C45EE62D-8222-46D7-B4FF-22B155B17561}.Debug|x86.Build.0 = Debug|Any CPU - {C45EE62D-8222-46D7-B4FF-22B155B17561}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C45EE62D-8222-46D7-B4FF-22B155B17561}.Release|Any CPU.Build.0 = Release|Any CPU - {C45EE62D-8222-46D7-B4FF-22B155B17561}.Release|x64.ActiveCfg = Release|Any CPU - {C45EE62D-8222-46D7-B4FF-22B155B17561}.Release|x64.Build.0 = Release|Any CPU - {C45EE62D-8222-46D7-B4FF-22B155B17561}.Release|x86.ActiveCfg = Release|Any CPU - {C45EE62D-8222-46D7-B4FF-22B155B17561}.Release|x86.Build.0 = Release|Any CPU - {FFB90DB8-1B9A-47E3-8252-746F1445CA02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FFB90DB8-1B9A-47E3-8252-746F1445CA02}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FFB90DB8-1B9A-47E3-8252-746F1445CA02}.Debug|x64.ActiveCfg = Debug|Any CPU - {FFB90DB8-1B9A-47E3-8252-746F1445CA02}.Debug|x64.Build.0 = Debug|Any CPU - {FFB90DB8-1B9A-47E3-8252-746F1445CA02}.Debug|x86.ActiveCfg = Debug|Any CPU - {FFB90DB8-1B9A-47E3-8252-746F1445CA02}.Debug|x86.Build.0 = Debug|Any CPU - {FFB90DB8-1B9A-47E3-8252-746F1445CA02}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FFB90DB8-1B9A-47E3-8252-746F1445CA02}.Release|Any CPU.Build.0 = Release|Any CPU - {FFB90DB8-1B9A-47E3-8252-746F1445CA02}.Release|x64.ActiveCfg = Release|Any CPU - {FFB90DB8-1B9A-47E3-8252-746F1445CA02}.Release|x64.Build.0 = Release|Any CPU - {FFB90DB8-1B9A-47E3-8252-746F1445CA02}.Release|x86.ActiveCfg = Release|Any CPU - {FFB90DB8-1B9A-47E3-8252-746F1445CA02}.Release|x86.Build.0 = Release|Any CPU + {51B85887-7083-47A0-B83F-4E1561B14AA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51B85887-7083-47A0-B83F-4E1561B14AA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51B85887-7083-47A0-B83F-4E1561B14AA9}.Debug|x64.ActiveCfg = Debug|Any CPU + {51B85887-7083-47A0-B83F-4E1561B14AA9}.Debug|x64.Build.0 = Debug|Any CPU + {51B85887-7083-47A0-B83F-4E1561B14AA9}.Debug|x86.ActiveCfg = Debug|Any CPU + {51B85887-7083-47A0-B83F-4E1561B14AA9}.Debug|x86.Build.0 = Debug|Any CPU + {51B85887-7083-47A0-B83F-4E1561B14AA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51B85887-7083-47A0-B83F-4E1561B14AA9}.Release|Any CPU.Build.0 = Release|Any CPU + {51B85887-7083-47A0-B83F-4E1561B14AA9}.Release|x64.ActiveCfg = Release|Any CPU + {51B85887-7083-47A0-B83F-4E1561B14AA9}.Release|x64.Build.0 = Release|Any CPU + {51B85887-7083-47A0-B83F-4E1561B14AA9}.Release|x86.ActiveCfg = Release|Any CPU + {51B85887-7083-47A0-B83F-4E1561B14AA9}.Release|x86.Build.0 = Release|Any CPU + {7C243CA8-55A0-4D73-9FF6-F5E801B7146D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C243CA8-55A0-4D73-9FF6-F5E801B7146D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C243CA8-55A0-4D73-9FF6-F5E801B7146D}.Debug|x64.ActiveCfg = Debug|Any CPU + {7C243CA8-55A0-4D73-9FF6-F5E801B7146D}.Debug|x64.Build.0 = Debug|Any CPU + {7C243CA8-55A0-4D73-9FF6-F5E801B7146D}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C243CA8-55A0-4D73-9FF6-F5E801B7146D}.Debug|x86.Build.0 = Debug|Any CPU + {7C243CA8-55A0-4D73-9FF6-F5E801B7146D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C243CA8-55A0-4D73-9FF6-F5E801B7146D}.Release|Any CPU.Build.0 = Release|Any CPU + {7C243CA8-55A0-4D73-9FF6-F5E801B7146D}.Release|x64.ActiveCfg = Release|Any CPU + {7C243CA8-55A0-4D73-9FF6-F5E801B7146D}.Release|x64.Build.0 = Release|Any CPU + {7C243CA8-55A0-4D73-9FF6-F5E801B7146D}.Release|x86.ActiveCfg = Release|Any CPU + {7C243CA8-55A0-4D73-9FF6-F5E801B7146D}.Release|x86.Build.0 = Release|Any CPU + {388EAD53-CCA4-4532-8DEC-5E9177776117}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {388EAD53-CCA4-4532-8DEC-5E9177776117}.Debug|Any CPU.Build.0 = Debug|Any CPU + {388EAD53-CCA4-4532-8DEC-5E9177776117}.Debug|x64.ActiveCfg = Debug|Any CPU + {388EAD53-CCA4-4532-8DEC-5E9177776117}.Debug|x64.Build.0 = Debug|Any CPU + {388EAD53-CCA4-4532-8DEC-5E9177776117}.Debug|x86.ActiveCfg = Debug|Any CPU + {388EAD53-CCA4-4532-8DEC-5E9177776117}.Debug|x86.Build.0 = Debug|Any CPU + {388EAD53-CCA4-4532-8DEC-5E9177776117}.Release|Any CPU.ActiveCfg = Release|Any CPU + {388EAD53-CCA4-4532-8DEC-5E9177776117}.Release|Any CPU.Build.0 = Release|Any CPU + {388EAD53-CCA4-4532-8DEC-5E9177776117}.Release|x64.ActiveCfg = Release|Any CPU + {388EAD53-CCA4-4532-8DEC-5E9177776117}.Release|x64.Build.0 = Release|Any CPU + {388EAD53-CCA4-4532-8DEC-5E9177776117}.Release|x86.ActiveCfg = Release|Any CPU + {388EAD53-CCA4-4532-8DEC-5E9177776117}.Release|x86.Build.0 = Release|Any CPU + {3875E45D-8400-46D9-89CE-485BADED0736}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3875E45D-8400-46D9-89CE-485BADED0736}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3875E45D-8400-46D9-89CE-485BADED0736}.Debug|x64.ActiveCfg = Debug|Any CPU + {3875E45D-8400-46D9-89CE-485BADED0736}.Debug|x64.Build.0 = Debug|Any CPU + {3875E45D-8400-46D9-89CE-485BADED0736}.Debug|x86.ActiveCfg = Debug|Any CPU + {3875E45D-8400-46D9-89CE-485BADED0736}.Debug|x86.Build.0 = Debug|Any CPU + {3875E45D-8400-46D9-89CE-485BADED0736}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3875E45D-8400-46D9-89CE-485BADED0736}.Release|Any CPU.Build.0 = Release|Any CPU + {3875E45D-8400-46D9-89CE-485BADED0736}.Release|x64.ActiveCfg = Release|Any CPU + {3875E45D-8400-46D9-89CE-485BADED0736}.Release|x64.Build.0 = Release|Any CPU + {3875E45D-8400-46D9-89CE-485BADED0736}.Release|x86.ActiveCfg = Release|Any CPU + {3875E45D-8400-46D9-89CE-485BADED0736}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {51B85887-7083-47A0-B83F-4E1561B14AA9} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {7C243CA8-55A0-4D73-9FF6-F5E801B7146D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {388EAD53-CCA4-4532-8DEC-5E9177776117} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {3875E45D-8400-46D9-89CE-485BADED0736} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + EndGlobalSection EndGlobal diff --git a/src/StevesBot.Worker/.editorconfig b/src/src/.editorconfig similarity index 100% rename from src/StevesBot.Worker/.editorconfig rename to src/src/.editorconfig diff --git a/src/src/Directory.Build.props b/src/src/Directory.Build.props new file mode 100644 index 0000000..634b868 --- /dev/null +++ b/src/src/Directory.Build.props @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/src/Directory.Packages.props b/src/src/Directory.Packages.props new file mode 100644 index 0000000..e98ac39 --- /dev/null +++ b/src/src/Directory.Packages.props @@ -0,0 +1,19 @@ + + + + true + + + + + + + + + + + + + + + diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs new file mode 100644 index 0000000..b5041fa --- /dev/null +++ b/src/src/StevesBot.Webhook/Program.cs @@ -0,0 +1,14 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.Run(); diff --git a/src/src/StevesBot.Webhook/Properties/launchSettings.json b/src/src/StevesBot.Webhook/Properties/launchSettings.json new file mode 100644 index 0000000..cddbc7b --- /dev/null +++ b/src/src/StevesBot.Webhook/Properties/launchSettings.json @@ -0,0 +1,23 @@ +ο»Ώ{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7231;http://localhost:5228", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5228", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + } +} diff --git a/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj b/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj new file mode 100644 index 0000000..250aa1e --- /dev/null +++ b/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/src/StevesBot.Worker/Discord/Extensions.cs b/src/src/StevesBot.Worker/Discord/Extensions.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Extensions.cs rename to src/src/StevesBot.Worker/Discord/Extensions.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/DiscordCloseCodes.cs b/src/src/StevesBot.Worker/Discord/Gateway/DiscordCloseCodes.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/DiscordCloseCodes.cs rename to src/src/StevesBot.Worker/Discord/Gateway/DiscordCloseCodes.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClient.cs b/src/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClient.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClient.cs rename to src/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClient.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClientException.cs b/src/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClientException.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClientException.cs rename to src/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClientException.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/DiscordIntents.cs b/src/src/StevesBot.Worker/Discord/Gateway/DiscordIntents.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/DiscordIntents.cs rename to src/src/StevesBot.Worker/Discord/Gateway/DiscordIntents.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/Data/Activity.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/Data/Activity.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/Data/Activity.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/Data/Activity.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/Data/ActivityType.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/Data/ActivityType.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/Data/ActivityType.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/Data/ActivityType.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/Data/HelloData.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/Data/HelloData.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/Data/HelloData.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/Data/HelloData.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/Data/IdentifyData.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/Data/IdentifyData.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/Data/IdentifyData.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/Data/IdentifyData.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/Data/IdentifyProperties.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/Data/IdentifyProperties.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/Data/IdentifyProperties.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/Data/IdentifyProperties.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/Data/PresenceStatus.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/Data/PresenceStatus.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/Data/PresenceStatus.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/Data/PresenceStatus.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/Data/ReadyData.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/Data/ReadyData.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/Data/ReadyData.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/Data/ReadyData.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/Data/ResumeData.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/Data/ResumeData.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/Data/ResumeData.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/Data/ResumeData.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/Data/UpdatePresenceData.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/Data/UpdatePresenceData.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/Data/UpdatePresenceData.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/Data/UpdatePresenceData.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEvent.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEvent.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/DiscordEvent.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEvent.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventConverter.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventConverter.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventConverter.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventConverter.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventTypes.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventTypes.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventTypes.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventTypes.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/DiscordMessageTypes.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/DiscordMessageTypes.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/DiscordMessageTypes.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/DiscordMessageTypes.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/DiscordOpCodes.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/DiscordOpCodes.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/DiscordOpCodes.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/DiscordOpCodes.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/DispatchDiscordEvent.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/DispatchDiscordEvent.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/DispatchDiscordEvent.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/DispatchDiscordEvent.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatAckDiscordEvent.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatAckDiscordEvent.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatAckDiscordEvent.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatAckDiscordEvent.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatDiscordEvent.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatDiscordEvent.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatDiscordEvent.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatDiscordEvent.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/HelloDiscordEvent.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/HelloDiscordEvent.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/HelloDiscordEvent.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/HelloDiscordEvent.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/IdentifyDiscordEvent.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/IdentifyDiscordEvent.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/IdentifyDiscordEvent.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/IdentifyDiscordEvent.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/InvalidSessionDiscordEvent.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/InvalidSessionDiscordEvent.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/InvalidSessionDiscordEvent.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/InvalidSessionDiscordEvent.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/MessageCreateDiscordEvent.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/MessageCreateDiscordEvent.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/MessageCreateDiscordEvent.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/MessageCreateDiscordEvent.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/ReadyDiscordEvent.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/ReadyDiscordEvent.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/ReadyDiscordEvent.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/ReadyDiscordEvent.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/ReconnectDiscordEvent.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/ReconnectDiscordEvent.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/ReconnectDiscordEvent.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/ReconnectDiscordEvent.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/ResumeDiscordEvent.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/ResumeDiscordEvent.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/ResumeDiscordEvent.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/ResumeDiscordEvent.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/UpdatePresenceDiscordEvent.cs b/src/src/StevesBot.Worker/Discord/Gateway/Events/UpdatePresenceDiscordEvent.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/Events/UpdatePresenceDiscordEvent.cs rename to src/src/StevesBot.Worker/Discord/Gateway/Events/UpdatePresenceDiscordEvent.cs diff --git a/src/StevesBot.Worker/Discord/Gateway/IDiscordGatewayClient.cs b/src/src/StevesBot.Worker/Discord/Gateway/IDiscordGatewayClient.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Gateway/IDiscordGatewayClient.cs rename to src/src/StevesBot.Worker/Discord/Gateway/IDiscordGatewayClient.cs diff --git a/src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs b/src/src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs rename to src/src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs diff --git a/src/StevesBot.Worker/Discord/Rest/DiscordRestClientException.cs b/src/src/StevesBot.Worker/Discord/Rest/DiscordRestClientException.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Rest/DiscordRestClientException.cs rename to src/src/StevesBot.Worker/Discord/Rest/DiscordRestClientException.cs diff --git a/src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs b/src/src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs rename to src/src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs diff --git a/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs b/src/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs rename to src/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs diff --git a/src/StevesBot.Worker/Discord/Rest/Responses/GatewayResponse.cs b/src/src/StevesBot.Worker/Discord/Rest/Responses/GatewayResponse.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Rest/Responses/GatewayResponse.cs rename to src/src/StevesBot.Worker/Discord/Rest/Responses/GatewayResponse.cs diff --git a/src/StevesBot.Worker/Discord/Shared/DiscordClientOptions.cs b/src/src/StevesBot.Worker/Discord/Shared/DiscordClientOptions.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Shared/DiscordClientOptions.cs rename to src/src/StevesBot.Worker/Discord/Shared/DiscordClientOptions.cs diff --git a/src/StevesBot.Worker/Discord/Shared/DiscordMessage.cs b/src/src/StevesBot.Worker/Discord/Shared/DiscordMessage.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Shared/DiscordMessage.cs rename to src/src/StevesBot.Worker/Discord/Shared/DiscordMessage.cs diff --git a/src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs b/src/src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs rename to src/src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs diff --git a/src/StevesBot.Worker/Discord/Shared/DiscordMessageReferenceTypes.cs b/src/src/StevesBot.Worker/Discord/Shared/DiscordMessageReferenceTypes.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Shared/DiscordMessageReferenceTypes.cs rename to src/src/StevesBot.Worker/Discord/Shared/DiscordMessageReferenceTypes.cs diff --git a/src/StevesBot.Worker/Discord/Shared/DiscordUser.cs b/src/src/StevesBot.Worker/Discord/Shared/DiscordUser.cs similarity index 100% rename from src/StevesBot.Worker/Discord/Shared/DiscordUser.cs rename to src/src/StevesBot.Worker/Discord/Shared/DiscordUser.cs diff --git a/src/StevesBot.Worker/Handlers/WelcomeMessageHandler.cs b/src/src/StevesBot.Worker/Handlers/WelcomeMessageHandler.cs similarity index 100% rename from src/StevesBot.Worker/Handlers/WelcomeMessageHandler.cs rename to src/src/StevesBot.Worker/Handlers/WelcomeMessageHandler.cs diff --git a/src/StevesBot.Worker/Program.cs b/src/src/StevesBot.Worker/Program.cs similarity index 100% rename from src/StevesBot.Worker/Program.cs rename to src/src/StevesBot.Worker/Program.cs diff --git a/src/StevesBot.Worker/Properties/launchSettings.json b/src/src/StevesBot.Worker/Properties/launchSettings.json similarity index 100% rename from src/StevesBot.Worker/Properties/launchSettings.json rename to src/src/StevesBot.Worker/Properties/launchSettings.json diff --git a/src/StevesBot.Worker/StevesBot.Worker.csproj b/src/src/StevesBot.Worker/StevesBot.Worker.csproj similarity index 64% rename from src/StevesBot.Worker/StevesBot.Worker.csproj rename to src/src/StevesBot.Worker/StevesBot.Worker.csproj index 581c7c2..f66917c 100644 --- a/src/StevesBot.Worker/StevesBot.Worker.csproj +++ b/src/src/StevesBot.Worker/StevesBot.Worker.csproj @@ -5,14 +5,14 @@ - - - - - - - - + + + + + + + + diff --git a/src/StevesBot.Worker/Telemetry/Extensions.cs b/src/src/StevesBot.Worker/Telemetry/Extensions.cs similarity index 100% rename from src/StevesBot.Worker/Telemetry/Extensions.cs rename to src/src/StevesBot.Worker/Telemetry/Extensions.cs diff --git a/src/StevesBot.Worker/Telemetry/Instrumentation.cs b/src/src/StevesBot.Worker/Telemetry/Instrumentation.cs similarity index 100% rename from src/StevesBot.Worker/Telemetry/Instrumentation.cs rename to src/src/StevesBot.Worker/Telemetry/Instrumentation.cs diff --git a/src/StevesBot.Worker/Telemetry/SeqOptions.cs b/src/src/StevesBot.Worker/Telemetry/SeqOptions.cs similarity index 100% rename from src/StevesBot.Worker/Telemetry/SeqOptions.cs rename to src/src/StevesBot.Worker/Telemetry/SeqOptions.cs diff --git a/src/StevesBot.Worker/Threading/AsyncLock.cs b/src/src/StevesBot.Worker/Threading/AsyncLock.cs similarity index 100% rename from src/StevesBot.Worker/Threading/AsyncLock.cs rename to src/src/StevesBot.Worker/Threading/AsyncLock.cs diff --git a/src/StevesBot.Worker/Threading/IAsyncLock.cs b/src/src/StevesBot.Worker/Threading/IAsyncLock.cs similarity index 100% rename from src/StevesBot.Worker/Threading/IAsyncLock.cs rename to src/src/StevesBot.Worker/Threading/IAsyncLock.cs diff --git a/src/StevesBot.Worker/Threading/LockReleaser.cs b/src/src/StevesBot.Worker/Threading/LockReleaser.cs similarity index 100% rename from src/StevesBot.Worker/Threading/LockReleaser.cs rename to src/src/StevesBot.Worker/Threading/LockReleaser.cs diff --git a/src/StevesBot.Worker/Usings.cs b/src/src/StevesBot.Worker/Usings.cs similarity index 100% rename from src/StevesBot.Worker/Usings.cs rename to src/src/StevesBot.Worker/Usings.cs diff --git a/src/StevesBot.Worker/WebSockets/IWebSocket.cs b/src/src/StevesBot.Worker/WebSockets/IWebSocket.cs similarity index 100% rename from src/StevesBot.Worker/WebSockets/IWebSocket.cs rename to src/src/StevesBot.Worker/WebSockets/IWebSocket.cs diff --git a/src/StevesBot.Worker/WebSockets/IWebSocketFactory.cs b/src/src/StevesBot.Worker/WebSockets/IWebSocketFactory.cs similarity index 100% rename from src/StevesBot.Worker/WebSockets/IWebSocketFactory.cs rename to src/src/StevesBot.Worker/WebSockets/IWebSocketFactory.cs diff --git a/src/StevesBot.Worker/WebSockets/WebSocket.cs b/src/src/StevesBot.Worker/WebSockets/WebSocket.cs similarity index 100% rename from src/StevesBot.Worker/WebSockets/WebSocket.cs rename to src/src/StevesBot.Worker/WebSockets/WebSocket.cs diff --git a/src/StevesBot.Worker/WebSockets/WebSocketFactory.cs b/src/src/StevesBot.Worker/WebSockets/WebSocketFactory.cs similarity index 100% rename from src/StevesBot.Worker/WebSockets/WebSocketFactory.cs rename to src/src/StevesBot.Worker/WebSockets/WebSocketFactory.cs diff --git a/src/StevesBot.Worker/Worker.cs b/src/src/StevesBot.Worker/Worker.cs similarity index 100% rename from src/StevesBot.Worker/Worker.cs rename to src/src/StevesBot.Worker/Worker.cs diff --git a/src/StevesBot.Worker/appsettings.Example.json b/src/src/StevesBot.Worker/appsettings.Example.json similarity index 100% rename from src/StevesBot.Worker/appsettings.Example.json rename to src/src/StevesBot.Worker/appsettings.Example.json diff --git a/src/StevesBot.Worker.Tests/.editorconfig b/src/tests/.editorconfig similarity index 100% rename from src/StevesBot.Worker.Tests/.editorconfig rename to src/tests/.editorconfig diff --git a/src/tests/Directory.Build.props b/src/tests/Directory.Build.props new file mode 100644 index 0000000..1d3cea3 --- /dev/null +++ b/src/tests/Directory.Build.props @@ -0,0 +1,15 @@ + + + + + + false + + + + + + + + diff --git a/src/tests/Directory.packages.props b/src/tests/Directory.packages.props new file mode 100644 index 0000000..fb6598f --- /dev/null +++ b/src/tests/Directory.packages.props @@ -0,0 +1,19 @@ + + + + true + + + + + + + + + + + + + + + diff --git a/src/StevesBot.Worker.Tests/StevesBot.Worker.Tests.csproj b/src/tests/StevesBot.Webhook.Tests/StevesBot.Webhook.Tests.csproj similarity index 52% rename from src/StevesBot.Worker.Tests/StevesBot.Worker.Tests.csproj rename to src/tests/StevesBot.Webhook.Tests/StevesBot.Webhook.Tests.csproj index d225104..f84a1e3 100644 --- a/src/StevesBot.Worker.Tests/StevesBot.Worker.Tests.csproj +++ b/src/tests/StevesBot.Webhook.Tests/StevesBot.Webhook.Tests.csproj @@ -1,32 +1,32 @@ - - - - false - +ο»Ώ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - + + + + + + + + + + + true ./TestResults/Coverage/ cobertura - [StevesBot.Worker]* + [StevesBot.Webhook]* **/Program.cs,**/Worker.cs @@ -34,13 +34,4 @@ - - - - - - - - - diff --git a/src/tests/StevesBot.Webhook.Tests/UnitTest1.cs b/src/tests/StevesBot.Webhook.Tests/UnitTest1.cs new file mode 100644 index 0000000..46d432d --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +ο»Ώnamespace StevesBot.Webhook.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/src/StevesBot.Worker.Tests/Integration/Infrastructure/TestWebSocketServer.cs b/src/tests/StevesBot.Worker.Tests/Integration/Infrastructure/TestWebSocketServer.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Integration/Infrastructure/TestWebSocketServer.cs rename to src/tests/StevesBot.Worker.Tests/Integration/Infrastructure/TestWebSocketServer.cs diff --git a/src/StevesBot.Worker.Tests/Integration/WebSocketTests.cs b/src/tests/StevesBot.Worker.Tests/Integration/WebSocketTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Integration/WebSocketTests.cs rename to src/tests/StevesBot.Worker.Tests/Integration/WebSocketTests.cs diff --git a/src/tests/StevesBot.Worker.Tests/StevesBot.Worker.Tests.csproj b/src/tests/StevesBot.Worker.Tests/StevesBot.Worker.Tests.csproj new file mode 100644 index 0000000..b55d340 --- /dev/null +++ b/src/tests/StevesBot.Worker.Tests/StevesBot.Worker.Tests.csproj @@ -0,0 +1,39 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + true + ./TestResults/Coverage/ + cobertura + [StevesBot.Worker]* + **/Program.cs,**/Worker.cs + + + + + + + + + + + + diff --git a/src/StevesBot.Worker.Tests/Unit/ActivityTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/ActivityTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/ActivityTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/ActivityTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/ActivityTypeTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/ActivityTypeTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/ActivityTypeTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/ActivityTypeTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/AsyncLockTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/AsyncLockTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/AsyncLockTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/AsyncLockTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/CreateMessageRequestTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/CreateMessageRequestTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/CreateMessageRequestTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/CreateMessageRequestTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordClientOptionsTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordClientOptionsTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/DiscordClientOptionsTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/DiscordClientOptionsTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordCloseCodesTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordCloseCodesTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/DiscordCloseCodesTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/DiscordCloseCodesTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordEventConverterTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordEventConverterTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/DiscordEventConverterTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/DiscordEventConverterTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordEventTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordEventTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/DiscordEventTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/DiscordEventTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordEventTypesTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordEventTypesTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/DiscordEventTypesTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/DiscordEventTypesTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordGatewayClientExceptionTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordGatewayClientExceptionTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/DiscordGatewayClientExceptionTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/DiscordGatewayClientExceptionTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordIntentsTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordIntentsTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/DiscordIntentsTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/DiscordIntentsTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTypesTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTypesTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTypesTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTypesTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordOpCodesTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordOpCodesTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/DiscordOpCodesTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/DiscordOpCodesTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordRestClientExceptionTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordRestClientExceptionTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/DiscordRestClientExceptionTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/DiscordRestClientExceptionTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordRestClientTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordRestClientTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/DiscordRestClientTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/DiscordRestClientTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/ExtensionsTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/ExtensionsTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/ExtensionsTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/ExtensionsTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/HeartbeatAckDiscordEventTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/HeartbeatAckDiscordEventTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/HeartbeatAckDiscordEventTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/HeartbeatAckDiscordEventTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/HeartbeatDiscordEventTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/HeartbeatDiscordEventTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/HeartbeatDiscordEventTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/HeartbeatDiscordEventTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/HelloDiscordEventTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/HelloDiscordEventTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/HelloDiscordEventTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/HelloDiscordEventTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/IdentifyDataTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/IdentifyDataTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/IdentifyDataTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/IdentifyDataTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/IdentifyDiscordEventTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/IdentifyDiscordEventTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/IdentifyDiscordEventTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/IdentifyDiscordEventTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/InvalidSessionDiscordEventTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/InvalidSessionDiscordEventTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/InvalidSessionDiscordEventTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/InvalidSessionDiscordEventTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/LockReleaserTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/LockReleaserTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/LockReleaserTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/LockReleaserTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/MessageCreateDiscordEventTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/MessageCreateDiscordEventTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/MessageCreateDiscordEventTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/MessageCreateDiscordEventTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/PresenceStatusTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/PresenceStatusTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/PresenceStatusTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/PresenceStatusTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/ReadyDiscordEventTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/ReadyDiscordEventTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/ReadyDiscordEventTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/ReadyDiscordEventTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/ReconnectDiscordEventTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/ReconnectDiscordEventTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/ReconnectDiscordEventTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/ReconnectDiscordEventTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/ResumeDataTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/ResumeDataTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/ResumeDataTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/ResumeDataTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/ResumeDiscordEventTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/ResumeDiscordEventTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/ResumeDiscordEventTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/ResumeDiscordEventTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/UpdatePresenceDataTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/UpdatePresenceDataTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/UpdatePresenceDataTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/UpdatePresenceDataTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/UpdatePresenceDiscordEventTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/UpdatePresenceDiscordEventTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/UpdatePresenceDiscordEventTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/UpdatePresenceDiscordEventTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/WebSocketFactoryTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/WebSocketFactoryTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/WebSocketFactoryTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/WebSocketFactoryTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/WebSocketTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/WebSocketTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/WebSocketTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/WebSocketTests.cs diff --git a/src/StevesBot.Worker.Tests/Unit/WelcomeMessageHandlerTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/WelcomeMessageHandlerTests.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Unit/WelcomeMessageHandlerTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/WelcomeMessageHandlerTests.cs diff --git a/src/StevesBot.Worker.Tests/Usings.cs b/src/tests/StevesBot.Worker.Tests/Usings.cs similarity index 100% rename from src/StevesBot.Worker.Tests/Usings.cs rename to src/tests/StevesBot.Worker.Tests/Usings.cs From a1ef35d0a7815eaee9384b7de0005cecf54c89fe Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 26 May 2025 11:56:43 -0500 Subject: [PATCH 02/31] feat: begin adding subscription to YT notifications --- src/src/StevesBot.Webhook/Program.cs | 13 ++++- .../StevesBot.Webhook.csproj | 2 + .../StevesBot.Webhook/SubscriptionWorker.cs | 49 +++++++++++++++++++ src/src/StevesBot.Webhook/Usings.cs | 6 +++ .../YouTube/IPubSubClient.cs | 16 ++++++ .../StevesBot.Webhook/YouTube/PubSubClient.cs | 32 ++++++++++++ .../YouTube/PubSubClientException.cs | 16 ++++++ .../YouTube/SubscriptionOptions.cs | 10 ++++ .../appsettings.Example.json | 6 +++ 9 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/src/StevesBot.Webhook/SubscriptionWorker.cs create mode 100644 src/src/StevesBot.Webhook/Usings.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/PubSubClient.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/SubscriptionOptions.cs create mode 100644 src/src/StevesBot.Webhook/appsettings.Example.json diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index b5041fa..16fa3cd 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -1,7 +1,18 @@ var builder = WebApplication.CreateBuilder(args); +builder.Services + .AddOptionsWithValidateOnStart() + .BindConfiguration(nameof(SubscriptionOptions)) + .ValidateDataAnnotations(); + +builder.Services + .AddHttpClient() + .AddStandardResilienceHandler(); + builder.Services.AddOpenApi(); +builder.Services.AddHostedService(); + var app = builder.Build(); if (app.Environment.IsDevelopment()) @@ -11,4 +22,4 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj b/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj index 250aa1e..b26a484 100644 --- a/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj +++ b/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj @@ -8,6 +8,8 @@ + + diff --git a/src/src/StevesBot.Webhook/SubscriptionWorker.cs b/src/src/StevesBot.Webhook/SubscriptionWorker.cs new file mode 100644 index 0000000..436ab1e --- /dev/null +++ b/src/src/StevesBot.Webhook/SubscriptionWorker.cs @@ -0,0 +1,49 @@ +namespace StevesBot.Webhook; + +internal sealed class SubscriptionWorker( + ILogger logger, + IOptions options, + IPubSubClient pubSubClient +) : IHostedLifecycleService +{ + private readonly ILogger _logger = logger; + private readonly SubscriptionOptions _options = options.Value; + private readonly IPubSubClient _pubSubClient = pubSubClient; + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StartingAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public async Task StartedAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Subscribing to notifications"); + + await _pubSubClient.SubscribeAsync( + _options.CallbackBaseUrl, + _options.TopicUrl, + cancellationToken + ); + } + + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StoppingAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StoppedAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/Usings.cs b/src/src/StevesBot.Webhook/Usings.cs new file mode 100644 index 0000000..6deeb72 --- /dev/null +++ b/src/src/StevesBot.Webhook/Usings.cs @@ -0,0 +1,6 @@ +global using System.ComponentModel.DataAnnotations; + +global using Microsoft.Extensions.Options; + +global using StevesBot.Webhook; +global using StevesBot.Webhook.YouTube; \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs b/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs new file mode 100644 index 0000000..4eb39f5 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs @@ -0,0 +1,16 @@ +namespace StevesBot.Webhook.YouTube; + +internal interface IPubSubClient +{ + Task SubscribeAsync( + string callbackUrl, + string topicUrl, + CancellationToken cancellationToken = default + ); + + Task UnsubscribeAsync( + string callbackUrl, + string topicUrl, + CancellationToken cancellationToken = default + ); +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs b/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs new file mode 100644 index 0000000..0c79a26 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs @@ -0,0 +1,32 @@ + +namespace StevesBot.Webhook.YouTube; + +internal sealed class PubSubClient(HttpClient httpClient) : IPubSubClient +{ + private const string SubscribeEndpoint = "subscribe"; + private readonly HttpClient _httpClient = httpClient; + + public async Task SubscribeAsync(string callbackUrl, string topicUrl, CancellationToken cancellationToken = default) + { + var uri = new Uri(SubscribeEndpoint, UriKind.Relative); + var formFields = new Dictionary() + { + { "hub.callback", callbackUrl }, + { "hub.topic", topicUrl }, + { "hub.verify", "async" }, + { "hub.mode", "subscribe" } + }; + using var form = new FormUrlEncodedContent(formFields); + var response = await _httpClient.PostAsync(uri, form, cancellationToken); + + if (response.IsSuccessStatusCode is false) + { + throw new PubSubClientException("Failed to subscribe"); + } + } + + public Task UnsubscribeAsync(string callbackUrl, string topicUrl, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} diff --git a/src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs b/src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs new file mode 100644 index 0000000..27aa0ec --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs @@ -0,0 +1,16 @@ +namespace StevesBot.Webhook.YouTube; + +internal sealed class PubSubClientException : Exception +{ + public PubSubClientException() + { + } + + public PubSubClientException(string message) : base(message) + { + } + + public PubSubClientException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/SubscriptionOptions.cs b/src/src/StevesBot.Webhook/YouTube/SubscriptionOptions.cs new file mode 100644 index 0000000..da8e9f1 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/SubscriptionOptions.cs @@ -0,0 +1,10 @@ +namespace StevesBot.Webhook.YouTube; + +internal sealed record SubscriptionOptions +{ + [Required] + public string CallbackBaseUrl { get; init; } = string.Empty; + + [Required] + public string TopicUrl { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/appsettings.Example.json b/src/src/StevesBot.Webhook/appsettings.Example.json new file mode 100644 index 0000000..87686fb --- /dev/null +++ b/src/src/StevesBot.Webhook/appsettings.Example.json @@ -0,0 +1,6 @@ +{ + "SubscriptionOptions": { + "CallbackUrl": "CallbackUrl", + "TopicUrl": "TopicUrl" + } +} From 0b42739cbb8f2e8fafa760a4f63237c97038f968 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 26 May 2025 12:38:04 -0500 Subject: [PATCH 03/31] feat: implement verification of subscription request --- src/src/StevesBot.Webhook/Program.cs | 35 ++++++++++++++++++- .../StevesBot.Webhook/SubscriptionWorker.cs | 2 +- .../YouTube/SubscriptionOptions.cs | 2 +- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index 16fa3cd..115485d 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Mvc; + var builder = WebApplication.CreateBuilder(args); builder.Services @@ -6,7 +8,9 @@ builder.Services .ValidateDataAnnotations(); builder.Services - .AddHttpClient() + .AddHttpClient( + static c => c.BaseAddress = new("https://pubsubhubbub.appspot.com") + ) .AddStandardResilienceHandler(); builder.Services.AddOpenApi(); @@ -22,4 +26,33 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); +const string ytCallback = "yt-callback"; + +app.MapGet( + ytCallback, + static ( + [FromQuery(Name = "hub.mode")] string mode, + [FromQuery(Name = "hub.topic")] string topic, + [FromQuery(Name = "hub.challenge")] string challenge, + [FromServices] IOptions subOptions, + [FromServices] ILogger logger + ) => + { + if (topic != subOptions.Value.TopicUrl) + { + logger.LogInformation("Received verification request for wrong topic: {Topic}", topic); + return Results.NotFound(); + } + + return Results.Text(challenge); + } +); + +// TODO: Implement logic to do the following: +// - extract video id from notification +// - identify video as a stream or not +// - if is stream create discord message +// - if not then just log a message +app.MapPost(ytCallback, static () => "hello"); + app.Run(); \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/SubscriptionWorker.cs b/src/src/StevesBot.Webhook/SubscriptionWorker.cs index 436ab1e..f4bc64b 100644 --- a/src/src/StevesBot.Webhook/SubscriptionWorker.cs +++ b/src/src/StevesBot.Webhook/SubscriptionWorker.cs @@ -25,7 +25,7 @@ internal sealed class SubscriptionWorker( _logger.LogInformation("Subscribing to notifications"); await _pubSubClient.SubscribeAsync( - _options.CallbackBaseUrl, + _options.CallbackUrl, _options.TopicUrl, cancellationToken ); diff --git a/src/src/StevesBot.Webhook/YouTube/SubscriptionOptions.cs b/src/src/StevesBot.Webhook/YouTube/SubscriptionOptions.cs index da8e9f1..f64cb56 100644 --- a/src/src/StevesBot.Webhook/YouTube/SubscriptionOptions.cs +++ b/src/src/StevesBot.Webhook/YouTube/SubscriptionOptions.cs @@ -3,7 +3,7 @@ namespace StevesBot.Webhook.YouTube; internal sealed record SubscriptionOptions { [Required] - public string CallbackBaseUrl { get; init; } = string.Empty; + public string CallbackUrl { get; init; } = string.Empty; [Required] public string TopicUrl { get; init; } = string.Empty; From 6543387270c3abc59f511afe6d256068421aec9d Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 26 May 2025 22:55:08 -0500 Subject: [PATCH 04/31] feat: begin working through how we will handle resubscribing and unsubscribing --- src/src/StevesBot.Webhook/Program.cs | 22 +------- src/src/StevesBot.Webhook/Usings.cs | 3 +- .../YouTube/IPubSubClient.cs | 8 +-- .../StevesBot.Webhook/YouTube/PubSubClient.cs | 27 +++++---- .../YouTube/PubSubClientException.cs | 16 ------ .../{ => YouTube}/SubscriptionWorker.cs | 12 +++- .../YouTube/Tasks/SubscribeTask.cs | 6 ++ .../YouTube/Tasks/SubscriptionTask.cs | 7 +++ .../YouTube/Tasks/UnsubscribeTask.cs | 5 ++ .../YouTube/VerifySubscriptionHandler.cs | 55 +++++++++++++++++++ 10 files changed, 104 insertions(+), 57 deletions(-) delete mode 100644 src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs rename src/src/StevesBot.Webhook/{ => YouTube}/SubscriptionWorker.cs (79%) create mode 100644 src/src/StevesBot.Webhook/YouTube/Tasks/SubscribeTask.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/Tasks/SubscriptionTask.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/Tasks/UnsubscribeTask.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index 115485d..03ca99f 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -1,5 +1,3 @@ -using Microsoft.AspNetCore.Mvc; - var builder = WebApplication.CreateBuilder(args); builder.Services @@ -28,25 +26,7 @@ app.UseHttpsRedirection(); const string ytCallback = "yt-callback"; -app.MapGet( - ytCallback, - static ( - [FromQuery(Name = "hub.mode")] string mode, - [FromQuery(Name = "hub.topic")] string topic, - [FromQuery(Name = "hub.challenge")] string challenge, - [FromServices] IOptions subOptions, - [FromServices] ILogger logger - ) => - { - if (topic != subOptions.Value.TopicUrl) - { - logger.LogInformation("Received verification request for wrong topic: {Topic}", topic); - return Results.NotFound(); - } - - return Results.Text(challenge); - } -); +app.MapGet(ytCallback, VerifySubscriptionHandler.HandleAsync); // TODO: Implement logic to do the following: // - extract video id from notification diff --git a/src/src/StevesBot.Webhook/Usings.cs b/src/src/StevesBot.Webhook/Usings.cs index 6deeb72..b901b81 100644 --- a/src/src/StevesBot.Webhook/Usings.cs +++ b/src/src/StevesBot.Webhook/Usings.cs @@ -1,6 +1,7 @@ global using System.ComponentModel.DataAnnotations; +global using Microsoft.AspNetCore.Mvc; global using Microsoft.Extensions.Options; global using StevesBot.Webhook; -global using StevesBot.Webhook.YouTube; \ No newline at end of file +global using StevesBot.Webhook.YouTube; diff --git a/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs b/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs index 4eb39f5..1d19566 100644 --- a/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs +++ b/src/src/StevesBot.Webhook/YouTube/IPubSubClient.cs @@ -2,13 +2,7 @@ namespace StevesBot.Webhook.YouTube; internal interface IPubSubClient { - Task SubscribeAsync( - string callbackUrl, - string topicUrl, - CancellationToken cancellationToken = default - ); - - Task UnsubscribeAsync( + Task SubscribeAsync( string callbackUrl, string topicUrl, CancellationToken cancellationToken = default diff --git a/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs b/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs index 0c79a26..0329764 100644 --- a/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs +++ b/src/src/StevesBot.Webhook/YouTube/PubSubClient.cs @@ -1,12 +1,15 @@ - namespace StevesBot.Webhook.YouTube; -internal sealed class PubSubClient(HttpClient httpClient) : IPubSubClient +internal sealed class PubSubClient( + HttpClient httpClient, + ILogger logger +) : IPubSubClient { private const string SubscribeEndpoint = "subscribe"; private readonly HttpClient _httpClient = httpClient; + private readonly ILogger _logger = logger; - public async Task SubscribeAsync(string callbackUrl, string topicUrl, CancellationToken cancellationToken = default) + public async Task SubscribeAsync(string callbackUrl, string topicUrl, CancellationToken cancellationToken = default) { var uri = new Uri(SubscribeEndpoint, UriKind.Relative); var formFields = new Dictionary() @@ -21,12 +24,16 @@ internal sealed class PubSubClient(HttpClient httpClient) : IPubSubClient if (response.IsSuccessStatusCode is false) { - throw new PubSubClientException("Failed to subscribe"); - } - } + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); - public Task UnsubscribeAsync(string callbackUrl, string topicUrl, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); + _logger.LogDebug( + "Failed to subscribe to topic {TopicUrl} with callback {CallbackUrl}: {ErrorMessage}", + topicUrl, + callbackUrl, + responseContent + ); + } + + return response.IsSuccessStatusCode; } -} +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs b/src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs deleted file mode 100644 index 27aa0ec..0000000 --- a/src/src/StevesBot.Webhook/YouTube/PubSubClientException.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace StevesBot.Webhook.YouTube; - -internal sealed class PubSubClientException : Exception -{ - public PubSubClientException() - { - } - - public PubSubClientException(string message) : base(message) - { - } - - public PubSubClientException(string message, Exception innerException) : base(message, innerException) - { - } -} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/SubscriptionWorker.cs b/src/src/StevesBot.Webhook/YouTube/SubscriptionWorker.cs similarity index 79% rename from src/src/StevesBot.Webhook/SubscriptionWorker.cs rename to src/src/StevesBot.Webhook/YouTube/SubscriptionWorker.cs index f4bc64b..bf6e100 100644 --- a/src/src/StevesBot.Webhook/SubscriptionWorker.cs +++ b/src/src/StevesBot.Webhook/YouTube/SubscriptionWorker.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Webhook; +namespace StevesBot.Webhook.YouTube; internal sealed class SubscriptionWorker( ILogger logger, @@ -24,11 +24,19 @@ internal sealed class SubscriptionWorker( { _logger.LogInformation("Subscribing to notifications"); - await _pubSubClient.SubscribeAsync( + var isSubscribed = await _pubSubClient.SubscribeAsync( _options.CallbackUrl, _options.TopicUrl, cancellationToken ); + + if (isSubscribed) + { + _logger.LogInformation("Successfully subscribed to notifications"); + return; + } + + _logger.LogWarning("Failed to subscribe to notifications"); } diff --git a/src/src/StevesBot.Webhook/YouTube/Tasks/SubscribeTask.cs b/src/src/StevesBot.Webhook/YouTube/Tasks/SubscribeTask.cs new file mode 100644 index 0000000..4c03a4a --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/Tasks/SubscribeTask.cs @@ -0,0 +1,6 @@ +namespace StevesBot.Webhook.YouTube.Tasks; + +internal sealed record SubscribeTask : SubscriptionTask +{ + public DateTimeOffset ExpiresAt { get; init; } +} diff --git a/src/src/StevesBot.Webhook/YouTube/Tasks/SubscriptionTask.cs b/src/src/StevesBot.Webhook/YouTube/Tasks/SubscriptionTask.cs new file mode 100644 index 0000000..888a649 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/Tasks/SubscriptionTask.cs @@ -0,0 +1,7 @@ +namespace StevesBot.Webhook.YouTube.Tasks; + +internal abstract record SubscriptionTask +{ + public string CallbackUrl { get; init; } = string.Empty; + public string TopicUrl { get; init; } = string.Empty; +} diff --git a/src/src/StevesBot.Webhook/YouTube/Tasks/UnsubscribeTask.cs b/src/src/StevesBot.Webhook/YouTube/Tasks/UnsubscribeTask.cs new file mode 100644 index 0000000..b678e77 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/Tasks/UnsubscribeTask.cs @@ -0,0 +1,5 @@ +namespace StevesBot.Webhook.YouTube.Tasks; + +internal sealed record UnsubscribeTask : SubscriptionTask +{ +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs b/src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs new file mode 100644 index 0000000..c20275f --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs @@ -0,0 +1,55 @@ +namespace StevesBot.Webhook.YouTube; + +internal static class VerifySubscriptionHandler +{ + public static IResult HandleAsync( + [FromQuery(Name = "hub.mode")] string mode, + [FromQuery(Name = "hub.topic")] string topic, + [FromQuery(Name = "hub.reason")] string? reason, + [FromQuery(Name = "hub.challenge")] string? challenge, + [FromQuery(Name = "hub.lease_seconds")] string? leaseSeconds, + [FromServices] IOptions subOptions, + [FromServices] ILogger logger + ) + { + if (mode is "denied") + { + logger.LogInformation("Received subscription denial for topic: {Topic}, reason: {Reason}", topic, reason); + return Results.BadRequest("Subscription denied"); + } + + if (topic != subOptions.Value.TopicUrl) + { + logger.LogInformation("Received verification request for wrong topic: {Topic}", topic); + return Results.NotFound(); + } + + if (mode is "subscribe") + { + // TODO: If it is a subscription request + // we need to queue up a resubscription + // request to be executed just before + // the hub.lease expires. + logger.LogInformation( + "Received subscription request for topic: {Topic}, challenge: {Challenge}, lease: {LeaseSeconds}", + topic, + challenge, + leaseSeconds + ); + } + + if (mode is "unsubscribe") + { + // TODO: If it is a unsubscription request + // we need to queue up an ubsubscription + // request to be executed immediately + logger.LogInformation( + "Received unsubscription request for topic: {Topic}, challenge: {Challenge}", + topic, + challenge + ); + } + + return Results.Text(challenge); + } +} \ No newline at end of file From c544e33e28c1b5cd2589b1f3157d56ff07f238ee Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 27 May 2025 16:48:57 -0500 Subject: [PATCH 05/31] feat: determine if notification is for stream or not --- src/src/StevesBot.Webhook/Program.cs | 81 +++++++++++++++++-- src/src/StevesBot.Webhook/Usings.cs | 5 +- .../YouTube/Data/IYouTubeDataApiClient.cs | 10 +++ .../YouTube/Data/YouTubeDataApiClient.cs | 54 +++++++++++++ .../Data/YouTubeLiveStreamingDetails.cs | 24 ++++++ .../YouTube/Data/YouTubePageInfo.cs | 12 +++ .../YouTube/Data/YouTubeVideo.cs | 12 +++ .../YouTube/Data/YouTubeVideoListResponse.cs | 12 +++ .../YouTube/PubSubClientOptions.cs | 6 ++ .../YouTube/SubscriptionWorker.cs | 67 ++++++++++++++- .../YouTube/Tasks/SubscribeTask.cs | 6 +- .../YouTube/Tasks/SubscriptionTask.cs | 7 -- .../YouTube/Tasks/UnsubscribeTask.cs | 5 -- .../YouTube/VerifySubscriptionHandler.cs | 25 ++++-- .../YouTube/YouTubeClientOptions.cs | 10 +++ .../appsettings.Example.json | 14 ++++ .../StevesBot.Webhook.Tests/UnitTest1.cs | 10 +-- 17 files changed, 323 insertions(+), 37 deletions(-) create mode 100644 src/src/StevesBot.Webhook/YouTube/Data/IYouTubeDataApiClient.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/Data/YouTubeDataApiClient.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/Data/YouTubeLiveStreamingDetails.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/Data/YouTubePageInfo.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideo.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideoListResponse.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/PubSubClientOptions.cs delete mode 100644 src/src/StevesBot.Webhook/YouTube/Tasks/SubscriptionTask.cs delete mode 100644 src/src/StevesBot.Webhook/YouTube/Tasks/UnsubscribeTask.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/YouTubeClientOptions.cs diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index 03ca99f..0bf22d4 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -1,18 +1,50 @@ +using System.Text.RegularExpressions; + var builder = WebApplication.CreateBuilder(args); +builder.Services.Configure( + static options => options.ShutdownTimeout = TimeSpan.FromSeconds(30) +); + builder.Services .AddOptionsWithValidateOnStart() .BindConfiguration(nameof(SubscriptionOptions)) .ValidateDataAnnotations(); +builder.Services + .AddOptionsWithValidateOnStart() + .BindConfiguration(nameof(YouTubeClientOptions)) + .ValidateDataAnnotations(); + +builder.Services + .AddOptionsWithValidateOnStart() + .BindConfiguration(nameof(PubSubClientOptions)) + .ValidateDataAnnotations(); + builder.Services .AddHttpClient( - static c => c.BaseAddress = new("https://pubsubhubbub.appspot.com") + static (sp, c) => + { + var options = sp.GetRequiredService>().Value; + c.BaseAddress = new(options.BaseUrl); + } + ) + .AddStandardResilienceHandler(); + +builder.Services + .AddHttpClient( + static (sp, c) => + { + var options = sp.GetRequiredService>().Value; + c.BaseAddress = new(options.BaseUrl); + } ) .AddStandardResilienceHandler(); builder.Services.AddOpenApi(); +builder.Services.AddSingleton(TimeProvider.System); +builder.Services.AddSingleton>(); builder.Services.AddHostedService(); var app = builder.Build(); @@ -22,17 +54,52 @@ if (app.Environment.IsDevelopment()) app.MapOpenApi(); } -app.UseHttpsRedirection(); +// app.UseHttpsRedirection(); const string ytCallback = "yt-callback"; app.MapGet(ytCallback, VerifySubscriptionHandler.HandleAsync); // TODO: Implement logic to do the following: -// - extract video id from notification -// - identify video as a stream or not // - if is stream create discord message -// - if not then just log a message -app.MapPost(ytCallback, static () => "hello"); +app.MapPost(ytCallback, static async (HttpContext context, [FromServices] ILogger logger, [FromServices] IYouTubeDataApiClient youTubeDataApiClient) => +{ + var body = ""; + using StreamReader stream = new(context.Request.Body); + body = await stream.ReadToEndAsync(); -app.Run(); \ No newline at end of file + var videoIdRegex = VideoIdRegex(); + var match = videoIdRegex.Match(body); + + if (match.Success is false) + { + logger.LogWarning("No video ID found in the request body."); + } + + var videoId = match.Groups[1].Value; + var video = await youTubeDataApiClient.GetVideoByIdAsync(videoId, ["liveStreamingDetails"]); + + if (video is null) + { + logger.LogWarning("Video with ID {VideoId} not found.", videoId); + return Results.NotFound(); + } + + if (video.IsStream is false) + { + logger.LogInformation("Video ID {VideoId} is not a live stream.", videoId); + return Results.Ok(); + } + + logger.LogInformation("Video ID {VideoId} is a live stream.", videoId); + + return Results.Ok(); +}); + +app.Run(); + +internal partial class Program +{ + [GeneratedRegex(@"(.*?)")] + private static partial Regex VideoIdRegex(); +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/Usings.cs b/src/src/StevesBot.Webhook/Usings.cs index b901b81..73e000f 100644 --- a/src/src/StevesBot.Webhook/Usings.cs +++ b/src/src/StevesBot.Webhook/Usings.cs @@ -1,7 +1,10 @@ +global using System.Collections.Concurrent; global using System.ComponentModel.DataAnnotations; +global using System.Text.Json.Serialization; global using Microsoft.AspNetCore.Mvc; global using Microsoft.Extensions.Options; -global using StevesBot.Webhook; global using StevesBot.Webhook.YouTube; +global using StevesBot.Webhook.YouTube.Data; +global using StevesBot.Webhook.YouTube.Tasks; \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/Data/IYouTubeDataApiClient.cs b/src/src/StevesBot.Webhook/YouTube/Data/IYouTubeDataApiClient.cs new file mode 100644 index 0000000..32f1df1 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/Data/IYouTubeDataApiClient.cs @@ -0,0 +1,10 @@ +namespace StevesBot.Webhook.YouTube.Data; + +internal interface IYouTubeDataApiClient +{ + Task GetVideoByIdAsync( + string videoId, + string[]? part = null, + CancellationToken cancellationToken = default + ); +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/Data/YouTubeDataApiClient.cs b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeDataApiClient.cs new file mode 100644 index 0000000..dad7469 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeDataApiClient.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.WebUtilities; + +namespace StevesBot.Webhook.YouTube.Data; + +internal sealed class YouTubeDataApiClient( + HttpClient httpClient, + ILogger logger, + IOptions options +) : IYouTubeDataApiClient +{ + private const string VideosEndpoint = "videos"; + private readonly HttpClient _httpClient = httpClient; + private readonly ILogger _logger = logger; + private readonly YouTubeClientOptions _options = options.Value; + + public async Task GetVideoByIdAsync( + string videoId, + string[]? part = null, + CancellationToken cancellationToken = default + ) + { + var queryParams = new Dictionary + { + ["id"] = videoId, + ["key"] = _options.ApiKey, + }; + + if (part is not null && part.Length > 0) + { + queryParams["part"] = string.Join(',', part); + } + + var requestEndpoint = QueryHelpers.AddQueryString(VideosEndpoint, queryParams); + var requestUri = new Uri(requestEndpoint, UriKind.Relative); + var response = await _httpClient.GetAsync(requestUri, cancellationToken); + + if (response.IsSuccessStatusCode is false) + { + var content = await response.Content.ReadAsStringAsync(cancellationToken); + + _logger.LogDebug( + "Failed to get video by ID {VideoId} from YouTube Data API. Status code: {StatusCode}, Content: {Content}", + videoId, + response.StatusCode, + content + ); + + return null; + } + + var responseContent = await response.Content.ReadFromJsonAsync(cancellationToken); + return responseContent?.Items.FirstOrDefault(i => i.Id == videoId); + } +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/Data/YouTubeLiveStreamingDetails.cs b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeLiveStreamingDetails.cs new file mode 100644 index 0000000..ffc8717 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeLiveStreamingDetails.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace StevesBot.Webhook.YouTube.Data; + +internal sealed record YouTubeLiveStreamingDetails +{ + [JsonPropertyName("actualStartTime")] + public DateTimeOffset? ActualStartTime { get; init; } + + [JsonPropertyName("actualEndTime")] + public DateTimeOffset? ActualEndTime { get; init; } + + [JsonPropertyName("scheduledStartTime")] + public DateTimeOffset? ScheduledStartTime { get; init; } + + [JsonPropertyName("scheduledEndTime")] + public DateTimeOffset? ScheduledEndTime { get; init; } + + [JsonPropertyName("concurrentViewers")] + public ulong? ConcurrentViewers { get; init; } + + [JsonPropertyName("activeLiveChatId")] + public string? ActiveLiveChatId { get; init; } +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/Data/YouTubePageInfo.cs b/src/src/StevesBot.Webhook/YouTube/Data/YouTubePageInfo.cs new file mode 100644 index 0000000..ea51800 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/Data/YouTubePageInfo.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace StevesBot.Webhook.YouTube.Data; + +internal sealed record YouTubePageInfo +{ + [JsonPropertyName("totalResults")] + public int TotalResults { get; init; } + + [JsonPropertyName("resultsPerPage")] + public int ResultsPerPage { get; init; } +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideo.cs b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideo.cs new file mode 100644 index 0000000..10ed6c9 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideo.cs @@ -0,0 +1,12 @@ +namespace StevesBot.Webhook.YouTube.Data; + +internal sealed record YouTubeVideo +{ + [JsonPropertyName("id")] + public string Id { get; init; } = string.Empty; + + [JsonPropertyName("liveStreamingDetails")] + public YouTubeLiveStreamingDetails? LiveStreamingDetails { get; init; } + + public bool IsStream => LiveStreamingDetails is not null; +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideoListResponse.cs b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideoListResponse.cs new file mode 100644 index 0000000..04d744e --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideoListResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace StevesBot.Webhook.YouTube.Data; + +internal sealed record YouTubeVideoListResponse +{ + [JsonPropertyName("items")] + public YouTubeVideo[] Items { get; init; } = []; + + [JsonPropertyName("pageInfo")] + public YouTubePageInfo PageInfo { get; init; } = new YouTubePageInfo(); +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/PubSubClientOptions.cs b/src/src/StevesBot.Webhook/YouTube/PubSubClientOptions.cs new file mode 100644 index 0000000..0ec922a --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/PubSubClientOptions.cs @@ -0,0 +1,6 @@ +namespace StevesBot.Webhook.YouTube; + +internal sealed class PubSubClientOptions +{ + public string BaseUrl { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/SubscriptionWorker.cs b/src/src/StevesBot.Webhook/YouTube/SubscriptionWorker.cs index bf6e100..5bb087b 100644 --- a/src/src/StevesBot.Webhook/YouTube/SubscriptionWorker.cs +++ b/src/src/StevesBot.Webhook/YouTube/SubscriptionWorker.cs @@ -3,15 +3,29 @@ namespace StevesBot.Webhook.YouTube; internal sealed class SubscriptionWorker( ILogger logger, IOptions options, - IPubSubClient pubSubClient -) : IHostedLifecycleService + IPubSubClient pubSubClient, + ConcurrentQueue subscriptionQueue, + TimeProvider timeProvider +) : IHostedLifecycleService, IDisposable { private readonly ILogger _logger = logger; private readonly SubscriptionOptions _options = options.Value; private readonly IPubSubClient _pubSubClient = pubSubClient; + private readonly ConcurrentQueue _subscriptionQueue = subscriptionQueue; + private readonly TimeProvider _timeProvider = timeProvider; + private ITimer? _subscriptionTimer; public Task StartAsync(CancellationToken cancellationToken) { + _logger.LogInformation("Starting subscription worker"); + + _subscriptionTimer = _timeProvider.CreateTimer( + callback: async _ => await ProcessSubscriptionQueueAsync(cancellationToken), + state: null, + dueTime: TimeSpan.FromSeconds(0), + period: TimeSpan.FromDays(1) + ); + return Task.CompletedTask; } @@ -42,6 +56,7 @@ internal sealed class SubscriptionWorker( public Task StopAsync(CancellationToken cancellationToken) { + _subscriptionTimer?.Change(Timeout.InfiniteTimeSpan, TimeSpan.Zero); return Task.CompletedTask; } @@ -54,4 +69,52 @@ internal sealed class SubscriptionWorker( { return Task.CompletedTask; } + + private async Task ProcessSubscriptionQueueAsync(CancellationToken cancellationToken) + { + while (_subscriptionQueue.TryDequeue(out var task)) + { + // We will attempt to process the task + // once every day so we will aim to + // process it two days before it expires. + var now = _timeProvider.GetUtcNow(); + var dueAt = task.ExpiresAt - TimeSpan.FromDays(2); + var isDue = now >= dueAt; + + if (isDue is false) + { + _logger.LogInformation( + "Skipping subscription task for topic {TopicUrl} as it is not due yet. Due at: {DueAt}, Current time: {CurrentTime}", + task.TopicUrl, + dueAt, + now + ); + + _subscriptionQueue.Enqueue(task); + + continue; + } + + _logger.LogInformation("Processing subscription task for topic: {TopicUrl}", task.TopicUrl); + + var isSubscribed = await _pubSubClient.SubscribeAsync( + task.CallbackUrl, + task.TopicUrl, + cancellationToken + ); + + if (isSubscribed) + { + _logger.LogInformation("Successfully subscribed to topic: {TopicUrl}", task.TopicUrl); + return; + } + + _logger.LogWarning("Failed to subscribe to topic: {TopicUrl}", task.TopicUrl); + } + } + + public void Dispose() + { + _subscriptionTimer?.Dispose(); + } } \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/Tasks/SubscribeTask.cs b/src/src/StevesBot.Webhook/YouTube/Tasks/SubscribeTask.cs index 4c03a4a..ffba96a 100644 --- a/src/src/StevesBot.Webhook/YouTube/Tasks/SubscribeTask.cs +++ b/src/src/StevesBot.Webhook/YouTube/Tasks/SubscribeTask.cs @@ -1,6 +1,8 @@ namespace StevesBot.Webhook.YouTube.Tasks; -internal sealed record SubscribeTask : SubscriptionTask +internal sealed record SubscribeTask { + public string CallbackUrl { get; init; } = string.Empty; + public string TopicUrl { get; init; } = string.Empty; public DateTimeOffset ExpiresAt { get; init; } -} +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/Tasks/SubscriptionTask.cs b/src/src/StevesBot.Webhook/YouTube/Tasks/SubscriptionTask.cs deleted file mode 100644 index 888a649..0000000 --- a/src/src/StevesBot.Webhook/YouTube/Tasks/SubscriptionTask.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StevesBot.Webhook.YouTube.Tasks; - -internal abstract record SubscriptionTask -{ - public string CallbackUrl { get; init; } = string.Empty; - public string TopicUrl { get; init; } = string.Empty; -} diff --git a/src/src/StevesBot.Webhook/YouTube/Tasks/UnsubscribeTask.cs b/src/src/StevesBot.Webhook/YouTube/Tasks/UnsubscribeTask.cs deleted file mode 100644 index b678e77..0000000 --- a/src/src/StevesBot.Webhook/YouTube/Tasks/UnsubscribeTask.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace StevesBot.Webhook.YouTube.Tasks; - -internal sealed record UnsubscribeTask : SubscriptionTask -{ -} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs b/src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs index c20275f..0286856 100644 --- a/src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs +++ b/src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs @@ -9,7 +9,8 @@ internal static class VerifySubscriptionHandler [FromQuery(Name = "hub.challenge")] string? challenge, [FromQuery(Name = "hub.lease_seconds")] string? leaseSeconds, [FromServices] IOptions subOptions, - [FromServices] ILogger logger + [FromServices] ILogger logger, + [FromServices] ConcurrentQueue subscriptionQueue ) { if (mode is "denied") @@ -26,23 +27,31 @@ internal static class VerifySubscriptionHandler if (mode is "subscribe") { - // TODO: If it is a subscription request - // we need to queue up a resubscription - // request to be executed just before - // the hub.lease expires. logger.LogInformation( "Received subscription request for topic: {Topic}, challenge: {Challenge}, lease: {LeaseSeconds}", topic, challenge, leaseSeconds ); + + if (string.IsNullOrWhiteSpace(leaseSeconds) || !long.TryParse(leaseSeconds, out var parsedSeconds)) + { + logger.LogWarning("Invalid or missing lease_seconds parameter: {LeaseSeconds}", leaseSeconds); + return Results.BadRequest("Invalid lease_seconds parameter"); + } + + var task = new SubscribeTask + { + CallbackUrl = subOptions.Value.CallbackUrl, + TopicUrl = topic, + ExpiresAt = DateTime.UtcNow.AddSeconds(parsedSeconds), + }; + + subscriptionQueue.Enqueue(task); } if (mode is "unsubscribe") { - // TODO: If it is a unsubscription request - // we need to queue up an ubsubscription - // request to be executed immediately logger.LogInformation( "Received unsubscription request for topic: {Topic}, challenge: {Challenge}", topic, diff --git a/src/src/StevesBot.Webhook/YouTube/YouTubeClientOptions.cs b/src/src/StevesBot.Webhook/YouTube/YouTubeClientOptions.cs new file mode 100644 index 0000000..78828f3 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/YouTubeClientOptions.cs @@ -0,0 +1,10 @@ +namespace StevesBot.Webhook.YouTube; + +internal sealed record YouTubeClientOptions +{ + [Required] + public string BaseUrl { get; init; } = string.Empty; + + [Required] + public string ApiKey { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/appsettings.Example.json b/src/src/StevesBot.Webhook/appsettings.Example.json index 87686fb..56defff 100644 --- a/src/src/StevesBot.Webhook/appsettings.Example.json +++ b/src/src/StevesBot.Webhook/appsettings.Example.json @@ -1,6 +1,20 @@ { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "YouTubeClientOptions": { + "BaseUrl": "BaseUrl", + "ApiKey": "ApiKey" + }, "SubscriptionOptions": { "CallbackUrl": "CallbackUrl", "TopicUrl": "TopicUrl" + }, + "PubSubClientOptions": { + "BaseUrl": "BaseUrl" } } diff --git a/src/tests/StevesBot.Webhook.Tests/UnitTest1.cs b/src/tests/StevesBot.Webhook.Tests/UnitTest1.cs index 46d432d..33fdda7 100644 --- a/src/tests/StevesBot.Webhook.Tests/UnitTest1.cs +++ b/src/tests/StevesBot.Webhook.Tests/UnitTest1.cs @@ -2,9 +2,9 @@ public class UnitTest1 { - [Fact] - public void Test1() - { + [Fact] + public void Test1() + { - } -} + } +} \ No newline at end of file From 1669bdeebcd8dfb9e470704211295c502f6a6bab Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 27 May 2025 16:56:38 -0500 Subject: [PATCH 06/31] fix: make sure it is a stream and it is live --- src/src/StevesBot.Webhook/Program.cs | 2 +- src/src/StevesBot.Webhook/YouTube/Data/YouTubeSnippet.cs | 7 +++++++ src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideo.cs | 5 ++++- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 src/src/StevesBot.Webhook/YouTube/Data/YouTubeSnippet.cs diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index 0bf22d4..29155f0 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -85,7 +85,7 @@ app.MapPost(ytCallback, static async (HttpContext context, [FromServices] ILogge return Results.NotFound(); } - if (video.IsStream is false) + if (video.IsLiveStream is false) { logger.LogInformation("Video ID {VideoId} is not a live stream.", videoId); return Results.Ok(); diff --git a/src/src/StevesBot.Webhook/YouTube/Data/YouTubeSnippet.cs b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeSnippet.cs new file mode 100644 index 0000000..7b0105f --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeSnippet.cs @@ -0,0 +1,7 @@ +namespace StevesBot.Webhook.YouTube.Data; + +internal sealed record YouTubeSnippet +{ + [JsonPropertyName("liveBroadcastContent")] + public string LiveBroadcastContent { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideo.cs b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideo.cs index 10ed6c9..6f68867 100644 --- a/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideo.cs +++ b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideo.cs @@ -8,5 +8,8 @@ internal sealed record YouTubeVideo [JsonPropertyName("liveStreamingDetails")] public YouTubeLiveStreamingDetails? LiveStreamingDetails { get; init; } - public bool IsStream => LiveStreamingDetails is not null; + [JsonPropertyName("snippet")] + public YouTubeSnippet Snippet { get; init; } = new(); + + public bool IsLiveStream => LiveStreamingDetails is not null && Snippet.LiveBroadcastContent is "live"; } \ No newline at end of file From 992811ccad0aaebc505d6f2afb62b6f3bf3c7f10 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 27 May 2025 17:10:57 -0500 Subject: [PATCH 07/31] refactor: extract to handler --- .../Handlers/NotificationHandler.cs | 51 ++++++++++++++++++ .../VerifySubscriptionHandler.cs | 2 +- src/src/StevesBot.Webhook/Program.cs | 54 +------------------ src/src/StevesBot.Webhook/Usings.cs | 2 + 4 files changed, 56 insertions(+), 53 deletions(-) create mode 100644 src/src/StevesBot.Webhook/Handlers/NotificationHandler.cs rename src/src/StevesBot.Webhook/{YouTube => Handlers}/VerifySubscriptionHandler.cs (98%) diff --git a/src/src/StevesBot.Webhook/Handlers/NotificationHandler.cs b/src/src/StevesBot.Webhook/Handlers/NotificationHandler.cs new file mode 100644 index 0000000..ba1cb5a --- /dev/null +++ b/src/src/StevesBot.Webhook/Handlers/NotificationHandler.cs @@ -0,0 +1,51 @@ +namespace StevesBot.Webhook.Handlers; + +// TODO: Implement logic to do the following: +// - if is stream create discord message +internal static class NotificationHandler +{ + public static async Task HandleAsync( + HttpContext context, + [FromServices] ILogger logger, + [FromServices] IYouTubeDataApiClient youTubeDataApiClient + ) + { + using StreamReader stream = new(context.Request.Body); + var body = await stream.ReadToEndAsync(); + + var videoIdRegex = VideoIdRegex.Regex(); + var match = videoIdRegex.Match(body); + + if (match.Success is false) + { + logger.LogWarning("No video ID found in the request body."); + return Results.BadRequest("No video ID found in the request body."); + } + + var videoId = match.Groups[1].Value; + var parts = new string[] { "liveStreamingDetails", "snippet" }; + var video = await youTubeDataApiClient.GetVideoByIdAsync(videoId, parts); + + if (video is null) + { + logger.LogWarning("Video with ID {VideoId} not found.", videoId); + return Results.NotFound(); + } + + if (video.IsLiveStream is false) + { + logger.LogInformation("Video ID {VideoId} is not a live stream.", videoId); + return Results.Ok(); + } + + logger.LogInformation("Video ID {VideoId} is a live stream.", videoId); + + return Results.Ok(); + } +} + +internal static partial class VideoIdRegex +{ + [GeneratedRegex(@"(.*?)")] + public static partial Regex Regex(); +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs b/src/src/StevesBot.Webhook/Handlers/VerifySubscriptionHandler.cs similarity index 98% rename from src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs rename to src/src/StevesBot.Webhook/Handlers/VerifySubscriptionHandler.cs index 0286856..14282bf 100644 --- a/src/src/StevesBot.Webhook/YouTube/VerifySubscriptionHandler.cs +++ b/src/src/StevesBot.Webhook/Handlers/VerifySubscriptionHandler.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Webhook.YouTube; +namespace StevesBot.Webhook.Handlers; internal static class VerifySubscriptionHandler { diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index 29155f0..c768d10 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -1,11 +1,5 @@ -using System.Text.RegularExpressions; - var builder = WebApplication.CreateBuilder(args); -builder.Services.Configure( - static options => options.ShutdownTimeout = TimeSpan.FromSeconds(30) -); - builder.Services .AddOptionsWithValidateOnStart() .BindConfiguration(nameof(SubscriptionOptions)) @@ -54,52 +48,8 @@ if (app.Environment.IsDevelopment()) app.MapOpenApi(); } -// app.UseHttpsRedirection(); - const string ytCallback = "yt-callback"; - app.MapGet(ytCallback, VerifySubscriptionHandler.HandleAsync); +app.MapPost(ytCallback, NotificationHandler.HandleAsync); -// TODO: Implement logic to do the following: -// - if is stream create discord message -app.MapPost(ytCallback, static async (HttpContext context, [FromServices] ILogger logger, [FromServices] IYouTubeDataApiClient youTubeDataApiClient) => -{ - var body = ""; - using StreamReader stream = new(context.Request.Body); - body = await stream.ReadToEndAsync(); - - var videoIdRegex = VideoIdRegex(); - var match = videoIdRegex.Match(body); - - if (match.Success is false) - { - logger.LogWarning("No video ID found in the request body."); - } - - var videoId = match.Groups[1].Value; - var video = await youTubeDataApiClient.GetVideoByIdAsync(videoId, ["liveStreamingDetails"]); - - if (video is null) - { - logger.LogWarning("Video with ID {VideoId} not found.", videoId); - return Results.NotFound(); - } - - if (video.IsLiveStream is false) - { - logger.LogInformation("Video ID {VideoId} is not a live stream.", videoId); - return Results.Ok(); - } - - logger.LogInformation("Video ID {VideoId} is a live stream.", videoId); - - return Results.Ok(); -}); - -app.Run(); - -internal partial class Program -{ - [GeneratedRegex(@"(.*?)")] - private static partial Regex VideoIdRegex(); -} \ No newline at end of file +app.Run(); \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/Usings.cs b/src/src/StevesBot.Webhook/Usings.cs index 73e000f..0346b11 100644 --- a/src/src/StevesBot.Webhook/Usings.cs +++ b/src/src/StevesBot.Webhook/Usings.cs @@ -1,10 +1,12 @@ global using System.Collections.Concurrent; global using System.ComponentModel.DataAnnotations; global using System.Text.Json.Serialization; +global using System.Text.RegularExpressions; global using Microsoft.AspNetCore.Mvc; global using Microsoft.Extensions.Options; +global using StevesBot.Webhook.Handlers; global using StevesBot.Webhook.YouTube; global using StevesBot.Webhook.YouTube.Data; global using StevesBot.Webhook.YouTube.Tasks; \ No newline at end of file From 1afd17f2d14562693cbb9646ca3d5bccf4e27e16 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 27 May 2025 23:13:19 -0500 Subject: [PATCH 08/31] feat: create discord message when stream goes live --- src/StevesBot.sln | 30 ++++++++++++++++ src/src/StevesBot.Library/.editorconfig | 3 ++ .../Discord/Common}/DiscordClientOptions.cs | 4 +-- .../Discord/Common}/DiscordMessage.cs | 6 ++-- .../Common}/DiscordMessageReference.cs | 6 ++-- .../Common/DiscordMessageReferenceTypes.cs | 7 ++++ .../Discord/Common/DiscordUser.cs | 9 +++++ .../Discord/Rest/DiscordRestClient.cs | 12 +++++-- .../Rest/DiscordRestClientException.cs | 4 +-- .../Discord/Rest/IDiscordRestClient.cs | 7 ++-- .../Rest/Requests/CreateMessageRequest.cs | 10 ++++++ .../Discord/Rest/Responses/GatewayResponse.cs | 7 ++++ .../Discord/ServicesExtensions.cs | 28 +++++++++++++++ .../StevesBot.Library.csproj | 14 ++++++++ .../Telemetry/HostExtensions.cs} | 25 +++++++++---- .../Telemetry/SeqOptions.cs | 2 +- .../Telemetry/StevesBotInstrumentation.cs | 15 ++++++++ .../Handlers/NotificationHandler.cs | 21 +++++++++-- src/src/StevesBot.Webhook/Program.cs | 17 +++++++++ .../StevesBot.Webhook.csproj | 10 +++--- src/src/StevesBot.Webhook/Usings.cs | 1 + .../StevesBot.Worker/Discord/Extensions.cs | 15 -------- .../Rest/Requests/CreateMessageRequest.cs | 6 ---- .../Discord/Rest/Responses/GatewayResponse.cs | 5 --- .../Shared/DiscordMessageReferenceTypes.cs | 7 ---- .../Discord/Shared/DiscordUser.cs | 7 ---- .../StevesBot.Worker/StevesBot.Worker.csproj | 8 +---- .../Telemetry/Instrumentation.cs | 13 ------- src/src/StevesBot.Worker/Usings.cs | 16 +++------ .../StevesBot.Library.Tests.csproj | 35 +++++++++++++++++++ .../StevesBot.Webhook.Tests/UnitTest1.cs | 10 ------ src/tests/StevesBot.Worker.Tests/Usings.cs | 7 ++-- 32 files changed, 254 insertions(+), 113 deletions(-) create mode 100644 src/src/StevesBot.Library/.editorconfig rename src/src/{StevesBot.Worker/Discord/Shared => StevesBot.Library/Discord/Common}/DiscordClientOptions.cs (63%) rename src/src/{StevesBot.Worker/Discord/Shared => StevesBot.Library/Discord/Common}/DiscordMessage.cs (78%) rename src/src/{StevesBot.Worker/Discord/Shared => StevesBot.Library/Discord/Common}/DiscordMessageReference.cs (70%) create mode 100644 src/src/StevesBot.Library/Discord/Common/DiscordMessageReferenceTypes.cs create mode 100644 src/src/StevesBot.Library/Discord/Common/DiscordUser.cs rename src/src/{StevesBot.Worker => StevesBot.Library}/Discord/Rest/DiscordRestClient.cs (87%) rename src/src/{StevesBot.Worker => StevesBot.Library}/Discord/Rest/DiscordRestClientException.cs (70%) rename src/src/{StevesBot.Worker => StevesBot.Library}/Discord/Rest/IDiscordRestClient.cs (57%) create mode 100644 src/src/StevesBot.Library/Discord/Rest/Requests/CreateMessageRequest.cs create mode 100644 src/src/StevesBot.Library/Discord/Rest/Responses/GatewayResponse.cs create mode 100644 src/src/StevesBot.Library/Discord/ServicesExtensions.cs create mode 100644 src/src/StevesBot.Library/StevesBot.Library.csproj rename src/src/{StevesBot.Worker/Telemetry/Extensions.cs => StevesBot.Library/Telemetry/HostExtensions.cs} (63%) rename src/src/{StevesBot.Worker => StevesBot.Library}/Telemetry/SeqOptions.cs (93%) create mode 100644 src/src/StevesBot.Library/Telemetry/StevesBotInstrumentation.cs delete mode 100644 src/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs delete mode 100644 src/src/StevesBot.Worker/Discord/Rest/Responses/GatewayResponse.cs delete mode 100644 src/src/StevesBot.Worker/Discord/Shared/DiscordMessageReferenceTypes.cs delete mode 100644 src/src/StevesBot.Worker/Discord/Shared/DiscordUser.cs delete mode 100644 src/src/StevesBot.Worker/Telemetry/Instrumentation.cs create mode 100644 src/tests/StevesBot.Library.Tests/StevesBot.Library.Tests.csproj delete mode 100644 src/tests/StevesBot.Webhook.Tests/UnitTest1.cs diff --git a/src/StevesBot.sln b/src/StevesBot.sln index 927b98f..5d88949 100644 --- a/src/StevesBot.sln +++ b/src/StevesBot.sln @@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StevesBot.Worker.Tests", "t EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StevesBot.Webhook.Tests", "tests\StevesBot.Webhook.Tests\StevesBot.Webhook.Tests.csproj", "{3875E45D-8400-46D9-89CE-485BADED0736}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StevesBot.Library", "src\StevesBot.Library\StevesBot.Library.csproj", "{07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StevesBot.Library.Tests", "tests\StevesBot.Library.Tests\StevesBot.Library.Tests.csproj", "{F67829A2-3600-4DD9-B324-18DDF4A0809B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,6 +77,30 @@ Global {3875E45D-8400-46D9-89CE-485BADED0736}.Release|x64.Build.0 = Release|Any CPU {3875E45D-8400-46D9-89CE-485BADED0736}.Release|x86.ActiveCfg = Release|Any CPU {3875E45D-8400-46D9-89CE-485BADED0736}.Release|x86.Build.0 = Release|Any CPU + {07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81}.Debug|x64.ActiveCfg = Debug|Any CPU + {07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81}.Debug|x64.Build.0 = Debug|Any CPU + {07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81}.Debug|x86.ActiveCfg = Debug|Any CPU + {07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81}.Debug|x86.Build.0 = Debug|Any CPU + {07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81}.Release|Any CPU.Build.0 = Release|Any CPU + {07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81}.Release|x64.ActiveCfg = Release|Any CPU + {07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81}.Release|x64.Build.0 = Release|Any CPU + {07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81}.Release|x86.ActiveCfg = Release|Any CPU + {07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81}.Release|x86.Build.0 = Release|Any CPU + {F67829A2-3600-4DD9-B324-18DDF4A0809B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F67829A2-3600-4DD9-B324-18DDF4A0809B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F67829A2-3600-4DD9-B324-18DDF4A0809B}.Debug|x64.ActiveCfg = Debug|Any CPU + {F67829A2-3600-4DD9-B324-18DDF4A0809B}.Debug|x64.Build.0 = Debug|Any CPU + {F67829A2-3600-4DD9-B324-18DDF4A0809B}.Debug|x86.ActiveCfg = Debug|Any CPU + {F67829A2-3600-4DD9-B324-18DDF4A0809B}.Debug|x86.Build.0 = Debug|Any CPU + {F67829A2-3600-4DD9-B324-18DDF4A0809B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F67829A2-3600-4DD9-B324-18DDF4A0809B}.Release|Any CPU.Build.0 = Release|Any CPU + {F67829A2-3600-4DD9-B324-18DDF4A0809B}.Release|x64.ActiveCfg = Release|Any CPU + {F67829A2-3600-4DD9-B324-18DDF4A0809B}.Release|x64.Build.0 = Release|Any CPU + {F67829A2-3600-4DD9-B324-18DDF4A0809B}.Release|x86.ActiveCfg = Release|Any CPU + {F67829A2-3600-4DD9-B324-18DDF4A0809B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -82,5 +110,7 @@ Global {7C243CA8-55A0-4D73-9FF6-F5E801B7146D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {388EAD53-CCA4-4532-8DEC-5E9177776117} = {0AB3BF05-4346-4AA6-1389-037BE0695223} {3875E45D-8400-46D9-89CE-485BADED0736} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {07B3FA35-02B7-46E6-BEDC-D64C5D0FBE81} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {F67829A2-3600-4DD9-B324-18DDF4A0809B} = {0AB3BF05-4346-4AA6-1389-037BE0695223} EndGlobalSection EndGlobal diff --git a/src/src/StevesBot.Library/.editorconfig b/src/src/StevesBot.Library/.editorconfig new file mode 100644 index 0000000..738ea45 --- /dev/null +++ b/src/src/StevesBot.Library/.editorconfig @@ -0,0 +1,3 @@ +[*.cs] +dotnet_diagnostic.CA1056.severity = none +dotnet_diagnostic.CA1054.severity = none diff --git a/src/src/StevesBot.Worker/Discord/Shared/DiscordClientOptions.cs b/src/src/StevesBot.Library/Discord/Common/DiscordClientOptions.cs similarity index 63% rename from src/src/StevesBot.Worker/Discord/Shared/DiscordClientOptions.cs rename to src/src/StevesBot.Library/Discord/Common/DiscordClientOptions.cs index 38547a0..ddfbca3 100644 --- a/src/src/StevesBot.Worker/Discord/Shared/DiscordClientOptions.cs +++ b/src/src/StevesBot.Library/Discord/Common/DiscordClientOptions.cs @@ -1,6 +1,6 @@ -namespace StevesBot.Worker.Discord.Shared; +namespace StevesBot.Library.Discord.Common; -internal sealed class DiscordClientOptions +public sealed class DiscordClientOptions { public string ApiUrl { get; init; } = string.Empty; public string AppToken { get; init; } = string.Empty; diff --git a/src/src/StevesBot.Worker/Discord/Shared/DiscordMessage.cs b/src/src/StevesBot.Library/Discord/Common/DiscordMessage.cs similarity index 78% rename from src/src/StevesBot.Worker/Discord/Shared/DiscordMessage.cs rename to src/src/StevesBot.Library/Discord/Common/DiscordMessage.cs index e29088f..bf286ef 100644 --- a/src/src/StevesBot.Worker/Discord/Shared/DiscordMessage.cs +++ b/src/src/StevesBot.Library/Discord/Common/DiscordMessage.cs @@ -1,6 +1,8 @@ -namespace StevesBot.Worker.Discord.Shared; +using System.Text.Json.Serialization; -internal sealed record DiscordMessage +namespace StevesBot.Library.Discord.Common; + +public sealed record DiscordMessage { [JsonPropertyName("id")] public string Id { get; init; } = string.Empty; diff --git a/src/src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs b/src/src/StevesBot.Library/Discord/Common/DiscordMessageReference.cs similarity index 70% rename from src/src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs rename to src/src/StevesBot.Library/Discord/Common/DiscordMessageReference.cs index a950137..a2c9416 100644 --- a/src/src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs +++ b/src/src/StevesBot.Library/Discord/Common/DiscordMessageReference.cs @@ -1,6 +1,8 @@ -namespace StevesBot.Worker.Discord.Shared; +using System.Text.Json.Serialization; -internal sealed record DiscordMessageReference( +namespace StevesBot.Library.Discord.Common; + +public sealed record DiscordMessageReference( [property: JsonPropertyName("type")] int Type, [property: JsonPropertyName("message_id")] string MessageId, [property: JsonPropertyName("channel_id")] string ChannelId, diff --git a/src/src/StevesBot.Library/Discord/Common/DiscordMessageReferenceTypes.cs b/src/src/StevesBot.Library/Discord/Common/DiscordMessageReferenceTypes.cs new file mode 100644 index 0000000..7c66727 --- /dev/null +++ b/src/src/StevesBot.Library/Discord/Common/DiscordMessageReferenceTypes.cs @@ -0,0 +1,7 @@ +namespace StevesBot.Library.Discord.Common; + +public static class DiscordMessageReferenceTypes +{ + public const int Default = 0; + public const int Forward = 1; +} \ No newline at end of file diff --git a/src/src/StevesBot.Library/Discord/Common/DiscordUser.cs b/src/src/StevesBot.Library/Discord/Common/DiscordUser.cs new file mode 100644 index 0000000..13a8393 --- /dev/null +++ b/src/src/StevesBot.Library/Discord/Common/DiscordUser.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace StevesBot.Library.Discord.Common; + +public sealed record DiscordUser +{ + [JsonPropertyName("id")] + public string Id { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs b/src/src/StevesBot.Library/Discord/Rest/DiscordRestClient.cs similarity index 87% rename from src/src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs rename to src/src/StevesBot.Library/Discord/Rest/DiscordRestClient.cs index 45b2ecd..4e251e6 100644 --- a/src/src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs +++ b/src/src/StevesBot.Library/Discord/Rest/DiscordRestClient.cs @@ -1,6 +1,14 @@ -namespace StevesBot.Worker.Discord.Rest; +using System.Net.Http.Json; -internal sealed class DiscordRestClient : IDiscordRestClient +using Microsoft.Extensions.Logging; + +using StevesBot.Library.Discord.Rest.Requests; +using StevesBot.Library.Discord.Rest.Responses; +using StevesBot.Library.Discord.Common; + +namespace StevesBot.Library.Discord.Rest; + +public sealed class DiscordRestClient : IDiscordRestClient { private readonly ILogger _logger; private readonly HttpClient _httpClient; diff --git a/src/src/StevesBot.Worker/Discord/Rest/DiscordRestClientException.cs b/src/src/StevesBot.Library/Discord/Rest/DiscordRestClientException.cs similarity index 70% rename from src/src/StevesBot.Worker/Discord/Rest/DiscordRestClientException.cs rename to src/src/StevesBot.Library/Discord/Rest/DiscordRestClientException.cs index 9e8e4b5..6f27517 100644 --- a/src/src/StevesBot.Worker/Discord/Rest/DiscordRestClientException.cs +++ b/src/src/StevesBot.Library/Discord/Rest/DiscordRestClientException.cs @@ -1,6 +1,6 @@ -namespace StevesBot.Worker.Discord.Rest; +namespace StevesBot.Library.Discord.Rest; -internal sealed class DiscordRestClientException : Exception +public sealed class DiscordRestClientException : Exception { public DiscordRestClientException() { diff --git a/src/src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs b/src/src/StevesBot.Library/Discord/Rest/IDiscordRestClient.cs similarity index 57% rename from src/src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs rename to src/src/StevesBot.Library/Discord/Rest/IDiscordRestClient.cs index a1f1f0b..dd057ef 100644 --- a/src/src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs +++ b/src/src/StevesBot.Library/Discord/Rest/IDiscordRestClient.cs @@ -1,6 +1,9 @@ -namespace StevesBot.Worker.Discord.Rest; +using StevesBot.Library.Discord.Rest.Requests; +using StevesBot.Library.Discord.Common; -internal interface IDiscordRestClient +namespace StevesBot.Library.Discord.Rest; + +public interface IDiscordRestClient { Task GetGatewayUrlAsync(CancellationToken cancellationToken = default); Task CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken = default); diff --git a/src/src/StevesBot.Library/Discord/Rest/Requests/CreateMessageRequest.cs b/src/src/StevesBot.Library/Discord/Rest/Requests/CreateMessageRequest.cs new file mode 100644 index 0000000..abe91a6 --- /dev/null +++ b/src/src/StevesBot.Library/Discord/Rest/Requests/CreateMessageRequest.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +using StevesBot.Library.Discord.Common; + +namespace StevesBot.Library.Discord.Rest.Requests; + +public sealed record CreateMessageRequest( + [property: JsonPropertyName("content")] string Content, + [property: JsonPropertyName("message_reference")] DiscordMessageReference? MessageReference +); \ No newline at end of file diff --git a/src/src/StevesBot.Library/Discord/Rest/Responses/GatewayResponse.cs b/src/src/StevesBot.Library/Discord/Rest/Responses/GatewayResponse.cs new file mode 100644 index 0000000..cc810d3 --- /dev/null +++ b/src/src/StevesBot.Library/Discord/Rest/Responses/GatewayResponse.cs @@ -0,0 +1,7 @@ +using System.Text.Json.Serialization; + +namespace StevesBot.Library.Discord.Rest.Responses; + +public sealed record GatewayResponse( + [property: JsonPropertyName("url")] string Url +); \ No newline at end of file diff --git a/src/src/StevesBot.Library/Discord/ServicesExtensions.cs b/src/src/StevesBot.Library/Discord/ServicesExtensions.cs new file mode 100644 index 0000000..d8089e7 --- /dev/null +++ b/src/src/StevesBot.Library/Discord/ServicesExtensions.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.DependencyInjection; + +using StevesBot.Library.Discord.Common; +using StevesBot.Library.Discord.Rest; +using StevesBot.Library.Telemetry; + +namespace StevesBot.Library.Discord; + +public static class ServicesExtensions +{ + public static IServiceCollection AddDiscordRestClient(this IServiceCollection services) + { + services + .AddHttpClient(static (sp, c) => + { + var discordOptions = sp.GetRequiredService(); + c.BaseAddress = new Uri(discordOptions.ApiUrl); + c.DefaultRequestHeaders.Authorization = new("Bot", discordOptions.AppToken); + + var userAgentString = $"DiscordBot (https://github.com/StevanFreeborn/steves-bot, {StevesBotInstrumentation.SourceVersion})"; + c.DefaultRequestHeaders.Add("User-Agent", userAgentString); + }) + .AddStandardResilienceHandler(); + + return services; + } + +} \ No newline at end of file diff --git a/src/src/StevesBot.Library/StevesBot.Library.csproj b/src/src/StevesBot.Library/StevesBot.Library.csproj new file mode 100644 index 0000000..fc1f3b1 --- /dev/null +++ b/src/src/StevesBot.Library/StevesBot.Library.csproj @@ -0,0 +1,14 @@ +ο»Ώ + + + + + + + + + + + + + diff --git a/src/src/StevesBot.Worker/Telemetry/Extensions.cs b/src/src/StevesBot.Library/Telemetry/HostExtensions.cs similarity index 63% rename from src/src/StevesBot.Worker/Telemetry/Extensions.cs rename to src/src/StevesBot.Library/Telemetry/HostExtensions.cs index 0e264a1..8d88139 100644 --- a/src/src/StevesBot.Worker/Telemetry/Extensions.cs +++ b/src/src/StevesBot.Library/Telemetry/HostExtensions.cs @@ -1,10 +1,21 @@ -namespace StevesBot.Worker.Telemetry; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; -internal static class Extensions +using OpenTelemetry.Exporter; +using OpenTelemetry.Logs; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace StevesBot.Library.Telemetry; + +public static class HostExtensions { public static HostApplicationBuilder AddTelemetry(this HostApplicationBuilder builder) { - builder.Services.AddSingleton(); + ArgumentNullException.ThrowIfNull(builder); + + builder.Services.AddSingleton(); var seq = new SeqOptions(); builder.Configuration.GetSection(nameof(SeqOptions)).Bind(seq); @@ -17,11 +28,11 @@ internal static class Extensions builder.Services.AddOpenTelemetry() .ConfigureResource(resource => { - resource.AddService(Instrumentation.SourceName, Instrumentation.SourceVersion); + resource.AddService(StevesBotInstrumentation.SourceName, StevesBotInstrumentation.SourceVersion); resource.AddAttributes(new Dictionary { - ["service.name"] = Instrumentation.SourceName, - ["service.version"] = Instrumentation.SourceVersion, + ["service.name"] = StevesBotInstrumentation.SourceName, + ["service.version"] = StevesBotInstrumentation.SourceVersion, ["service.instance.id"] = Environment.MachineName, ["service.namespace"] = "stevesbot", ["service.environment"] = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "production", @@ -38,7 +49,7 @@ internal static class Extensions }) .WithTracing(tb => { - tb.AddSource(Instrumentation.SourceName); + tb.AddSource(StevesBotInstrumentation.SourceName); tb.AddAspNetCoreInstrumentation(); tb.AddHttpClientInstrumentation(); tb.AddOtlpExporter(o => diff --git a/src/src/StevesBot.Worker/Telemetry/SeqOptions.cs b/src/src/StevesBot.Library/Telemetry/SeqOptions.cs similarity index 93% rename from src/src/StevesBot.Worker/Telemetry/SeqOptions.cs rename to src/src/StevesBot.Library/Telemetry/SeqOptions.cs index cf5b801..8c2ea69 100644 --- a/src/src/StevesBot.Worker/Telemetry/SeqOptions.cs +++ b/src/src/StevesBot.Library/Telemetry/SeqOptions.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Telemetry; +namespace StevesBot.Library.Telemetry; internal sealed class SeqOptions { diff --git a/src/src/StevesBot.Library/Telemetry/StevesBotInstrumentation.cs b/src/src/StevesBot.Library/Telemetry/StevesBotInstrumentation.cs new file mode 100644 index 0000000..f08b327 --- /dev/null +++ b/src/src/StevesBot.Library/Telemetry/StevesBotInstrumentation.cs @@ -0,0 +1,15 @@ +using System.Diagnostics; + +namespace StevesBot.Library.Telemetry; + +public sealed class StevesBotInstrumentation : IDisposable +{ + public const string SourceName = "StevesBot.Worker"; + public const string SourceVersion = "0.0.0"; + public ActivitySource Source { get; } = new(SourceName, SourceVersion); + + public void Dispose() + { + Source.Dispose(); + } +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/Handlers/NotificationHandler.cs b/src/src/StevesBot.Webhook/Handlers/NotificationHandler.cs index ba1cb5a..00a86e4 100644 --- a/src/src/StevesBot.Webhook/Handlers/NotificationHandler.cs +++ b/src/src/StevesBot.Webhook/Handlers/NotificationHandler.cs @@ -1,3 +1,6 @@ +using StevesBot.Library.Discord.Rest; +using StevesBot.Library.Discord.Rest.Requests; + namespace StevesBot.Webhook.Handlers; // TODO: Implement logic to do the following: @@ -7,11 +10,13 @@ internal static class NotificationHandler public static async Task HandleAsync( HttpContext context, [FromServices] ILogger logger, - [FromServices] IYouTubeDataApiClient youTubeDataApiClient + [FromServices] IYouTubeDataApiClient youTubeDataApiClient, + [FromServices] IDiscordRestClient discordRestClient, + CancellationToken cancellationToken ) { using StreamReader stream = new(context.Request.Body); - var body = await stream.ReadToEndAsync(); + var body = await stream.ReadToEndAsync(cancellationToken); var videoIdRegex = VideoIdRegex.Regex(); var match = videoIdRegex.Match(body); @@ -24,7 +29,7 @@ internal static class NotificationHandler var videoId = match.Groups[1].Value; var parts = new string[] { "liveStreamingDetails", "snippet" }; - var video = await youTubeDataApiClient.GetVideoByIdAsync(videoId, parts); + var video = await youTubeDataApiClient.GetVideoByIdAsync(videoId, parts, cancellationToken); if (video is null) { @@ -40,6 +45,16 @@ internal static class NotificationHandler logger.LogInformation("Video ID {VideoId} is a live stream.", videoId); + await discordRestClient.CreateMessageAsync( + channelId: "1372680179121524778", + request: new CreateMessageRequest( + Content: "@everyone Stevan is trying to be a streamer again! " + + $"Check out the stream here: https://www.youtube.com/watch?v={videoId}", + MessageReference: null + ), + cancellationToken: cancellationToken + ); + return Results.Ok(); } } diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index c768d10..9fbc310 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -1,3 +1,5 @@ +using StevesBot.Library.Discord.Common; + var builder = WebApplication.CreateBuilder(args); builder.Services @@ -41,6 +43,21 @@ builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSingleton>(); builder.Services.AddHostedService(); +// TODO: This probably should be +// moved into the AddDiscordRestClient extension method. +// and we should probably just take the dependency +// on IOptions in DiscordRestClient +builder.Services.AddOptions() + .BindConfiguration(nameof(DiscordClientOptions)); + +builder.Services.AddSingleton(static sp => +{ + var options = sp.GetRequiredService>().Value; + return options; +}); + +builder.Services.AddDiscordRestClient(); + var app = builder.Build(); if (app.Environment.IsDevelopment()) diff --git a/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj b/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj index b26a484..79c3094 100644 --- a/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj +++ b/src/src/StevesBot.Webhook/StevesBot.Webhook.csproj @@ -1,15 +1,13 @@ - - net9.0 - enable - enable - - + + + + diff --git a/src/src/StevesBot.Webhook/Usings.cs b/src/src/StevesBot.Webhook/Usings.cs index 0346b11..5f3aeea 100644 --- a/src/src/StevesBot.Webhook/Usings.cs +++ b/src/src/StevesBot.Webhook/Usings.cs @@ -6,6 +6,7 @@ global using System.Text.RegularExpressions; global using Microsoft.AspNetCore.Mvc; global using Microsoft.Extensions.Options; +global using StevesBot.Library.Discord; global using StevesBot.Webhook.Handlers; global using StevesBot.Webhook.YouTube; global using StevesBot.Webhook.YouTube.Data; diff --git a/src/src/StevesBot.Worker/Discord/Extensions.cs b/src/src/StevesBot.Worker/Discord/Extensions.cs index 5271625..0bf3be6 100644 --- a/src/src/StevesBot.Worker/Discord/Extensions.cs +++ b/src/src/StevesBot.Worker/Discord/Extensions.cs @@ -2,21 +2,6 @@ namespace StevesBot.Worker.Discord; internal static class Extensions { - public static IServiceCollection AddDiscordRestClient(this IServiceCollection services) - { - services - .AddHttpClient(static (sp, c) => - { - var discordOptions = sp.GetRequiredService(); - c.BaseAddress = new Uri(discordOptions.ApiUrl); - c.DefaultRequestHeaders.Authorization = new("Bot", discordOptions.AppToken); - c.DefaultRequestHeaders.Add("User-Agent", $"DiscordBot (https://github.com/StevanFreeborn/steves-bot, {Instrumentation.SourceVersion})"); - }) - .AddStandardResilienceHandler(); - - return services; - } - public static IServiceCollection AddDiscordGatewayClient(this IServiceCollection services, Action? configure = null) { services.AddSingleton(sp => diff --git a/src/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs b/src/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs deleted file mode 100644 index cf96e12..0000000 --- a/src/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace StevesBot.Worker.Discord.Rest.Requests; - -internal sealed record CreateMessageRequest( - [property: JsonPropertyName("content")] string Content, - [property: JsonPropertyName("message_reference")] DiscordMessageReference? MessageReference -); \ No newline at end of file diff --git a/src/src/StevesBot.Worker/Discord/Rest/Responses/GatewayResponse.cs b/src/src/StevesBot.Worker/Discord/Rest/Responses/GatewayResponse.cs deleted file mode 100644 index e8f7a69..0000000 --- a/src/src/StevesBot.Worker/Discord/Rest/Responses/GatewayResponse.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace StevesBot.Worker.Discord.Rest.Responses; - -internal sealed record GatewayResponse( - [property: JsonPropertyName("url")] string Url -); \ No newline at end of file diff --git a/src/src/StevesBot.Worker/Discord/Shared/DiscordMessageReferenceTypes.cs b/src/src/StevesBot.Worker/Discord/Shared/DiscordMessageReferenceTypes.cs deleted file mode 100644 index 8d43052..0000000 --- a/src/src/StevesBot.Worker/Discord/Shared/DiscordMessageReferenceTypes.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StevesBot.Worker.Discord.Shared; - -internal static class DiscordMessageReferenceTypes -{ - public const int Default = 0; - public const int Forward = 1; -} \ No newline at end of file diff --git a/src/src/StevesBot.Worker/Discord/Shared/DiscordUser.cs b/src/src/StevesBot.Worker/Discord/Shared/DiscordUser.cs deleted file mode 100644 index eba9acf..0000000 --- a/src/src/StevesBot.Worker/Discord/Shared/DiscordUser.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StevesBot.Worker.Discord.Shared; - -internal sealed record DiscordUser -{ - [JsonPropertyName("id")] - public string Id { get; init; } = string.Empty; -} \ No newline at end of file diff --git a/src/src/StevesBot.Worker/StevesBot.Worker.csproj b/src/src/StevesBot.Worker/StevesBot.Worker.csproj index f66917c..026278e 100644 --- a/src/src/StevesBot.Worker/StevesBot.Worker.csproj +++ b/src/src/StevesBot.Worker/StevesBot.Worker.csproj @@ -8,15 +8,9 @@ - - - - - - - + diff --git a/src/src/StevesBot.Worker/Telemetry/Instrumentation.cs b/src/src/StevesBot.Worker/Telemetry/Instrumentation.cs deleted file mode 100644 index 9843dc0..0000000 --- a/src/src/StevesBot.Worker/Telemetry/Instrumentation.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace StevesBot.Worker.Telemetry; - -internal sealed class Instrumentation : IDisposable -{ - public const string SourceName = "StevesBot.Worker"; - public const string SourceVersion = "0.0.0"; - public readonly ActivitySource Source = new(SourceName, SourceVersion); - - public void Dispose() - { - Source.Dispose(); - } -} \ No newline at end of file diff --git a/src/src/StevesBot.Worker/Usings.cs b/src/src/StevesBot.Worker/Usings.cs index 2466a9e..11b7725 100644 --- a/src/src/StevesBot.Worker/Usings.cs +++ b/src/src/StevesBot.Worker/Usings.cs @@ -1,5 +1,3 @@ -global using System.Diagnostics; -global using System.Net.Http.Json; global using System.Net.WebSockets; global using System.Reflection; global using System.Text; @@ -8,21 +6,17 @@ global using System.Text.Json.Serialization; global using Microsoft.Extensions.Options; -global using OpenTelemetry.Exporter; -global using OpenTelemetry.Logs; -global using OpenTelemetry.Resources; -global using OpenTelemetry.Trace; +global using StevesBot.Library.Discord; +global using StevesBot.Library.Telemetry; global using StevesBot.Worker; global using StevesBot.Worker.Discord; global using StevesBot.Worker.Discord.Gateway; global using StevesBot.Worker.Discord.Gateway.Events; global using StevesBot.Worker.Discord.Gateway.Events.Data; -global using StevesBot.Worker.Discord.Rest; -global using StevesBot.Worker.Discord.Rest.Requests; -global using StevesBot.Worker.Discord.Rest.Responses; -global using StevesBot.Worker.Discord.Shared; +global using StevesBot.Library.Discord.Rest; +global using StevesBot.Library.Discord.Rest.Requests; +global using StevesBot.Library.Discord.Common; global using StevesBot.Worker.Handlers; -global using StevesBot.Worker.Telemetry; global using StevesBot.Worker.Threading; global using StevesBot.Worker.WebSockets; \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/StevesBot.Library.Tests.csproj b/src/tests/StevesBot.Library.Tests/StevesBot.Library.Tests.csproj new file mode 100644 index 0000000..7694b4b --- /dev/null +++ b/src/tests/StevesBot.Library.Tests/StevesBot.Library.Tests.csproj @@ -0,0 +1,35 @@ +ο»Ώ + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + true + ./TestResults/Coverage/ + cobertura + [StevesBot.Lib]* + + + + + + + diff --git a/src/tests/StevesBot.Webhook.Tests/UnitTest1.cs b/src/tests/StevesBot.Webhook.Tests/UnitTest1.cs deleted file mode 100644 index 33fdda7..0000000 --- a/src/tests/StevesBot.Webhook.Tests/UnitTest1.cs +++ /dev/null @@ -1,10 +0,0 @@ -ο»Ώnamespace StevesBot.Webhook.Tests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - - } -} \ No newline at end of file diff --git a/src/tests/StevesBot.Worker.Tests/Usings.cs b/src/tests/StevesBot.Worker.Tests/Usings.cs index 3e07822..eeafbcc 100644 --- a/src/tests/StevesBot.Worker.Tests/Usings.cs +++ b/src/tests/StevesBot.Worker.Tests/Usings.cs @@ -16,13 +16,14 @@ global using Moq; global using RichardSzalay.MockHttp; +global using StevesBot.Library.Discord; +global using StevesBot.Library.Discord.Common; +global using StevesBot.Library.Discord.Rest; +global using StevesBot.Library.Discord.Rest.Requests; global using StevesBot.Worker.Discord; global using StevesBot.Worker.Discord.Gateway; global using StevesBot.Worker.Discord.Gateway.Events; global using StevesBot.Worker.Discord.Gateway.Events.Data; -global using StevesBot.Worker.Discord.Rest; -global using StevesBot.Worker.Discord.Rest.Requests; -global using StevesBot.Worker.Discord.Shared; global using StevesBot.Worker.Tests.Integration.Infrastructure; global using StevesBot.Worker.Threading; global using StevesBot.Worker.WebSockets; \ No newline at end of file From 2ddc472f46dc3736715c3c9b5b7e6241f8606547 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 28 May 2025 17:14:55 -0500 Subject: [PATCH 09/31] feat: add Discord notification options and handlers for YouTube events --- .../YouTube/DiscordNotificationOptions.cs | 10 ++++++++ .../Handlers/NotificationHandler.cs | 25 +++++++++++-------- .../Handlers/VerifySubscriptionHandler.cs | 2 +- .../appsettings.Example.json | 9 +++++++ 4 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 src/src/StevesBot.Webhook/YouTube/DiscordNotificationOptions.cs rename src/src/StevesBot.Webhook/{ => YouTube}/Handlers/NotificationHandler.cs (73%) rename src/src/StevesBot.Webhook/{ => YouTube}/Handlers/VerifySubscriptionHandler.cs (97%) diff --git a/src/src/StevesBot.Webhook/YouTube/DiscordNotificationOptions.cs b/src/src/StevesBot.Webhook/YouTube/DiscordNotificationOptions.cs new file mode 100644 index 0000000..b373411 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/DiscordNotificationOptions.cs @@ -0,0 +1,10 @@ +namespace StevesBot.Webhook.YouTube; + +internal sealed record DiscordNotificationOptions +{ + [Required] + public string ChannelId { get; init; } = string.Empty; + + [Required] + public string MessageFormat { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/Handlers/NotificationHandler.cs b/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs similarity index 73% rename from src/src/StevesBot.Webhook/Handlers/NotificationHandler.cs rename to src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs index 00a86e4..4226558 100644 --- a/src/src/StevesBot.Webhook/Handlers/NotificationHandler.cs +++ b/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs @@ -1,10 +1,5 @@ -using StevesBot.Library.Discord.Rest; -using StevesBot.Library.Discord.Rest.Requests; +namespace StevesBot.Webhook.YouTube.Handlers; -namespace StevesBot.Webhook.Handlers; - -// TODO: Implement logic to do the following: -// - if is stream create discord message internal static class NotificationHandler { public static async Task HandleAsync( @@ -12,6 +7,7 @@ internal static class NotificationHandler [FromServices] ILogger logger, [FromServices] IYouTubeDataApiClient youTubeDataApiClient, [FromServices] IDiscordRestClient discordRestClient, + [FromServices] IOptionsMonitor discordNotificationOptions, CancellationToken cancellationToken ) { @@ -43,13 +39,22 @@ internal static class NotificationHandler return Results.Ok(); } - logger.LogInformation("Video ID {VideoId} is a live stream.", videoId); + logger.LogInformation( + "Video ID {VideoId} is a live stream. Creating discord message in channel {ChannelId}", + videoId, + discordNotificationOptions.CurrentValue.ChannelId + ); + + var msg = string.Format( + CultureInfo.InvariantCulture, + discordNotificationOptions.CurrentValue.MessageFormat, + videoId + ); await discordRestClient.CreateMessageAsync( - channelId: "1372680179121524778", + channelId: discordNotificationOptions.CurrentValue.ChannelId, request: new CreateMessageRequest( - Content: "@everyone Stevan is trying to be a streamer again! " + - $"Check out the stream here: https://www.youtube.com/watch?v={videoId}", + Content: msg, MessageReference: null ), cancellationToken: cancellationToken diff --git a/src/src/StevesBot.Webhook/Handlers/VerifySubscriptionHandler.cs b/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs similarity index 97% rename from src/src/StevesBot.Webhook/Handlers/VerifySubscriptionHandler.cs rename to src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs index 14282bf..a334b25 100644 --- a/src/src/StevesBot.Webhook/Handlers/VerifySubscriptionHandler.cs +++ b/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Webhook.Handlers; +namespace StevesBot.Webhook.YouTube.Handlers; internal static class VerifySubscriptionHandler { diff --git a/src/src/StevesBot.Webhook/appsettings.Example.json b/src/src/StevesBot.Webhook/appsettings.Example.json index 56defff..db3a004 100644 --- a/src/src/StevesBot.Webhook/appsettings.Example.json +++ b/src/src/StevesBot.Webhook/appsettings.Example.json @@ -16,5 +16,14 @@ }, "PubSubClientOptions": { "BaseUrl": "BaseUrl" + }, + "DiscordClientOptions": { + "ApiUrl": "ApiUrl", + "AppToken": "AppToken", + "Intents": 0 + }, + "DiscordNotificationOptions": { + "ChannelId": "ChannelId", + "MessageFormat": "MessageFormat" } } From 113cdf7a82d1ede5868b1242279f516397b917fb Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 28 May 2025 17:15:26 -0500 Subject: [PATCH 10/31] refactor: clean up usings --- src/src/StevesBot.Webhook/Program.cs | 19 ++++++++----------- src/src/StevesBot.Webhook/Usings.cs | 7 ++++++- .../YouTube/Data/YouTubeDataApiClient.cs | 2 -- .../Data/YouTubeLiveStreamingDetails.cs | 2 -- .../YouTube/Data/YouTubePageInfo.cs | 2 -- .../YouTube/Data/YouTubeVideoListResponse.cs | 2 -- 6 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index 9fbc310..b585858 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -1,5 +1,3 @@ -using StevesBot.Library.Discord.Common; - var builder = WebApplication.CreateBuilder(args); builder.Services @@ -43,18 +41,16 @@ builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSingleton>(); builder.Services.AddHostedService(); -// TODO: This probably should be -// moved into the AddDiscordRestClient extension method. -// and we should probably just take the dependency -// on IOptions in DiscordRestClient +builder.Services.AddOptionsWithValidateOnStart() + .BindConfiguration(nameof(DiscordNotificationOptions)) + .ValidateDataAnnotations(); + builder.Services.AddOptions() .BindConfiguration(nameof(DiscordClientOptions)); -builder.Services.AddSingleton(static sp => -{ - var options = sp.GetRequiredService>().Value; - return options; -}); +builder.Services.AddSingleton( + static sp => sp.GetRequiredService>().Value +); builder.Services.AddDiscordRestClient(); @@ -66,6 +62,7 @@ if (app.Environment.IsDevelopment()) } const string ytCallback = "yt-callback"; + app.MapGet(ytCallback, VerifySubscriptionHandler.HandleAsync); app.MapPost(ytCallback, NotificationHandler.HandleAsync); diff --git a/src/src/StevesBot.Webhook/Usings.cs b/src/src/StevesBot.Webhook/Usings.cs index 5f3aeea..5bd5405 100644 --- a/src/src/StevesBot.Webhook/Usings.cs +++ b/src/src/StevesBot.Webhook/Usings.cs @@ -1,13 +1,18 @@ global using System.Collections.Concurrent; global using System.ComponentModel.DataAnnotations; +global using System.Globalization; global using System.Text.Json.Serialization; global using System.Text.RegularExpressions; global using Microsoft.AspNetCore.Mvc; +global using Microsoft.AspNetCore.WebUtilities; global using Microsoft.Extensions.Options; global using StevesBot.Library.Discord; -global using StevesBot.Webhook.Handlers; +global using StevesBot.Library.Discord.Common; +global using StevesBot.Library.Discord.Rest; +global using StevesBot.Library.Discord.Rest.Requests; global using StevesBot.Webhook.YouTube; global using StevesBot.Webhook.YouTube.Data; +global using StevesBot.Webhook.YouTube.Handlers; global using StevesBot.Webhook.YouTube.Tasks; \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/Data/YouTubeDataApiClient.cs b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeDataApiClient.cs index dad7469..78ee387 100644 --- a/src/src/StevesBot.Webhook/YouTube/Data/YouTubeDataApiClient.cs +++ b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeDataApiClient.cs @@ -1,5 +1,3 @@ -using Microsoft.AspNetCore.WebUtilities; - namespace StevesBot.Webhook.YouTube.Data; internal sealed class YouTubeDataApiClient( diff --git a/src/src/StevesBot.Webhook/YouTube/Data/YouTubeLiveStreamingDetails.cs b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeLiveStreamingDetails.cs index ffc8717..b4781a1 100644 --- a/src/src/StevesBot.Webhook/YouTube/Data/YouTubeLiveStreamingDetails.cs +++ b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeLiveStreamingDetails.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace StevesBot.Webhook.YouTube.Data; internal sealed record YouTubeLiveStreamingDetails diff --git a/src/src/StevesBot.Webhook/YouTube/Data/YouTubePageInfo.cs b/src/src/StevesBot.Webhook/YouTube/Data/YouTubePageInfo.cs index ea51800..717ee98 100644 --- a/src/src/StevesBot.Webhook/YouTube/Data/YouTubePageInfo.cs +++ b/src/src/StevesBot.Webhook/YouTube/Data/YouTubePageInfo.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace StevesBot.Webhook.YouTube.Data; internal sealed record YouTubePageInfo diff --git a/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideoListResponse.cs b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideoListResponse.cs index 04d744e..2c259f7 100644 --- a/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideoListResponse.cs +++ b/src/src/StevesBot.Webhook/YouTube/Data/YouTubeVideoListResponse.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace StevesBot.Webhook.YouTube.Data; internal sealed record YouTubeVideoListResponse From 21adfcd3bbd4df5027645323be522e1ee9d759d8 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 28 May 2025 17:15:38 -0500 Subject: [PATCH 11/31] tests: begin moving tests to lib test project --- .../Unit/ServicesExtensionsTests.cs | 28 +++++++++++++++++++ src/tests/StevesBot.Library.Tests/Usings.cs | 5 ++++ .../Unit/ExtensionsTests.cs | 12 -------- 3 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 src/tests/StevesBot.Library.Tests/Unit/ServicesExtensionsTests.cs create mode 100644 src/tests/StevesBot.Library.Tests/Usings.cs diff --git a/src/tests/StevesBot.Library.Tests/Unit/ServicesExtensionsTests.cs b/src/tests/StevesBot.Library.Tests/Unit/ServicesExtensionsTests.cs new file mode 100644 index 0000000..0737a77 --- /dev/null +++ b/src/tests/StevesBot.Library.Tests/Unit/ServicesExtensionsTests.cs @@ -0,0 +1,28 @@ +namespace StevesBot.Library.Tests.Unit; + +public class ServicesExtensionsTests +{ + private readonly DiscordClientOptions _discordClientOptions = new() + { + ApiUrl = "https://test.com", + AppToken = "test_token", + }; + private readonly ServiceCollection _services = new(); + + public ServicesExtensionsTests() + { + _services.AddSingleton(_discordClientOptions); + } + + [Fact] + public void AddDiscordRestClient_WhenCalled_ItShouldAddDiscordRestClient() + { + _services.AddDiscordRestClient(); + + var act = () => _services + .BuildServiceProvider() + .GetRequiredService(); + + act.Should().NotThrow(); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/Usings.cs b/src/tests/StevesBot.Library.Tests/Usings.cs new file mode 100644 index 0000000..99b9928 --- /dev/null +++ b/src/tests/StevesBot.Library.Tests/Usings.cs @@ -0,0 +1,5 @@ +global using Microsoft.Extensions.DependencyInjection; + +global using StevesBot.Library.Discord; +global using StevesBot.Library.Discord.Common; +global using StevesBot.Library.Discord.Rest; \ No newline at end of file diff --git a/src/tests/StevesBot.Worker.Tests/Unit/ExtensionsTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/ExtensionsTests.cs index 210d5dc..e017584 100644 --- a/src/tests/StevesBot.Worker.Tests/Unit/ExtensionsTests.cs +++ b/src/tests/StevesBot.Worker.Tests/Unit/ExtensionsTests.cs @@ -24,18 +24,6 @@ public class ExtensionsTests _services.AddSingleton(_mockScopeFactory.Object); } - [Fact] - public void AddDiscordRestClient_WhenCalled_ItShouldAddDiscordRestClient() - { - _services.AddDiscordRestClient(); - - var act = () => _services - .BuildServiceProvider() - .GetRequiredService(); - - act.Should().NotThrow(); - } - [Fact] public void AddDiscordGatewayClient_WhenCalled_ItShouldAddDiscordGatewayClient() { From 887b95f79853f29e590a9f770136c9a9125586ab Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 28 May 2025 21:43:57 -0500 Subject: [PATCH 12/31] fix: prevent duplicate stream notifications - add store that is singleton to keep track of last posted stream and prevent posting twice for same stream --- src/src/StevesBot.Webhook/Program.cs | 2 ++ .../YouTube/Handlers/NotificationHandler.cs | 9 +++++++++ .../YouTube/LastPostedStreamStore.cs | 11 +++++++++++ 3 files changed, 22 insertions(+) create mode 100644 src/src/StevesBot.Webhook/YouTube/LastPostedStreamStore.cs diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index b585858..4bed4fb 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -41,6 +41,8 @@ builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSingleton>(); builder.Services.AddHostedService(); +builder.Services.AddSingleton(); + builder.Services.AddOptionsWithValidateOnStart() .BindConfiguration(nameof(DiscordNotificationOptions)) .ValidateDataAnnotations(); diff --git a/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs b/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs index 4226558..c75d2fc 100644 --- a/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs +++ b/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs @@ -8,6 +8,7 @@ internal static class NotificationHandler [FromServices] IYouTubeDataApiClient youTubeDataApiClient, [FromServices] IDiscordRestClient discordRestClient, [FromServices] IOptionsMonitor discordNotificationOptions, + [FromServices] LastPostedStreamStore lastPostedStream, CancellationToken cancellationToken ) { @@ -39,6 +40,14 @@ internal static class NotificationHandler return Results.Ok(); } + if (lastPostedStream.Value == videoId) + { + logger.LogInformation("Video ID {VideoId} has already been posted. Skipping notification.", videoId); + return Results.Ok(); + } + + lastPostedStream.SetValue(videoId); + logger.LogInformation( "Video ID {VideoId} is a live stream. Creating discord message in channel {ChannelId}", videoId, diff --git a/src/src/StevesBot.Webhook/YouTube/LastPostedStreamStore.cs b/src/src/StevesBot.Webhook/YouTube/LastPostedStreamStore.cs new file mode 100644 index 0000000..7030e9f --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/LastPostedStreamStore.cs @@ -0,0 +1,11 @@ +namespace StevesBot.Webhook.YouTube; + +internal class LastPostedStreamStore +{ + public string Value { get; private set; } = string.Empty; + + public void SetValue(string value) + { + Value = value; + } +} \ No newline at end of file From 6479531126ae9b5963dfb336a1148194342d1c84 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 28 May 2025 21:59:50 -0500 Subject: [PATCH 13/31] refactor: create interface for store --- src/src/StevesBot.Webhook/Program.cs | 2 +- .../YouTube/Handlers/NotificationHandler.cs | 2 +- .../YouTube/ILastPostedStreamStore.cs | 5 +++++ src/src/StevesBot.Webhook/YouTube/IStore.cs | 7 +++++++ .../YouTube/LastPostedStreamStore.cs | 11 ++++++++--- 5 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 src/src/StevesBot.Webhook/YouTube/ILastPostedStreamStore.cs create mode 100644 src/src/StevesBot.Webhook/YouTube/IStore.cs diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index 4bed4fb..d092e81 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -41,7 +41,7 @@ builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSingleton>(); builder.Services.AddHostedService(); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddOptionsWithValidateOnStart() .BindConfiguration(nameof(DiscordNotificationOptions)) diff --git a/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs b/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs index c75d2fc..763fcbe 100644 --- a/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs +++ b/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs @@ -8,7 +8,7 @@ internal static class NotificationHandler [FromServices] IYouTubeDataApiClient youTubeDataApiClient, [FromServices] IDiscordRestClient discordRestClient, [FromServices] IOptionsMonitor discordNotificationOptions, - [FromServices] LastPostedStreamStore lastPostedStream, + [FromServices] ILastPostedStreamStore lastPostedStream, CancellationToken cancellationToken ) { diff --git a/src/src/StevesBot.Webhook/YouTube/ILastPostedStreamStore.cs b/src/src/StevesBot.Webhook/YouTube/ILastPostedStreamStore.cs new file mode 100644 index 0000000..fe587f8 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/ILastPostedStreamStore.cs @@ -0,0 +1,5 @@ +namespace StevesBot.Webhook.YouTube; + +internal interface ILastPostedStreamStore : IStore +{ +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/IStore.cs b/src/src/StevesBot.Webhook/YouTube/IStore.cs new file mode 100644 index 0000000..994c506 --- /dev/null +++ b/src/src/StevesBot.Webhook/YouTube/IStore.cs @@ -0,0 +1,7 @@ +namespace StevesBot.Webhook.YouTube; + +internal interface IStore +{ + void SetValue(T value); + bool HasValue(T value); +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/LastPostedStreamStore.cs b/src/src/StevesBot.Webhook/YouTube/LastPostedStreamStore.cs index 7030e9f..a82c9a0 100644 --- a/src/src/StevesBot.Webhook/YouTube/LastPostedStreamStore.cs +++ b/src/src/StevesBot.Webhook/YouTube/LastPostedStreamStore.cs @@ -1,11 +1,16 @@ namespace StevesBot.Webhook.YouTube; -internal class LastPostedStreamStore +internal class LastPostedStreamStore : ILastPostedStreamStore { - public string Value { get; private set; } = string.Empty; + private string _value = string.Empty; public void SetValue(string value) { - Value = value; + _value = value; + } + + public bool HasValue(string value) + { + return _value.Equals(value, StringComparison.OrdinalIgnoreCase); } } \ No newline at end of file From ce45893caaee32dd038704803788650c25ce6e6e Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Thu, 29 May 2025 00:09:19 -0500 Subject: [PATCH 14/31] refactor: extract tests to library test project --- .../Discord/ServicesExtensions.cs | 4 +- .../Telemetry/HostExtensions.cs | 16 +++-- .../Telemetry/IInstrumentation.cs | 10 +++ .../Telemetry/StevesBotInstrumentation.cs | 15 ----- .../StevesBotWebhookInstrumentation.cs | 18 +++++ .../StevesBotWorkerInstrumentation.cs | 18 +++++ src/src/StevesBot.Webhook/Program.cs | 2 + src/src/StevesBot.Webhook/Usings.cs | 1 + .../YouTube/Handlers/NotificationHandler.cs | 2 +- .../appsettings.Example.json | 5 ++ src/src/StevesBot.Worker/Program.cs | 4 +- .../Unit/CreateMessageRequestTests.cs | 2 +- .../Unit/DiscordClientOptionsTests.cs | 2 +- .../Unit/DiscordMessageReferenceTests.cs | 2 +- .../Unit/DiscordMessageReferenceTypesTests.cs | 2 +- .../Unit/DiscordMessageTests.cs | 1 + .../Unit/DiscordRestClientExceptionTests.cs | 2 +- .../Unit/DiscordRestClientTests.cs | 4 +- .../Unit/DiscordUserTests.cs | 1 + .../Unit/GatewayResponseTests.cs | 0 .../Unit/HostExtensionsTests.cs | 6 ++ .../Unit/SeqOptionsTests.cs | 66 +++++++++++++++++++ .../StevesBotWebhookInstrumentationTests.cs | 32 +++++++++ .../StevesBotWorkerInstrumentationTests.cs | 32 +++++++++ src/tests/StevesBot.Library.Tests/Usings.cs | 13 +++- 25 files changed, 226 insertions(+), 34 deletions(-) create mode 100644 src/src/StevesBot.Library/Telemetry/IInstrumentation.cs delete mode 100644 src/src/StevesBot.Library/Telemetry/StevesBotInstrumentation.cs create mode 100644 src/src/StevesBot.Library/Telemetry/StevesBotWebhookInstrumentation.cs create mode 100644 src/src/StevesBot.Library/Telemetry/StevesBotWorkerInstrumentation.cs rename src/tests/{StevesBot.Worker.Tests => StevesBot.Library.Tests}/Unit/CreateMessageRequestTests.cs (92%) rename src/tests/{StevesBot.Worker.Tests => StevesBot.Library.Tests}/Unit/DiscordClientOptionsTests.cs (95%) rename src/tests/{StevesBot.Worker.Tests => StevesBot.Library.Tests}/Unit/DiscordMessageReferenceTests.cs (94%) rename src/tests/{StevesBot.Worker.Tests => StevesBot.Library.Tests}/Unit/DiscordMessageReferenceTypesTests.cs (90%) create mode 100644 src/tests/StevesBot.Library.Tests/Unit/DiscordMessageTests.cs rename src/tests/{StevesBot.Worker.Tests => StevesBot.Library.Tests}/Unit/DiscordRestClientExceptionTests.cs (95%) rename src/tests/{StevesBot.Worker.Tests => StevesBot.Library.Tests}/Unit/DiscordRestClientTests.cs (98%) create mode 100644 src/tests/StevesBot.Library.Tests/Unit/DiscordUserTests.cs create mode 100644 src/tests/StevesBot.Library.Tests/Unit/GatewayResponseTests.cs create mode 100644 src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs create mode 100644 src/tests/StevesBot.Library.Tests/Unit/SeqOptionsTests.cs create mode 100644 src/tests/StevesBot.Library.Tests/Unit/StevesBotWebhookInstrumentationTests.cs create mode 100644 src/tests/StevesBot.Library.Tests/Unit/StevesBotWorkerInstrumentationTests.cs diff --git a/src/src/StevesBot.Library/Discord/ServicesExtensions.cs b/src/src/StevesBot.Library/Discord/ServicesExtensions.cs index d8089e7..542dcd5 100644 --- a/src/src/StevesBot.Library/Discord/ServicesExtensions.cs +++ b/src/src/StevesBot.Library/Discord/ServicesExtensions.cs @@ -17,7 +17,9 @@ public static class ServicesExtensions c.BaseAddress = new Uri(discordOptions.ApiUrl); c.DefaultRequestHeaders.Authorization = new("Bot", discordOptions.AppToken); - var userAgentString = $"DiscordBot (https://github.com/StevanFreeborn/steves-bot, {StevesBotInstrumentation.SourceVersion})"; + var instrumentation = sp.GetRequiredService(); + + var userAgentString = $"DiscordBot (https://github.com/StevanFreeborn/steves-bot, {instrumentation.SourceVersion})"; c.DefaultRequestHeaders.Add("User-Agent", userAgentString); }) .AddStandardResilienceHandler(); diff --git a/src/src/StevesBot.Library/Telemetry/HostExtensions.cs b/src/src/StevesBot.Library/Telemetry/HostExtensions.cs index 8d88139..ea474d3 100644 --- a/src/src/StevesBot.Library/Telemetry/HostExtensions.cs +++ b/src/src/StevesBot.Library/Telemetry/HostExtensions.cs @@ -11,11 +11,15 @@ namespace StevesBot.Library.Telemetry; public static class HostExtensions { - public static HostApplicationBuilder AddTelemetry(this HostApplicationBuilder builder) + public static IHostApplicationBuilder AddTelemetry(this IHostApplicationBuilder builder, Func instrumentationFunc) { ArgumentNullException.ThrowIfNull(builder); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(instrumentationFunc); + + var instrumentation = builder.Services + .BuildServiceProvider() + .GetRequiredService(); var seq = new SeqOptions(); builder.Configuration.GetSection(nameof(SeqOptions)).Bind(seq); @@ -28,11 +32,11 @@ public static class HostExtensions builder.Services.AddOpenTelemetry() .ConfigureResource(resource => { - resource.AddService(StevesBotInstrumentation.SourceName, StevesBotInstrumentation.SourceVersion); + resource.AddService(instrumentation.SourceName, instrumentation.SourceVersion); resource.AddAttributes(new Dictionary { - ["service.name"] = StevesBotInstrumentation.SourceName, - ["service.version"] = StevesBotInstrumentation.SourceVersion, + ["service.name"] = instrumentation.SourceName, + ["service.version"] = instrumentation.SourceVersion, ["service.instance.id"] = Environment.MachineName, ["service.namespace"] = "stevesbot", ["service.environment"] = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "production", @@ -49,7 +53,7 @@ public static class HostExtensions }) .WithTracing(tb => { - tb.AddSource(StevesBotInstrumentation.SourceName); + tb.AddSource(instrumentation.SourceName); tb.AddAspNetCoreInstrumentation(); tb.AddHttpClientInstrumentation(); tb.AddOtlpExporter(o => diff --git a/src/src/StevesBot.Library/Telemetry/IInstrumentation.cs b/src/src/StevesBot.Library/Telemetry/IInstrumentation.cs new file mode 100644 index 0000000..1fd6eb7 --- /dev/null +++ b/src/src/StevesBot.Library/Telemetry/IInstrumentation.cs @@ -0,0 +1,10 @@ +using System.Diagnostics; + +namespace StevesBot.Library.Telemetry; + +public interface IInstrumentation : IDisposable +{ + string SourceName { get; } + string SourceVersion { get; } + ActivitySource Source { get; } +} \ No newline at end of file diff --git a/src/src/StevesBot.Library/Telemetry/StevesBotInstrumentation.cs b/src/src/StevesBot.Library/Telemetry/StevesBotInstrumentation.cs deleted file mode 100644 index f08b327..0000000 --- a/src/src/StevesBot.Library/Telemetry/StevesBotInstrumentation.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Diagnostics; - -namespace StevesBot.Library.Telemetry; - -public sealed class StevesBotInstrumentation : IDisposable -{ - public const string SourceName = "StevesBot.Worker"; - public const string SourceVersion = "0.0.0"; - public ActivitySource Source { get; } = new(SourceName, SourceVersion); - - public void Dispose() - { - Source.Dispose(); - } -} \ No newline at end of file diff --git a/src/src/StevesBot.Library/Telemetry/StevesBotWebhookInstrumentation.cs b/src/src/StevesBot.Library/Telemetry/StevesBotWebhookInstrumentation.cs new file mode 100644 index 0000000..e56aefa --- /dev/null +++ b/src/src/StevesBot.Library/Telemetry/StevesBotWebhookInstrumentation.cs @@ -0,0 +1,18 @@ +using System.Diagnostics; + +namespace StevesBot.Library.Telemetry; + +public sealed class StevesBotWebhookInstrumentation : IInstrumentation +{ + private const string SourceNameValue = "StevesBot.Webhook"; + private const string SourceVersionValue = "0.0.0"; + + public string SourceName { get; } = SourceNameValue; + public string SourceVersion { get; } = SourceVersionValue; + public ActivitySource Source { get; } = new ActivitySource(SourceNameValue, SourceVersionValue); + + public void Dispose() + { + Source.Dispose(); + } +} \ No newline at end of file diff --git a/src/src/StevesBot.Library/Telemetry/StevesBotWorkerInstrumentation.cs b/src/src/StevesBot.Library/Telemetry/StevesBotWorkerInstrumentation.cs new file mode 100644 index 0000000..de63a27 --- /dev/null +++ b/src/src/StevesBot.Library/Telemetry/StevesBotWorkerInstrumentation.cs @@ -0,0 +1,18 @@ +using System.Diagnostics; + +namespace StevesBot.Library.Telemetry; + +public sealed class StevesBotWorkerInstrumentation : IInstrumentation +{ + private const string SourceNameValue = "StevesBot.Worker"; + private const string SourceVersionValue = "0.0.0"; + + public string SourceName { get; } = SourceNameValue; + public string SourceVersion { get; } = SourceVersionValue; + public ActivitySource Source { get; } = new ActivitySource(SourceNameValue, SourceVersionValue); + + public void Dispose() + { + Source.Dispose(); + } +} \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index d092e81..5f876b8 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -1,5 +1,7 @@ var builder = WebApplication.CreateBuilder(args); +builder.AddTelemetry(static () => new StevesBotWebhookInstrumentation()); + builder.Services .AddOptionsWithValidateOnStart() .BindConfiguration(nameof(SubscriptionOptions)) diff --git a/src/src/StevesBot.Webhook/Usings.cs b/src/src/StevesBot.Webhook/Usings.cs index 5bd5405..7694e5f 100644 --- a/src/src/StevesBot.Webhook/Usings.cs +++ b/src/src/StevesBot.Webhook/Usings.cs @@ -12,6 +12,7 @@ global using StevesBot.Library.Discord; global using StevesBot.Library.Discord.Common; global using StevesBot.Library.Discord.Rest; global using StevesBot.Library.Discord.Rest.Requests; +global using StevesBot.Library.Telemetry; global using StevesBot.Webhook.YouTube; global using StevesBot.Webhook.YouTube.Data; global using StevesBot.Webhook.YouTube.Handlers; diff --git a/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs b/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs index 763fcbe..c7257e3 100644 --- a/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs +++ b/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs @@ -40,7 +40,7 @@ internal static class NotificationHandler return Results.Ok(); } - if (lastPostedStream.Value == videoId) + if (lastPostedStream.HasValue(videoId)) { logger.LogInformation("Video ID {VideoId} has already been posted. Skipping notification.", videoId); return Results.Ok(); diff --git a/src/src/StevesBot.Webhook/appsettings.Example.json b/src/src/StevesBot.Webhook/appsettings.Example.json index db3a004..eb9bcb2 100644 --- a/src/src/StevesBot.Webhook/appsettings.Example.json +++ b/src/src/StevesBot.Webhook/appsettings.Example.json @@ -25,5 +25,10 @@ "DiscordNotificationOptions": { "ChannelId": "ChannelId", "MessageFormat": "MessageFormat" + }, + "SeqOptions": { + "ServerUrl": "ServerUrl", + "ApiKeyHeader": "ApiKeyHeader", + "ApiKey": "ApiKey" } } diff --git a/src/src/StevesBot.Worker/Program.cs b/src/src/StevesBot.Worker/Program.cs index 722a736..36654b0 100644 --- a/src/src/StevesBot.Worker/Program.cs +++ b/src/src/StevesBot.Worker/Program.cs @@ -11,7 +11,7 @@ builder.Services.AddSingleton(static sp => return discordOptions; }); -builder.AddTelemetry(); +builder.AddTelemetry(static () => new StevesBotWorkerInstrumentation()); builder.Services.AddSingleton(); builder.Services.AddSingleton(TimeProvider.System); @@ -26,4 +26,4 @@ builder.Services.AddHostedService(); var host = builder.Build(); -await host.RunAsync(); \ No newline at end of file +await host.RunAsync(); diff --git a/src/tests/StevesBot.Worker.Tests/Unit/CreateMessageRequestTests.cs b/src/tests/StevesBot.Library.Tests/Unit/CreateMessageRequestTests.cs similarity index 92% rename from src/tests/StevesBot.Worker.Tests/Unit/CreateMessageRequestTests.cs rename to src/tests/StevesBot.Library.Tests/Unit/CreateMessageRequestTests.cs index 597ee76..2ec8a42 100644 --- a/src/tests/StevesBot.Worker.Tests/Unit/CreateMessageRequestTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/CreateMessageRequestTests.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Tests.Unit; +namespace StevesBot.Library.Tests.Unit; public class CreateMessageRequestTests { diff --git a/src/tests/StevesBot.Worker.Tests/Unit/DiscordClientOptionsTests.cs b/src/tests/StevesBot.Library.Tests/Unit/DiscordClientOptionsTests.cs similarity index 95% rename from src/tests/StevesBot.Worker.Tests/Unit/DiscordClientOptionsTests.cs rename to src/tests/StevesBot.Library.Tests/Unit/DiscordClientOptionsTests.cs index cd2f5c5..dc04065 100644 --- a/src/tests/StevesBot.Worker.Tests/Unit/DiscordClientOptionsTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/DiscordClientOptionsTests.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Tests.Unit; +namespace StevesBot.Library.Tests.Unit; public class DiscordClientOptionsTests { diff --git a/src/tests/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTests.cs b/src/tests/StevesBot.Library.Tests/Unit/DiscordMessageReferenceTests.cs similarity index 94% rename from src/tests/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTests.cs rename to src/tests/StevesBot.Library.Tests/Unit/DiscordMessageReferenceTests.cs index d96d002..2d4a356 100644 --- a/src/tests/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/DiscordMessageReferenceTests.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Tests.Unit; +namespace StevesBot.Library.Tests.Unit; public class DiscordMessageReferenceTests { diff --git a/src/tests/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTypesTests.cs b/src/tests/StevesBot.Library.Tests/Unit/DiscordMessageReferenceTypesTests.cs similarity index 90% rename from src/tests/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTypesTests.cs rename to src/tests/StevesBot.Library.Tests/Unit/DiscordMessageReferenceTypesTests.cs index cc00aca..e3c2645 100644 --- a/src/tests/StevesBot.Worker.Tests/Unit/DiscordMessageReferenceTypesTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/DiscordMessageReferenceTypesTests.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Tests.Unit; +namespace StevesBot.Library.Tests.Unit; public class DiscordMessageReferenceTypesTests { diff --git a/src/tests/StevesBot.Library.Tests/Unit/DiscordMessageTests.cs b/src/tests/StevesBot.Library.Tests/Unit/DiscordMessageTests.cs new file mode 100644 index 0000000..4e5d120 --- /dev/null +++ b/src/tests/StevesBot.Library.Tests/Unit/DiscordMessageTests.cs @@ -0,0 +1 @@ +namespace StevesBot.Library.Tests.Unit; \ No newline at end of file diff --git a/src/tests/StevesBot.Worker.Tests/Unit/DiscordRestClientExceptionTests.cs b/src/tests/StevesBot.Library.Tests/Unit/DiscordRestClientExceptionTests.cs similarity index 95% rename from src/tests/StevesBot.Worker.Tests/Unit/DiscordRestClientExceptionTests.cs rename to src/tests/StevesBot.Library.Tests/Unit/DiscordRestClientExceptionTests.cs index 5860b21..ea3dff3 100644 --- a/src/tests/StevesBot.Worker.Tests/Unit/DiscordRestClientExceptionTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/DiscordRestClientExceptionTests.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Tests.Unit; +namespace StevesBot.Library.Tests.Unit; public class DiscordRestClientExceptionTests { diff --git a/src/tests/StevesBot.Worker.Tests/Unit/DiscordRestClientTests.cs b/src/tests/StevesBot.Library.Tests/Unit/DiscordRestClientTests.cs similarity index 98% rename from src/tests/StevesBot.Worker.Tests/Unit/DiscordRestClientTests.cs rename to src/tests/StevesBot.Library.Tests/Unit/DiscordRestClientTests.cs index 17749d4..02dd99c 100644 --- a/src/tests/StevesBot.Worker.Tests/Unit/DiscordRestClientTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/DiscordRestClientTests.cs @@ -1,6 +1,4 @@ -using Microsoft.AspNetCore.Http.HttpResults; - -namespace StevesBot.Worker.Tests.Unit; +namespace StevesBot.Library.Tests.Unit; public sealed class DiscordRestClientTests : IDisposable { diff --git a/src/tests/StevesBot.Library.Tests/Unit/DiscordUserTests.cs b/src/tests/StevesBot.Library.Tests/Unit/DiscordUserTests.cs new file mode 100644 index 0000000..4e5d120 --- /dev/null +++ b/src/tests/StevesBot.Library.Tests/Unit/DiscordUserTests.cs @@ -0,0 +1 @@ +namespace StevesBot.Library.Tests.Unit; \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/Unit/GatewayResponseTests.cs b/src/tests/StevesBot.Library.Tests/Unit/GatewayResponseTests.cs new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs b/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs new file mode 100644 index 0000000..8b8f489 --- /dev/null +++ b/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs @@ -0,0 +1,6 @@ +namespace StevesBot.Library.Tests.Unit; + +public class HostExtensionsTests +{ + +} \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/Unit/SeqOptionsTests.cs b/src/tests/StevesBot.Library.Tests/Unit/SeqOptionsTests.cs new file mode 100644 index 0000000..4d14454 --- /dev/null +++ b/src/tests/StevesBot.Library.Tests/Unit/SeqOptionsTests.cs @@ -0,0 +1,66 @@ +namespace StevesBot.Library.Tests.Unit; + +public class SeqOptionsTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnInstance() + { + var result = new SeqOptions(); + + result.ServerUrl.Should().Be(string.Empty); + result.ApiKeyHeader.Should().Be(string.Empty); + result.ApiKey.Should().Be(string.Empty); + result.IsEnabled.Should().BeFalse(); + result.LogEndpoint.Should().Be($"{string.Empty}/ingest/otlp/v1/logs"); + result.TraceEndpoint.Should().Be($"{string.Empty}/ingest/otlp/v1/traces"); + result.AuthHeader.Should().Be($"{string.Empty}={string.Empty}"); + } + + [Fact] + public void Constructor_WhenPropertiesSet_ItShouldReturnCorrectValues() + { + var serverUrl = "http://example.com"; + var apiKeyHeader = "ApiKeyHeader"; + var apiKey = "ApiKeyValue"; + + var result = new SeqOptions + { + ServerUrl = serverUrl, + ApiKeyHeader = apiKeyHeader, + ApiKey = apiKey + }; + + result.ServerUrl.Should().Be(serverUrl); + result.ApiKeyHeader.Should().Be(apiKeyHeader); + result.ApiKey.Should().Be(apiKey); + result.IsEnabled.Should().BeTrue(); + result.LogEndpoint.Should().Be($"{serverUrl}/ingest/otlp/v1/logs"); + result.TraceEndpoint.Should().Be($"{serverUrl}/ingest/otlp/v1/traces"); + result.AuthHeader.Should().Be($"{apiKeyHeader}={apiKey}"); + } + + [Theory] + [InlineData(null, null, null, false)] + [InlineData("", "", "", false)] + [InlineData("http://example.com", null, null, false)] + [InlineData("http://example.com", "", "", false)] + [InlineData(null, "ApiKeyHeader", null, false)] + [InlineData(null, null, "ApiKeyValue", false)] + [InlineData("http://example.com", "ApiKeyHeader", "ApiKeyValue", true)] + [InlineData("http://example.com", "", "ApiKeyValue", false)] + [InlineData("", "ApiKeyHeader", "ApiKeyValue", false)] + [InlineData("http://example.com", "ApiKeyHeader", null, false)] + [InlineData("", "ApiKeyHeader", null, false)] + [InlineData(null, "ApiKeyHeader", "ApiKeyValue", false)] + public void IsEnabled_WhenPropertiesAreEmpty_ItShouldReturnFalse(string? serverUrl, string? apiKeyHeader, string? apiKey, bool expected) + { + var result = new SeqOptions + { + ServerUrl = serverUrl!, + ApiKeyHeader = apiKeyHeader!, + ApiKey = apiKey! + }; + + result.IsEnabled.Should().Be(expected); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/Unit/StevesBotWebhookInstrumentationTests.cs b/src/tests/StevesBot.Library.Tests/Unit/StevesBotWebhookInstrumentationTests.cs new file mode 100644 index 0000000..c762d06 --- /dev/null +++ b/src/tests/StevesBot.Library.Tests/Unit/StevesBotWebhookInstrumentationTests.cs @@ -0,0 +1,32 @@ +namespace StevesBot.Library.Tests.Unit; + +public sealed class StevesBotWebhookInstrumentationTests : IDisposable +{ + private readonly StevesBotWebhookInstrumentation _instrumentation = new(); + + [Fact] + public void SourceName_WhenCalled_ItShouldReturnCorrectName() + { + _instrumentation.SourceName.Should().Be("StevesBot.Webhook"); + } + + [Fact] + public void SourceVersion_WhenCalled_ItShouldReturnCorrectVersion() + { + _instrumentation.SourceVersion.Should().Be("0.0.0"); + } + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnInstance() + { + using var result = new StevesBotWorkerInstrumentation(); + using var expectedSource = new ActivitySource(_instrumentation.SourceName, _instrumentation.SourceVersion); + + result.Source.Should().BeEquivalentTo(expectedSource); + } + + public void Dispose() + { + _instrumentation.Dispose(); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/Unit/StevesBotWorkerInstrumentationTests.cs b/src/tests/StevesBot.Library.Tests/Unit/StevesBotWorkerInstrumentationTests.cs new file mode 100644 index 0000000..3d94aa6 --- /dev/null +++ b/src/tests/StevesBot.Library.Tests/Unit/StevesBotWorkerInstrumentationTests.cs @@ -0,0 +1,32 @@ +namespace StevesBot.Library.Tests.Unit; + +public sealed class StevesBotWorkerInstrumentationTests : IDisposable +{ + private readonly StevesBotWorkerInstrumentation _instrumentation = new(); + + [Fact] + public void SourceName_WhenCalled_ItShouldReturnCorrectName() + { + _instrumentation.SourceName.Should().Be("StevesBot.Worker"); + } + + [Fact] + public void SourceVersion_WhenCalled_ItShouldReturnCorrectVersion() + { + _instrumentation.SourceVersion.Should().Be("0.0.0"); + } + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnInstance() + { + using var result = new StevesBotWorkerInstrumentation(); + using var expectedSource = new ActivitySource(_instrumentation.SourceName, _instrumentation.SourceVersion); + + result.Source.Should().BeEquivalentTo(expectedSource); + } + + public void Dispose() + { + _instrumentation.Dispose(); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/Usings.cs b/src/tests/StevesBot.Library.Tests/Usings.cs index 99b9928..61c33bf 100644 --- a/src/tests/StevesBot.Library.Tests/Usings.cs +++ b/src/tests/StevesBot.Library.Tests/Usings.cs @@ -1,5 +1,16 @@ +global using System.Diagnostics; +global using System.Net; +global using System.Text.Json; + global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; + +global using Moq; + +global using RichardSzalay.MockHttp; global using StevesBot.Library.Discord; global using StevesBot.Library.Discord.Common; -global using StevesBot.Library.Discord.Rest; \ No newline at end of file +global using StevesBot.Library.Discord.Rest; +global using StevesBot.Library.Discord.Rest.Requests; +global using StevesBot.Library.Telemetry; From 0d31f1201279d8bb69987235dcf70ed220885264 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Thu, 29 May 2025 22:41:53 -0500 Subject: [PATCH 15/31] tests: add tests for library --- .../Telemetry/HostExtensions.cs | 13 ++-- .../YouTube/ILastPostedStreamStore.cs | 4 +- src/src/StevesBot.Webhook/YouTube/IStore.cs | 7 --- .../StevesBot.Library.Tests.csproj | 2 +- .../Unit/DiscordMessageTests.cs | 42 ++++++++++++- .../Unit/DiscordUserTests.cs | 22 ++++++- .../Unit/GatewayResponseTests.cs | 13 ++++ .../Unit/HostExtensionsTests.cs | 59 +++++++++++++++++++ .../Unit/ServicesExtensionsTests.cs | 2 + .../StevesBotWebhookInstrumentationTests.cs | 2 +- src/tests/StevesBot.Library.Tests/Usings.cs | 7 +++ 11 files changed, 155 insertions(+), 18 deletions(-) delete mode 100644 src/src/StevesBot.Webhook/YouTube/IStore.cs diff --git a/src/src/StevesBot.Library/Telemetry/HostExtensions.cs b/src/src/StevesBot.Library/Telemetry/HostExtensions.cs index ea474d3..1afb0d1 100644 --- a/src/src/StevesBot.Library/Telemetry/HostExtensions.cs +++ b/src/src/StevesBot.Library/Telemetry/HostExtensions.cs @@ -14,12 +14,7 @@ public static class HostExtensions public static IHostApplicationBuilder AddTelemetry(this IHostApplicationBuilder builder, Func instrumentationFunc) { ArgumentNullException.ThrowIfNull(builder); - - builder.Services.AddSingleton(instrumentationFunc); - - var instrumentation = builder.Services - .BuildServiceProvider() - .GetRequiredService(); + ArgumentNullException.ThrowIfNull(instrumentationFunc); var seq = new SeqOptions(); builder.Configuration.GetSection(nameof(SeqOptions)).Bind(seq); @@ -29,6 +24,12 @@ public static class HostExtensions return builder; } + builder.Services.AddSingleton(instrumentationFunc()); + + var instrumentation = builder.Services + .BuildServiceProvider() + .GetRequiredService(); + builder.Services.AddOpenTelemetry() .ConfigureResource(resource => { diff --git a/src/src/StevesBot.Webhook/YouTube/ILastPostedStreamStore.cs b/src/src/StevesBot.Webhook/YouTube/ILastPostedStreamStore.cs index fe587f8..84a85f7 100644 --- a/src/src/StevesBot.Webhook/YouTube/ILastPostedStreamStore.cs +++ b/src/src/StevesBot.Webhook/YouTube/ILastPostedStreamStore.cs @@ -1,5 +1,7 @@ namespace StevesBot.Webhook.YouTube; -internal interface ILastPostedStreamStore : IStore +internal interface ILastPostedStreamStore { + void SetValue(string value); + bool HasValue(string value); } \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/IStore.cs b/src/src/StevesBot.Webhook/YouTube/IStore.cs deleted file mode 100644 index 994c506..0000000 --- a/src/src/StevesBot.Webhook/YouTube/IStore.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StevesBot.Webhook.YouTube; - -internal interface IStore -{ - void SetValue(T value); - bool HasValue(T value); -} \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/StevesBot.Library.Tests.csproj b/src/tests/StevesBot.Library.Tests/StevesBot.Library.Tests.csproj index 7694b4b..813a84e 100644 --- a/src/tests/StevesBot.Library.Tests/StevesBot.Library.Tests.csproj +++ b/src/tests/StevesBot.Library.Tests/StevesBot.Library.Tests.csproj @@ -25,7 +25,7 @@ true ./TestResults/Coverage/ cobertura - [StevesBot.Lib]* + [StevesBot.Library]* diff --git a/src/tests/StevesBot.Library.Tests/Unit/DiscordMessageTests.cs b/src/tests/StevesBot.Library.Tests/Unit/DiscordMessageTests.cs index 4e5d120..dd087d4 100644 --- a/src/tests/StevesBot.Library.Tests/Unit/DiscordMessageTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/DiscordMessageTests.cs @@ -1 +1,41 @@ -namespace StevesBot.Library.Tests.Unit; \ No newline at end of file +namespace StevesBot.Library.Tests.Unit; + +public class DiscordMessageTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstance() + { + var result = new DiscordMessage(); + + result.Id.Should().BeEmpty(); + result.Type.Should().Be(0); + result.ChannelId.Should().BeEmpty(); + result.GuildId.Should().BeEmpty(); + result.Author.Should().BeEquivalentTo(new DiscordUser()); + } + + [Fact] + public void Constructor_WhenCalledWithParameters_ItShouldReturnAnInstance() + { + var id = "12345"; + var type = 1; + var channelId = "67890"; + var guildId = "54321"; + var author = new DiscordUser { Id = "11111" }; + + var result = new DiscordMessage + { + Id = id, + Type = type, + ChannelId = channelId, + GuildId = guildId, + Author = author + }; + + result.Id.Should().Be(id); + result.Type.Should().Be(type); + result.ChannelId.Should().Be(channelId); + result.GuildId.Should().Be(guildId); + result.Author.Should().BeEquivalentTo(author); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/Unit/DiscordUserTests.cs b/src/tests/StevesBot.Library.Tests/Unit/DiscordUserTests.cs index 4e5d120..ff0edc0 100644 --- a/src/tests/StevesBot.Library.Tests/Unit/DiscordUserTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/DiscordUserTests.cs @@ -1 +1,21 @@ -namespace StevesBot.Library.Tests.Unit; \ No newline at end of file +namespace StevesBot.Library.Tests.Unit; + +public class DiscordUserTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstance() + { + var result = new DiscordUser(); + + result.Id.Should().BeEmpty(); + } + + [Fact] + public void Constructor_WhenCalledWithId_ItShouldReturnAnInstanceWithId() + { + var id = "12345"; + var result = new DiscordUser { Id = id }; + + result.Id.Should().Be(id); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/Unit/GatewayResponseTests.cs b/src/tests/StevesBot.Library.Tests/Unit/GatewayResponseTests.cs index e69de29..350a81e 100644 --- a/src/tests/StevesBot.Library.Tests/Unit/GatewayResponseTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/GatewayResponseTests.cs @@ -0,0 +1,13 @@ +namespace StevesBot.Library.Tests.Unit; + +public class GatewayResponseTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstance() + { + var url = "https://example.com"; + var result = new GatewayResponse(url); + + result.Url.Should().Be(url); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs b/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs index 8b8f489..48a9200 100644 --- a/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs @@ -2,5 +2,64 @@ namespace StevesBot.Library.Tests.Unit; public class HostExtensionsTests { + [Fact] + public void AddTelemetry_WhenCalledAndSeqOptionsNotConfigured_ItShouldNotAddTelemetry() + { + var builder = WebApplication.CreateBuilder(); + builder.AddTelemetry(static () => new StevesBotWebhookInstrumentation()); + + var app = builder.Build(); + + app.Services + .GetService() + .Should() + .BeNull(); + + app.Services + .GetService() + .Should() + .BeNull(); + + app.Services + .GetService() + .Should() + .BeNull(); + } + + [Fact] + public void AddTelemetry_WhenCalledAndSeqOptionsConfigured_ItShouldAddTelemetry() + { + var builder = WebApplication.CreateBuilder(); + + var json = $@"{{ + ""SeqOptions"": {{ + ""ServerUrl"": ""http://localhost:5341"", + ""ApiKey"": ""my-api-key"", + ""ApiKeyHeader"": ""X-Seq-ApiKey"" + }} + }}"; + + builder.Configuration.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))); + + builder.AddTelemetry(static () => new StevesBotWebhookInstrumentation()); + + var app = builder.Build(); + + app.Services + .GetService() + .Should() + .NotBeNull(); + + app.Services + .GetServices() + .Where(static p => p is OpenTelemetryLoggerProvider) + .Should() + .NotBeEmpty(); + + app.Services + .GetService() + .Should() + .NotBeNull(); + } } \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/Unit/ServicesExtensionsTests.cs b/src/tests/StevesBot.Library.Tests/Unit/ServicesExtensionsTests.cs index 0737a77..5006291 100644 --- a/src/tests/StevesBot.Library.Tests/Unit/ServicesExtensionsTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/ServicesExtensionsTests.cs @@ -7,10 +7,12 @@ public class ServicesExtensionsTests ApiUrl = "https://test.com", AppToken = "test_token", }; + private readonly Mock _instrumentationMock = new(); private readonly ServiceCollection _services = new(); public ServicesExtensionsTests() { + _services.AddSingleton(_instrumentationMock.Object); _services.AddSingleton(_discordClientOptions); } diff --git a/src/tests/StevesBot.Library.Tests/Unit/StevesBotWebhookInstrumentationTests.cs b/src/tests/StevesBot.Library.Tests/Unit/StevesBotWebhookInstrumentationTests.cs index c762d06..8eea5f8 100644 --- a/src/tests/StevesBot.Library.Tests/Unit/StevesBotWebhookInstrumentationTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/StevesBotWebhookInstrumentationTests.cs @@ -19,7 +19,7 @@ public sealed class StevesBotWebhookInstrumentationTests : IDisposable [Fact] public void Constructor_WhenCalled_ItShouldReturnInstance() { - using var result = new StevesBotWorkerInstrumentation(); + using var result = new StevesBotWebhookInstrumentation(); using var expectedSource = new ActivitySource(_instrumentation.SourceName, _instrumentation.SourceVersion); result.Source.Should().BeEquivalentTo(expectedSource); diff --git a/src/tests/StevesBot.Library.Tests/Usings.cs b/src/tests/StevesBot.Library.Tests/Usings.cs index 61c33bf..ebcc03a 100644 --- a/src/tests/StevesBot.Library.Tests/Usings.cs +++ b/src/tests/StevesBot.Library.Tests/Usings.cs @@ -1,16 +1,23 @@ global using System.Diagnostics; global using System.Net; +global using System.Text; global using System.Text.Json; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; global using Moq; +global using OpenTelemetry.Logs; +global using OpenTelemetry.Trace; + global using RichardSzalay.MockHttp; global using StevesBot.Library.Discord; global using StevesBot.Library.Discord.Common; global using StevesBot.Library.Discord.Rest; global using StevesBot.Library.Discord.Rest.Requests; +global using StevesBot.Library.Discord.Rest.Responses; global using StevesBot.Library.Telemetry; From c7d0df2fca503ea7b94dcfa6c3bb7eab712aaea5 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Thu, 29 May 2025 23:01:00 -0500 Subject: [PATCH 16/31] tests: begin adding tests to webhook project --- .../YouTube/LastPostedStreamStore.cs | 2 ++ .../Unit/DiscordNotificationOptionsTests.cs | 28 +++++++++++++++ .../Unit/LastPostedStreamStore.cs | 36 +++++++++++++++++++ .../Unit/PubSubClientOptionsTests.cs | 25 +++++++++++++ .../Unit/SubscribeTaskTests.cs | 33 +++++++++++++++++ .../Unit/SubscriptionOptionsTests.cs | 29 +++++++++++++++ .../Unit/YouTubeClientOptionsTests.cs | 29 +++++++++++++++ src/tests/StevesBot.Webhook.Tests/Usings.cs | 2 ++ 8 files changed, 184 insertions(+) create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/DiscordNotificationOptionsTests.cs create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/LastPostedStreamStore.cs create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/PubSubClientOptionsTests.cs create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/SubscribeTaskTests.cs create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/SubscriptionOptionsTests.cs create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/YouTubeClientOptionsTests.cs create mode 100644 src/tests/StevesBot.Webhook.Tests/Usings.cs diff --git a/src/src/StevesBot.Webhook/YouTube/LastPostedStreamStore.cs b/src/src/StevesBot.Webhook/YouTube/LastPostedStreamStore.cs index a82c9a0..4ab8975 100644 --- a/src/src/StevesBot.Webhook/YouTube/LastPostedStreamStore.cs +++ b/src/src/StevesBot.Webhook/YouTube/LastPostedStreamStore.cs @@ -6,11 +6,13 @@ internal class LastPostedStreamStore : ILastPostedStreamStore public void SetValue(string value) { + ArgumentNullException.ThrowIfNull(value); _value = value; } public bool HasValue(string value) { + ArgumentNullException.ThrowIfNull(value); return _value.Equals(value, StringComparison.OrdinalIgnoreCase); } } \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/DiscordNotificationOptionsTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/DiscordNotificationOptionsTests.cs new file mode 100644 index 0000000..8b18ca5 --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/DiscordNotificationOptionsTests.cs @@ -0,0 +1,28 @@ +namespace StevesBot.Webhook.Tests.Unit; + +public class DiscordNotificationOptionsTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnInstance() + { + var options = new DiscordNotificationOptions(); + + options.ChannelId.Should().BeEmpty(); + } + + [Fact] + public void Constructor_WhenCalledWithProperties_ItShouldReturnInstance() + { + var channelId = "1234567890"; + var messageFormat = "New video uploaded: {VideoTitle}"; + + var options = new DiscordNotificationOptions + { + ChannelId = channelId, + MessageFormat = messageFormat + }; + + options.ChannelId.Should().Be(channelId); + options.MessageFormat.Should().Be(messageFormat); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/LastPostedStreamStore.cs b/src/tests/StevesBot.Webhook.Tests/Unit/LastPostedStreamStore.cs new file mode 100644 index 0000000..90f1cd1 --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/LastPostedStreamStore.cs @@ -0,0 +1,36 @@ +namespace StevesBot.Webhook.Tests.Unit; + +public class LastPostedStreamStoreTests +{ + [Fact] + public void SetValue_WhenCalledWithNull_ItShouldThrowArgumentNullException() + { + var store = new LastPostedStreamStore(); + + var act = () => store.SetValue(null!); + + act.Should().Throw(); + } + + [Fact] + public void HasValue_WhenCalledWithNull_ItShouldThrowArgumentNullException() + { + var store = new LastPostedStreamStore(); + + var act = () => store.HasValue(null!); + + act.Should().Throw(); + } + + + [Fact] + public void SetValueHasValue_WhenCalled_ItShouldStoreValue() + { + var store = new LastPostedStreamStore(); + var streamId = "test-stream-id"; + + store.SetValue(streamId); + + store.HasValue(streamId).Should().BeTrue(); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/PubSubClientOptionsTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/PubSubClientOptionsTests.cs new file mode 100644 index 0000000..324b5f3 --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/PubSubClientOptionsTests.cs @@ -0,0 +1,25 @@ +namespace StevesBot.Webhook.Tests.Unit; + +public class PubSubClientOptionsTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnInstance() + { + var options = new PubSubClientOptions(); + + options.BaseUrl.Should().BeEmpty(); + } + + [Fact] + public void Constructor_WhenCalledWithProperties_ItShouldReturnInstance() + { + var baseUrl = "https://example.com"; + + var options = new PubSubClientOptions() + { + BaseUrl = baseUrl + }; + + options.BaseUrl.Should().Be(baseUrl); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/SubscribeTaskTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/SubscribeTaskTests.cs new file mode 100644 index 0000000..463a642 --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/SubscribeTaskTests.cs @@ -0,0 +1,33 @@ +namespace StevesBot.Webhook.Tests.Unit; + +public class SubscribeTaskTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnInstance() + { + var task = new SubscribeTask(); + + task.CallbackUrl.Should().BeEmpty(); + task.TopicUrl.Should().BeEmpty(); + task.ExpiresAt.Should().Be(DateTime.MinValue); + } + + [Fact] + public void Constructor_WhenCalledWithProperties_ItShouldReturnInstance() + { + var callbackUrl = "https://example.com/callback"; + var topicUrl = "https://example.com/topic"; + var expiresAt = DateTime.UtcNow.AddDays(1); + + var task = new SubscribeTask + { + CallbackUrl = callbackUrl, + TopicUrl = topicUrl, + ExpiresAt = expiresAt + }; + + task.CallbackUrl.Should().Be(callbackUrl); + task.TopicUrl.Should().Be(topicUrl); + task.ExpiresAt.Should().Be(expiresAt); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/SubscriptionOptionsTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/SubscriptionOptionsTests.cs new file mode 100644 index 0000000..f86d1e0 --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/SubscriptionOptionsTests.cs @@ -0,0 +1,29 @@ +namespace StevesBot.Webhook.Tests.Unit; + +public class SubscriptionOptionsTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnInstance() + { + var options = new SubscriptionOptions(); + + options.CallbackUrl.Should().BeEmpty(); + options.TopicUrl.Should().BeEmpty(); + } + + [Fact] + public void Constructor_WhenCalledWithProperties_ItShouldReturnInstance() + { + var callbackUrl = "https://example.com/callback"; + var topicUrl = "https://example.com/topic"; + + var options = new SubscriptionOptions + { + CallbackUrl = callbackUrl, + TopicUrl = topicUrl + }; + + options.CallbackUrl.Should().Be(callbackUrl); + options.TopicUrl.Should().Be(topicUrl); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeClientOptionsTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeClientOptionsTests.cs new file mode 100644 index 0000000..2746534 --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeClientOptionsTests.cs @@ -0,0 +1,29 @@ +namespace StevesBot.Webhook.Tests.Unit; + +public class YouTubeClientOptionsTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnInstance() + { + var options = new YouTubeClientOptions(); + + options.BaseUrl.Should().BeEmpty(); + options.ApiKey.Should().BeEmpty(); + } + + [Fact] + public void Constructor_WhenCalledWithProperties_ItShouldReturnInstance() + { + var baseUrl = "https://example.com"; + var apiKey = "test-api-key"; + + var options = new YouTubeClientOptions + { + BaseUrl = baseUrl, + ApiKey = apiKey + }; + + options.BaseUrl.Should().Be(baseUrl); + options.ApiKey.Should().Be(apiKey); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Usings.cs b/src/tests/StevesBot.Webhook.Tests/Usings.cs new file mode 100644 index 0000000..4617e08 --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Usings.cs @@ -0,0 +1,2 @@ +global using StevesBot.Webhook.YouTube; +global using StevesBot.Webhook.YouTube.Tasks; \ No newline at end of file From cbf75982270a842fe00d614748c3177469e77d44 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 30 May 2025 15:46:20 -0500 Subject: [PATCH 17/31] tests: WRITE TESTS FIRST...when will I learn --- .../Unit/PubSubClientTests.cs | 36 ++++++++++ .../Unit/YouTubePageInfoTests.cs | 6 ++ .../Unit/YouTubeSnippetTests.cs | 25 +++++++ .../Unit/YouTubeVideoListResponseTests.cs | 31 +++++++++ .../Unit/YouTubeVideoTests.cs | 68 +++++++++++++++++++ src/tests/StevesBot.Webhook.Tests/Usings.cs | 11 ++- 6 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/PubSubClientTests.cs create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/YouTubePageInfoTests.cs create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/YouTubeSnippetTests.cs create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/YouTubeVideoListResponseTests.cs create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/YouTubeVideoTests.cs diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/PubSubClientTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/PubSubClientTests.cs new file mode 100644 index 0000000..f00bac1 --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/PubSubClientTests.cs @@ -0,0 +1,36 @@ + +namespace StevesBot.Webhook.Tests.Unit; + +public sealed class PubSubClientTests : IDisposable +{ + private readonly MockHttpMessageHandler _mockHttpMessageHandler = new(); + private readonly Mock> _mockLogger = new(); + private readonly PubSubClient _sut; + + public PubSubClientTests() + { + var mockHttpClient = _mockHttpMessageHandler.ToHttpClient(); + mockHttpClient.BaseAddress = new("https://test.com"); + + _sut = new(mockHttpClient, _mockLogger.Object); + } + + [Theory] + [InlineData(HttpStatusCode.InternalServerError, false)] + [InlineData(HttpStatusCode.OK, true)] + public async Task SubscribeAsync_WhenCalled_ItShouldReturnExpectedResult(HttpStatusCode givenStatusCode, bool expectedResult) + { + _mockHttpMessageHandler + .When("*/subscribe") + .Respond(givenStatusCode); + + var result = await _sut.SubscribeAsync("callbackUrl", "topicUrl"); + + result.Should().Be(expectedResult); + } + + public void Dispose() + { + _mockHttpMessageHandler.Dispose(); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/YouTubePageInfoTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubePageInfoTests.cs new file mode 100644 index 0000000..10ea56c --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubePageInfoTests.cs @@ -0,0 +1,6 @@ +namespace StevesBot.Webhook.Tests.Unit; + +public class YouTubePageInfoTests +{ + +} \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeSnippetTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeSnippetTests.cs new file mode 100644 index 0000000..1e2a061 --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeSnippetTests.cs @@ -0,0 +1,25 @@ +namespace StevesBot.Webhook.Tests.Unit; + +public class YouTubeSnippetTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstance() + { + var result = new YouTubeSnippet(); + + result.LiveBroadcastContent.Should().BeEmpty(); + } + + [Fact] + public void Constructor_WhenCalledWithValues_ItShouldReturnAnInstance() + { + var content = "live"; + + var result = new YouTubeSnippet() + { + LiveBroadcastContent = content, + }; + + result.LiveBroadcastContent.Should().Be(content); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeVideoListResponseTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeVideoListResponseTests.cs new file mode 100644 index 0000000..2bf7ea3 --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeVideoListResponseTests.cs @@ -0,0 +1,31 @@ +using StevesBot.Webhook.YouTube.Data; + +namespace StevesBot.Webhook.Tests.Unit; + +public class YouTubeVideoListResponseTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstance() + { + var result = new YouTubeVideoListResponse(); + + result.Items.Should().BeEmpty(); + result.PageInfo.Should().BeEquivalentTo(new YouTubePageInfo()); + } + + [Fact] + public void Constructor_WhenCalledWithValues_ItShouldReturnAnInstance() + { + var videos = new YouTubeVideo[] { new() }; + var pageInfo = new YouTubePageInfo(); + + var result = new YouTubeVideoListResponse() + { + Items = videos, + PageInfo = pageInfo, + }; + + result.Items.Should().BeSameAs(videos); + result.PageInfo.Should().BeSameAs(pageInfo); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeVideoTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeVideoTests.cs new file mode 100644 index 0000000..128d5fd --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeVideoTests.cs @@ -0,0 +1,68 @@ +namespace StevesBot.Webhook.Tests.Unit; + +public class YouTubeVideoTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstance() + { + var result = new YouTubeVideo(); + + result.Id.Should().BeEmpty(); + result.LiveStreamingDetails.Should().BeNull(); + result.Snippet.Should().BeEquivalentTo(new YouTubeSnippet()); + } + + [Fact] + public void Constructor_WhenCalledWithValues_ItShouldReturnAnInstance() + { + var id = "id"; + var details = new YouTubeLiveStreamingDetails(); + var snippet = new YouTubeSnippet(); + + var result = new YouTubeVideo() + { + Id = id, + LiveStreamingDetails = details, + Snippet = snippet, + }; + + result.Id.Should().Be(id); + result.LiveStreamingDetails.Should().BeSameAs(details); + result.Snippet.Should().BeSameAs(snippet); + } + + [Fact] + public void IsLiveStream_WhenLiveStreamingDetailsIsNull_ItShouldReturnFalse() + { + var video = new YouTubeVideo() + { + Snippet = new() { LiveBroadcastContent = "live" }, + }; + + video.IsLiveStream.Should().BeFalse(); + } + + [Fact] + public void IsLiveStream_WhenSnippetLiveBroadcastContentIsNotLive_ItShouldReturnFalse() + { + var video = new YouTubeVideo() + { + LiveStreamingDetails = new(), + Snippet = new() { LiveBroadcastContent = "none" }, + }; + + video.IsLiveStream.Should().BeFalse(); + } + + [Fact] + public void IsLiveStream_WhenDetailsIsNotNullAndContentIsLive_ItShouldReturnTrue() + { + var video = new YouTubeVideo() + { + LiveStreamingDetails = new(), + Snippet = new() { LiveBroadcastContent = "live" }, + }; + + video.IsLiveStream.Should().BeTrue(); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Usings.cs b/src/tests/StevesBot.Webhook.Tests/Usings.cs index 4617e08..aadd939 100644 --- a/src/tests/StevesBot.Webhook.Tests/Usings.cs +++ b/src/tests/StevesBot.Webhook.Tests/Usings.cs @@ -1,2 +1,11 @@ +global using System.Net; + +global using Microsoft.Extensions.Logging; + +global using Moq; + +global using RichardSzalay.MockHttp; + global using StevesBot.Webhook.YouTube; -global using StevesBot.Webhook.YouTube.Tasks; \ No newline at end of file +global using StevesBot.Webhook.YouTube.Data; +global using StevesBot.Webhook.YouTube.Tasks; From 7a12f2768d1333f2a43989f0474378eb2b51f806 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 30 May 2025 17:27:23 -0500 Subject: [PATCH 18/31] tests: add tests for page info --- .../Unit/YouTubePageInfoTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/YouTubePageInfoTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubePageInfoTests.cs index 10ea56c..5bf628f 100644 --- a/src/tests/StevesBot.Webhook.Tests/Unit/YouTubePageInfoTests.cs +++ b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubePageInfoTests.cs @@ -2,5 +2,28 @@ namespace StevesBot.Webhook.Tests.Unit; public class YouTubePageInfoTests { + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstance() + { + var result = new YouTubePageInfo(); + result.TotalResults.Should().Be(0); + result.ResultsPerPage.Should().Be(0); + } + + [Fact] + public void Constructor_WhenCalledWithValues_ItShouldReturnAnInstance() + { + var total = 10; + var resultsPerPage = 1; + + var result = new YouTubePageInfo() + { + TotalResults = total, + ResultsPerPage = resultsPerPage, + }; + + result.TotalResults.Should().Be(total); + result.ResultsPerPage.Should().Be(resultsPerPage); + } } \ No newline at end of file From 5399372c053d43bd92b3511156cd2ee1f05f7767 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 30 May 2025 20:52:33 -0500 Subject: [PATCH 19/31] fix: don't make rest api client dependent on instrumentation --- src/src/StevesBot.Library/Discord/ServicesExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/src/StevesBot.Library/Discord/ServicesExtensions.cs b/src/src/StevesBot.Library/Discord/ServicesExtensions.cs index 542dcd5..71a9183 100644 --- a/src/src/StevesBot.Library/Discord/ServicesExtensions.cs +++ b/src/src/StevesBot.Library/Discord/ServicesExtensions.cs @@ -17,9 +17,10 @@ public static class ServicesExtensions c.BaseAddress = new Uri(discordOptions.ApiUrl); c.DefaultRequestHeaders.Authorization = new("Bot", discordOptions.AppToken); - var instrumentation = sp.GetRequiredService(); + var instrumentation = sp.GetService(); + var version = instrumentation?.SourceVersion ?? "0.0.0"; - var userAgentString = $"DiscordBot (https://github.com/StevanFreeborn/steves-bot, {instrumentation.SourceVersion})"; + var userAgentString = $"DiscordBot (https://github.com/StevanFreeborn/steves-bot, {version})"; c.DefaultRequestHeaders.Add("User-Agent", userAgentString); }) .AddStandardResilienceHandler(); From 390b6f56c50782b3be1ef09078650824e73a6d54 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sat, 31 May 2025 21:58:18 -0500 Subject: [PATCH 20/31] tests: add tests for data client --- .../StevesBot.Webhook.Tests.csproj | 2 +- .../Unit/SubscribeTaskTests.cs | 4 +- .../Unit/YouTubeDataApiClientTests.cs | 81 +++++++++++++++++++ .../Unit/YouTubeLiveStreamingDetailsTests.cs | 42 ++++++++++ 4 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/YouTubeDataApiClientTests.cs create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/YouTubeLiveStreamingDetailsTests.cs diff --git a/src/tests/StevesBot.Webhook.Tests/StevesBot.Webhook.Tests.csproj b/src/tests/StevesBot.Webhook.Tests/StevesBot.Webhook.Tests.csproj index f84a1e3..eedb9b8 100644 --- a/src/tests/StevesBot.Webhook.Tests/StevesBot.Webhook.Tests.csproj +++ b/src/tests/StevesBot.Webhook.Tests/StevesBot.Webhook.Tests.csproj @@ -27,7 +27,7 @@ ./TestResults/Coverage/ cobertura [StevesBot.Webhook]* - **/Program.cs,**/Worker.cs + **/Program.cs,**/SubscriptionWorker.cs diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/SubscribeTaskTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/SubscribeTaskTests.cs index 463a642..2c63a3b 100644 --- a/src/tests/StevesBot.Webhook.Tests/Unit/SubscribeTaskTests.cs +++ b/src/tests/StevesBot.Webhook.Tests/Unit/SubscribeTaskTests.cs @@ -9,7 +9,7 @@ public class SubscribeTaskTests task.CallbackUrl.Should().BeEmpty(); task.TopicUrl.Should().BeEmpty(); - task.ExpiresAt.Should().Be(DateTime.MinValue); + task.ExpiresAt.Should().Be(DateTimeOffset.MinValue); } [Fact] @@ -23,7 +23,7 @@ public class SubscribeTaskTests { CallbackUrl = callbackUrl, TopicUrl = topicUrl, - ExpiresAt = expiresAt + ExpiresAt = expiresAt, }; task.CallbackUrl.Should().Be(callbackUrl); diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeDataApiClientTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeDataApiClientTests.cs new file mode 100644 index 0000000..1021642 --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeDataApiClientTests.cs @@ -0,0 +1,81 @@ +using System.Text.Json; + +using Microsoft.Extensions.Options; + +namespace StevesBot.Webhook.Tests.Unit; + +public sealed class YouTubeDataApiClientTests : IDisposable +{ + private readonly MockHttpMessageHandler _mockHttpMessageHandler = new(); + private readonly Mock> _mockLogger = new(); + private readonly Mock> _mockOptions = new(); + private readonly YouTubeDataApiClient _sut; + + public YouTubeDataApiClientTests() + { + var httpClient = _mockHttpMessageHandler.ToHttpClient(); + httpClient.BaseAddress = new Uri("https://test.com"); + + _mockOptions + .Setup(static x => x.Value) + .Returns(new YouTubeClientOptions()); + + _sut = new(httpClient, _mockLogger.Object, _mockOptions.Object); + } + + [Fact] + public async Task GetVideoByIdAsync_WhenRequestFails_ItShouldReturnNull() + { + _mockHttpMessageHandler + .When("*/videos") + .Respond(HttpStatusCode.InternalServerError); + + var result = await _sut.GetVideoByIdAsync("video_id"); + + result.Should().BeNull(); + } + + [Fact] + public async Task GetVideoByIdAsync_WhenRequestSucceedsButItemsEmpty_ItShouldReturnNull() + { + var videosResponse = new YouTubeVideoListResponse(); + + _mockHttpMessageHandler + .When("*/videos") + .Respond( + HttpStatusCode.OK, + "application/json", + JsonSerializer.Serialize(videosResponse) + ); + + var result = await _sut.GetVideoByIdAsync("video_id", ["snippet"]); + + result.Should().BeNull(); + } + + [Fact] + public async Task GetVideoByIdAsync_WhenRequestSucceedsAndVideoFound_ItShouldReturnVideo() + { + var videoId = "video_id"; + var video = new YouTubeVideo() { Id = videoId }; + var videosResponse = new YouTubeVideoListResponse() { Items = [video] }; + + _mockHttpMessageHandler + .When("*/videos") + .Respond( + HttpStatusCode.OK, + "application/json", + JsonSerializer.Serialize(videosResponse) + ); + + var result = await _sut.GetVideoByIdAsync(videoId); + + result.Should().BeEquivalentTo(video); + + } + + public void Dispose() + { + _mockHttpMessageHandler.Dispose(); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeLiveStreamingDetailsTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeLiveStreamingDetailsTests.cs new file mode 100644 index 0000000..004d2c6 --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/YouTubeLiveStreamingDetailsTests.cs @@ -0,0 +1,42 @@ +namespace StevesBot.Webhook.Tests.Unit; + +public class YouTubeLiveStreamingDetailsTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstance() + { + var result = new YouTubeLiveStreamingDetails(); + + result.ActualStartTime.Should().BeNull(); + result.ActualEndTime.Should().BeNull(); + result.ScheduledStartTime.Should().BeNull(); + result.ScheduledEndTime.Should().BeNull(); + result.ConcurrentViewers.Should().BeNull(); + result.ActiveLiveChatId.Should().BeNull(); + } + + [Fact] + public void Constructor_WhenCalledWithValues_ItShouldReturnAnInstance() + { + var now = DateTimeOffset.UtcNow; + var viewers = 100UL; + var chatId = "chad_id"; + + var result = new YouTubeLiveStreamingDetails() + { + ActualStartTime = now, + ActualEndTime = now, + ScheduledStartTime = now, + ScheduledEndTime = now, + ConcurrentViewers = viewers, + ActiveLiveChatId = chatId, + }; + + result.ActualStartTime.Should().Be(now); + result.ActualEndTime.Should().Be(now); + result.ScheduledStartTime.Should().Be(now); + result.ScheduledEndTime.Should().Be(now); + result.ConcurrentViewers.Should().Be(viewers); + result.ActiveLiveChatId.Should().Be(chatId); + } +} \ No newline at end of file From 7f9404dff39fd25f935a04e48b86a7240542559d Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 2 Jun 2025 00:00:28 -0500 Subject: [PATCH 21/31] chore: stub out tests --- .../YouTube/Handlers/NotificationHandler.cs | 2 +- .../Unit/NotificationHandlerTests.cs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/NotificationHandlerTests.cs diff --git a/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs b/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs index c7257e3..7d809f7 100644 --- a/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs +++ b/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs @@ -9,7 +9,7 @@ internal static class NotificationHandler [FromServices] IDiscordRestClient discordRestClient, [FromServices] IOptionsMonitor discordNotificationOptions, [FromServices] ILastPostedStreamStore lastPostedStream, - CancellationToken cancellationToken + CancellationToken cancellationToken = default ) { using StreamReader stream = new(context.Request.Body); diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/NotificationHandlerTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/NotificationHandlerTests.cs new file mode 100644 index 0000000..64e8ccc --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/NotificationHandlerTests.cs @@ -0,0 +1,6 @@ +namespace StevesBot.Webhook.Tests.Unit; + +public class NotificationHandlerTests +{ + +} \ No newline at end of file From 3d0321bc42f9afacef521ab0ae52d90eabe45c4f Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 2 Jun 2025 11:11:36 -0500 Subject: [PATCH 22/31] tests: add test for notification handler --- .../YouTube/Handlers/NotificationHandler.cs | 6 +- .../StevesBot.Webhook.Tests.csproj | 1 + .../Unit/NotificationHandlerTests.cs | 201 ++++++++++++++++++ 3 files changed, 205 insertions(+), 3 deletions(-) diff --git a/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs b/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs index 7d809f7..b8b3680 100644 --- a/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs +++ b/src/src/StevesBot.Webhook/YouTube/Handlers/NotificationHandler.cs @@ -8,7 +8,7 @@ internal static class NotificationHandler [FromServices] IYouTubeDataApiClient youTubeDataApiClient, [FromServices] IDiscordRestClient discordRestClient, [FromServices] IOptionsMonitor discordNotificationOptions, - [FromServices] ILastPostedStreamStore lastPostedStream, + [FromServices] ILastPostedStreamStore lastPostedStreamStore, CancellationToken cancellationToken = default ) { @@ -40,13 +40,13 @@ internal static class NotificationHandler return Results.Ok(); } - if (lastPostedStream.HasValue(videoId)) + if (lastPostedStreamStore.HasValue(videoId)) { logger.LogInformation("Video ID {VideoId} has already been posted. Skipping notification.", videoId); return Results.Ok(); } - lastPostedStream.SetValue(videoId); + lastPostedStreamStore.SetValue(videoId); logger.LogInformation( "Video ID {VideoId} is a live stream. Creating discord message in channel {ChannelId}", diff --git a/src/tests/StevesBot.Webhook.Tests/StevesBot.Webhook.Tests.csproj b/src/tests/StevesBot.Webhook.Tests/StevesBot.Webhook.Tests.csproj index eedb9b8..b2ad87a 100644 --- a/src/tests/StevesBot.Webhook.Tests/StevesBot.Webhook.Tests.csproj +++ b/src/tests/StevesBot.Webhook.Tests/StevesBot.Webhook.Tests.csproj @@ -27,6 +27,7 @@ ./TestResults/Coverage/ cobertura [StevesBot.Webhook]* + [StevesBot.Webhook]System.Text.RegularExpressions.Generated.* **/Program.cs,**/SubscriptionWorker.cs diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/NotificationHandlerTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/NotificationHandlerTests.cs index 64e8ccc..8321783 100644 --- a/src/tests/StevesBot.Webhook.Tests/Unit/NotificationHandlerTests.cs +++ b/src/tests/StevesBot.Webhook.Tests/Unit/NotificationHandlerTests.cs @@ -1,6 +1,207 @@ +using System.Globalization; +using System.Text; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +using StevesBot.Library.Discord.Rest; +using StevesBot.Library.Discord.Rest.Requests; +using StevesBot.Webhook.YouTube.Handlers; + namespace StevesBot.Webhook.Tests.Unit; public class NotificationHandlerTests { + private readonly Mock _mockHttpRequest = new(); + private readonly Mock _mockHttpContext = new(); + private readonly Mock> _mockLogger = new(); + private readonly Mock _mockYtDataApiClient = new(); + private readonly Mock _mockDiscordRestClient = new(); + private readonly Mock> _mockNotificationsOptions = new(); + private readonly Mock _mockLastPostedStore = new(); + public NotificationHandlerTests() + { + _mockHttpContext + .Setup(static x => x.Request) + .Returns(_mockHttpRequest.Object); + } + + [Fact] + public async Task HandleAsync_WhenVideoIdNotFound_ItShouldReturnBadRequest() + { + SetupMockRequestBodyStream("This is a test"); + + var result = await HandleAsync(); + + result.Should().BeOfType>(); + } + + [Fact] + public async Task HandleAsync_WhenVideoIsNotFound_ItShouldReturnNotFound() + { + SetupMockRequestBodyStream("videoId"); + + _mockYtDataApiClient + .Setup(static x => x.GetVideoByIdAsync( + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .ReturnsAsync(null as YouTubeVideo); + + var result = await HandleAsync(); + + result.Should().BeOfType(); + } + + [Fact] + public async Task HandleAsync_WhenVideoIsFoundButIsNotALiveStream_ItShouldReturnOkButNotCreateDiscordMessage() + { + SetupMockRequestBodyStream("videoId"); + + _mockYtDataApiClient + .Setup(static x => x.GetVideoByIdAsync( + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .ReturnsAsync(new YouTubeVideo()); + + var result = await HandleAsync(); + + result.Should().BeOfType(); + + _mockLastPostedStore + .Verify(static x => x.SetValue(It.IsAny()), Times.Never); + + _mockDiscordRestClient + .Verify( + static x => x.CreateMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Never + ); + } + + [Fact] + public async Task HandleAsync_WhenVideoIsFoundButItHasSameIdAsLastVideo_ItShouldReturnOkButNotCreateDiscordMessage() + { + var videoId = "video_id"; + + SetupMockRequestBodyStream($"{videoId}"); + + _mockYtDataApiClient + .Setup(static x => x.GetVideoByIdAsync( + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .ReturnsAsync(new YouTubeVideo() + { + Id = videoId, + LiveStreamingDetails = new(), + Snippet = new() + { + LiveBroadcastContent = "live" + } + }); + + _mockLastPostedStore + .Setup(static x => x.HasValue(It.IsAny())) + .Returns(true); + + var result = await HandleAsync(); + + result.Should().BeOfType(); + + _mockLastPostedStore + .Verify(static x => x.SetValue(It.IsAny()), Times.Never); + + _mockDiscordRestClient + .Verify( + static x => x.CreateMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Never + ); + } + + [Fact] + public async Task HandleAsync_WhenVideoIsFoundAndItIsANewLiveStream_ItShouldReturnOkStoreTheIdAndCreateADiscordMessage() + { + var videoId = "video_id"; + + SetupMockRequestBodyStream($"{videoId}"); + + _mockYtDataApiClient + .Setup(static x => x.GetVideoByIdAsync( + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .ReturnsAsync(new YouTubeVideo() + { + Id = videoId, + LiveStreamingDetails = new(), + Snippet = new() + { + LiveBroadcastContent = "live" + } + }); + + _mockLastPostedStore + .Setup(static x => x.HasValue(It.IsAny())) + .Returns(false); + + _mockNotificationsOptions + .Setup(x => x.CurrentValue) + .Returns(new DiscordNotificationOptions()); + + var result = await HandleAsync(); + + result.Should().BeOfType(); + + _mockLastPostedStore + .Verify( + x => x.SetValue( + It.Is(s => string.Equals(s, videoId, StringComparison.Ordinal)) + ), + Times.Once + ); + + _mockDiscordRestClient + .Verify( + static x => x.CreateMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + } + + private void SetupMockRequestBodyStream(string content) + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + _mockHttpRequest.Setup(static x => x.Body).Returns(stream); + } + + private Task HandleAsync() + { + return NotificationHandler.HandleAsync( + _mockHttpContext.Object, + _mockLogger.Object, + _mockYtDataApiClient.Object, + _mockDiscordRestClient.Object, + _mockNotificationsOptions.Object, + _mockLastPostedStore.Object + ); + } } \ No newline at end of file From 68ba283856aa8b5e17e506ed901807f806d2116e Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:31:09 -0500 Subject: [PATCH 23/31] tests: more tests --- src/src/StevesBot.Webhook/Program.cs | 2 +- .../Handlers/VerifySubscriptionHandler.cs | 6 ++-- .../Unit/NotificationHandlerTests.cs | 2 -- .../Unit/VerifySubscriptionHandlerTests.cs | 30 +++++++++++++++++++ src/tests/StevesBot.Webhook.Tests/Usings.cs | 10 ++++++- 5 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 src/tests/StevesBot.Webhook.Tests/Unit/VerifySubscriptionHandlerTests.cs diff --git a/src/src/StevesBot.Webhook/Program.cs b/src/src/StevesBot.Webhook/Program.cs index 5f876b8..18ef1f0 100644 --- a/src/src/StevesBot.Webhook/Program.cs +++ b/src/src/StevesBot.Webhook/Program.cs @@ -67,7 +67,7 @@ if (app.Environment.IsDevelopment()) const string ytCallback = "yt-callback"; -app.MapGet(ytCallback, VerifySubscriptionHandler.HandleAsync); +app.MapGet(ytCallback, VerifySubscriptionHandler.Handle); app.MapPost(ytCallback, NotificationHandler.HandleAsync); app.Run(); \ No newline at end of file diff --git a/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs b/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs index a334b25..506dac2 100644 --- a/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs +++ b/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs @@ -2,7 +2,7 @@ namespace StevesBot.Webhook.YouTube.Handlers; internal static class VerifySubscriptionHandler { - public static IResult HandleAsync( + public static IResult Handle( [FromQuery(Name = "hub.mode")] string mode, [FromQuery(Name = "hub.topic")] string topic, [FromQuery(Name = "hub.reason")] string? reason, @@ -19,7 +19,7 @@ internal static class VerifySubscriptionHandler return Results.BadRequest("Subscription denied"); } - if (topic != subOptions.Value.TopicUrl) + if (string.Equals(topic, subOptions.Value.TopicUrl, StringComparison.OrdinalIgnoreCase) is false) { logger.LogInformation("Received verification request for wrong topic: {Topic}", topic); return Results.NotFound(); @@ -34,7 +34,7 @@ internal static class VerifySubscriptionHandler leaseSeconds ); - if (string.IsNullOrWhiteSpace(leaseSeconds) || !long.TryParse(leaseSeconds, out var parsedSeconds)) + if (string.IsNullOrWhiteSpace(leaseSeconds) || long.TryParse(leaseSeconds, out var parsedSeconds) is false) { logger.LogWarning("Invalid or missing lease_seconds parameter: {LeaseSeconds}", leaseSeconds); return Results.BadRequest("Invalid lease_seconds parameter"); diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/NotificationHandlerTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/NotificationHandlerTests.cs index 8321783..5ba7615 100644 --- a/src/tests/StevesBot.Webhook.Tests/Unit/NotificationHandlerTests.cs +++ b/src/tests/StevesBot.Webhook.Tests/Unit/NotificationHandlerTests.cs @@ -1,9 +1,7 @@ -using System.Globalization; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using StevesBot.Library.Discord.Rest; diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/VerifySubscriptionHandlerTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/VerifySubscriptionHandlerTests.cs new file mode 100644 index 0000000..75cb231 --- /dev/null +++ b/src/tests/StevesBot.Webhook.Tests/Unit/VerifySubscriptionHandlerTests.cs @@ -0,0 +1,30 @@ +namespace StevesBot.Webhook.Tests.Unit; + +public class VerifySubscriptionHandlerTests +{ + private readonly Mock> _mockSubOptions = new(); + private readonly Mock> _mockLogger = new(); + private readonly ConcurrentQueue _subscriptionQueue = new(); + + [Fact] + public void Handle_WhenModeIsDenied_ItShouldReturnBadRequest() + { + var result = Handle("denied", "topic"); + + result.Should().BeOfType>(); + } + + private IResult Handle(string mode, string topic, string? reason = null, string? challenge = null, string? leaseSeconds = null) + { + return VerifySubscriptionHandler.Handle( + mode, + topic, + reason, + challenge, + leaseSeconds, + _mockSubOptions.Object, + _mockLogger.Object, + _subscriptionQueue + ); + } +} \ No newline at end of file diff --git a/src/tests/StevesBot.Webhook.Tests/Usings.cs b/src/tests/StevesBot.Webhook.Tests/Usings.cs index aadd939..ed26c30 100644 --- a/src/tests/StevesBot.Webhook.Tests/Usings.cs +++ b/src/tests/StevesBot.Webhook.Tests/Usings.cs @@ -1,11 +1,19 @@ +global using System.Collections.Concurrent; global using System.Net; +global using System.Text; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Http.HttpResults; global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; global using Moq; global using RichardSzalay.MockHttp; +global using StevesBot.Library.Discord.Rest; +global using StevesBot.Library.Discord.Rest.Requests; global using StevesBot.Webhook.YouTube; global using StevesBot.Webhook.YouTube.Data; -global using StevesBot.Webhook.YouTube.Tasks; +global using StevesBot.Webhook.YouTube.Handlers; +global using StevesBot.Webhook.YouTube.Tasks; \ No newline at end of file From e616bdeddaac8de5078d1d6531c9649dd011b335 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:44:02 -0500 Subject: [PATCH 24/31] tests: finish adding tests for webhook --- .../Handlers/VerifySubscriptionHandler.cs | 5 +- .../Unit/VerifySubscriptionHandlerTests.cs | 95 ++++++++++++++++++- 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs b/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs index 506dac2..be548f6 100644 --- a/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs +++ b/src/src/StevesBot.Webhook/YouTube/Handlers/VerifySubscriptionHandler.cs @@ -10,7 +10,8 @@ internal static class VerifySubscriptionHandler [FromQuery(Name = "hub.lease_seconds")] string? leaseSeconds, [FromServices] IOptions subOptions, [FromServices] ILogger logger, - [FromServices] ConcurrentQueue subscriptionQueue + [FromServices] ConcurrentQueue subscriptionQueue, + [FromServices] TimeProvider timeProvider ) { if (mode is "denied") @@ -44,7 +45,7 @@ internal static class VerifySubscriptionHandler { CallbackUrl = subOptions.Value.CallbackUrl, TopicUrl = topic, - ExpiresAt = DateTime.UtcNow.AddSeconds(parsedSeconds), + ExpiresAt = timeProvider.GetUtcNow().AddSeconds(parsedSeconds), }; subscriptionQueue.Enqueue(task); diff --git a/src/tests/StevesBot.Webhook.Tests/Unit/VerifySubscriptionHandlerTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/VerifySubscriptionHandlerTests.cs index 75cb231..4c3f278 100644 --- a/src/tests/StevesBot.Webhook.Tests/Unit/VerifySubscriptionHandlerTests.cs +++ b/src/tests/StevesBot.Webhook.Tests/Unit/VerifySubscriptionHandlerTests.cs @@ -1,3 +1,5 @@ +using System.Globalization; + namespace StevesBot.Webhook.Tests.Unit; public class VerifySubscriptionHandlerTests @@ -5,6 +7,7 @@ public class VerifySubscriptionHandlerTests private readonly Mock> _mockSubOptions = new(); private readonly Mock> _mockLogger = new(); private readonly ConcurrentQueue _subscriptionQueue = new(); + private readonly Mock _mockTimeProvider = new(); [Fact] public void Handle_WhenModeIsDenied_ItShouldReturnBadRequest() @@ -14,6 +17,95 @@ public class VerifySubscriptionHandlerTests result.Should().BeOfType>(); } + [Fact] + public void Handle_WhenTopicDoesNotMatch_ItShouldReturnNotFound() + { + SetupSubscriptionOptions(new() { TopicUrl = "expected-topic" }); + + var result = Handle("subscribe", "wrong-topic"); + + result.Should().BeOfType(); + } + + [Fact] + public void Handle_WhenModeIsSubscribeAndLeaseSecondsIsNotPresent_ItShouldReturnBadRequest() + { + var topic = "topic"; + + SetupSubscriptionOptions(new() { TopicUrl = topic }); + + var result = Handle("subscribe", topic, leaseSeconds: null); + + result.Should().BeOfType>(); + } + + [Fact] + public void Handle_WhenModeIsSubscribeAndLeaseSecondsIsInvalid_ItShouldReturnBadRequest() + { + var topic = "topic"; + + SetupSubscriptionOptions(new() { TopicUrl = topic }); + + var result = Handle("subscribe", topic, leaseSeconds: "invalid"); + + result.Should().BeOfType>(); + } + + [Fact] + public void Handle_WhenModeIsUnsubscribe_ItShouldReturnOk() + { + var topic = "topic"; + + SetupSubscriptionOptions(new() { TopicUrl = topic }); + + var result = Handle("unsubscribe", topic, challenge: "challenge"); + + result.Should().BeOfType(); + } + + [Fact] + public void Handle_WhenModeIsSubscribe_ItShouldEnqueueSubscriptionTask() + { + var challenge = "challenge"; + var topic = "topic"; + var leaseSeconds = 3600; + var callbackUrl = "https://example.com/callback"; + var now = DateTime.UtcNow; + + _mockTimeProvider + .Setup(static tp => tp.GetUtcNow()) + .Returns(now); + + SetupSubscriptionOptions(new() { TopicUrl = topic, CallbackUrl = callbackUrl }); + + var result = Handle( + "subscribe", + topic, + challenge: challenge, + leaseSeconds: leaseSeconds.ToString(CultureInfo.InvariantCulture) + ); + + result.Should().BeOfType(); + result.As().ResponseContent.Should().Be(challenge); + + _subscriptionQueue.Count.Should().Be(1); + + var task = _subscriptionQueue.TryDequeue(out var dequeuedTask); + + task.Should().BeTrue(); + dequeuedTask.Should().NotBeNull(); + dequeuedTask!.CallbackUrl.Should().Be(callbackUrl); + dequeuedTask.TopicUrl.Should().Be(topic); + dequeuedTask.ExpiresAt.Should().Be(now.AddSeconds(leaseSeconds)); + } + + private void SetupSubscriptionOptions(SubscriptionOptions options) + { + _mockSubOptions + .Setup(static o => o.Value) + .Returns(options); + } + private IResult Handle(string mode, string topic, string? reason = null, string? challenge = null, string? leaseSeconds = null) { return VerifySubscriptionHandler.Handle( @@ -24,7 +116,8 @@ public class VerifySubscriptionHandlerTests leaseSeconds, _mockSubOptions.Object, _mockLogger.Object, - _subscriptionQueue + _subscriptionQueue, + _mockTimeProvider.Object ); } } \ No newline at end of file From 852f27c7b5daa2fd56192efbb445b70c394493bd Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:35:04 -0500 Subject: [PATCH 25/31] chore: rework dockerfiles to account for multi project solution with centralized package+build prop management --- src/Dockerfile | 14 ------------- src/StevesBot.Webhook.Dockerfile | 35 ++++++++++++++++++++++++++++++++ src/StevesBot.Worker.Dockerfile | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 14 deletions(-) delete mode 100644 src/Dockerfile create mode 100644 src/StevesBot.Webhook.Dockerfile create mode 100644 src/StevesBot.Worker.Dockerfile diff --git a/src/Dockerfile b/src/Dockerfile deleted file mode 100644 index 83404cb..0000000 --- a/src/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS base -WORKDIR /app -COPY ./StevesBot.Worker/StevesBot.Worker.csproj ./ -COPY ./Directory.Build.props ./ -RUN dotnet restore -COPY ./StevesBot.Worker/. ./ - -FROM base AS publish-stage -RUN dotnet publish -c Release -o dist - -FROM mcr.microsoft.com/dotnet/aspnet:9.0 -WORKDIR /app -COPY --from=publish-stage /app/dist ./ -ENTRYPOINT ["dotnet", "StevesBot.Worker.dll"] \ No newline at end of file diff --git a/src/StevesBot.Webhook.Dockerfile b/src/StevesBot.Webhook.Dockerfile new file mode 100644 index 0000000..f9909e1 --- /dev/null +++ b/src/StevesBot.Webhook.Dockerfile @@ -0,0 +1,35 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS base +WORKDIR /app + +# solution level +COPY *.sln ./ +COPY Directory.Build.props ./ + +# src level +COPY src/StevesBot.Library/*.csproj src/StevesBot.Library/ +COPY src/StevesBot.Worker/*.csproj src/StevesBot.Worker/ +COPY src/StevesBot.Webhook/*.csproj src/StevesBot.Webhook/ +COPY src/Directory.Build.props src/ +COPY src/Directory.Packages.props src/ + +# test level +COPY tests/StevesBot.Library.Tests/*.csproj tests/StevesBot.Library.Tests/ +COPY tests/StevesBot.Worker.Tests/*.csproj tests/StevesBot.Worker.Tests/ +COPY tests/StevesBot.Webhook.Tests/*.csproj tests/StevesBot.Webhook.Tests/ +COPY tests/Directory.Build.props tests/ +COPY tests/Directory.Packages.props tests/ + +COPY ./src/StevesBot.Worker/StevesBot.Worker.csproj ./ +COPY ./Directory.Build.props ./ + +RUN dotnet restore StevesBot.sln + +COPY . . + +FROM base AS publish-stage +RUN dotnet publish -c Release -o dist src/StevesBot.Webhook/StevesBot.Webhook.csproj + +FROM mcr.microsoft.com/dotnet/aspnet:9.0 +WORKDIR /app +COPY --from=publish-stage /app/dist ./ +ENTRYPOINT ["dotnet", "StevesBot.Webhook.dll"] diff --git a/src/StevesBot.Worker.Dockerfile b/src/StevesBot.Worker.Dockerfile new file mode 100644 index 0000000..0a199f3 --- /dev/null +++ b/src/StevesBot.Worker.Dockerfile @@ -0,0 +1,35 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS base +WORKDIR /app + +# solution level +COPY *.sln ./ +COPY Directory.Build.props ./ + +# src level +COPY src/StevesBot.Library/*.csproj src/StevesBot.Library/ +COPY src/StevesBot.Worker/*.csproj src/StevesBot.Worker/ +COPY src/StevesBot.Webhook/*.csproj src/StevesBot.Webhook/ +COPY src/Directory.Build.props src/ +COPY src/Directory.Packages.props src/ + +# test level +COPY tests/StevesBot.Library.Tests/*.csproj tests/StevesBot.Library.Tests/ +COPY tests/StevesBot.Worker.Tests/*.csproj tests/StevesBot.Worker.Tests/ +COPY tests/StevesBot.Webhook.Tests/*.csproj tests/StevesBot.Webhook.Tests/ +COPY tests/Directory.Build.props tests/ +COPY tests/Directory.Packages.props tests/ + +COPY ./src/StevesBot.Worker/StevesBot.Worker.csproj ./ +COPY ./Directory.Build.props ./ + +RUN dotnet restore StevesBot.sln + +COPY . . + +FROM base AS publish-stage +RUN dotnet publish -c Release -o dist src/StevesBot.Worker/StevesBot.Worker.csproj + +FROM mcr.microsoft.com/dotnet/aspnet:9.0 +WORKDIR /app +COPY --from=publish-stage /app/dist ./ +ENTRYPOINT ["dotnet", "StevesBot.Worker.dll"] From 8e0ec2d3d5fcb60869852dc3743dca77c329ecf0 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 3 Jun 2025 16:31:09 -0500 Subject: [PATCH 26/31] refactor: moving telemetry wip --- src/compose.yml | 40 +++++++++++++++++++ .../StevesBotWebhookInstrumentation.cs | 4 +- .../StevesBotWorkerInstrumentation.cs | 4 +- src/src/StevesBot.Worker/Usings.cs | 8 ++-- .../Unit/HostExtensionsTests.cs | 18 +++++++-- .../StevesBotWebhookInstrumentationTests.cs | 2 +- src/tests/StevesBot.Webhook.Tests/Usings.cs | 2 + .../StevesBotWorkerInstrumentationTests.cs | 2 +- src/tests/StevesBot.Worker.Tests/Usings.cs | 6 +-- 9 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 src/compose.yml rename src/src/{StevesBot.Library => StevesBot.Webhook}/Telemetry/StevesBotWebhookInstrumentation.cs (78%) rename src/src/{StevesBot.Library => StevesBot.Worker}/Telemetry/StevesBotWorkerInstrumentation.cs (79%) rename src/tests/{StevesBot.Library.Tests => StevesBot.Webhook.Tests}/Unit/StevesBotWebhookInstrumentationTests.cs (95%) rename src/tests/{StevesBot.Library.Tests => StevesBot.Worker.Tests}/Unit/StevesBotWorkerInstrumentationTests.cs (95%) diff --git a/src/compose.yml b/src/compose.yml new file mode 100644 index 0000000..dfa8537 --- /dev/null +++ b/src/compose.yml @@ -0,0 +1,40 @@ +name: StevesBot +services: + webhook: + container_name: steves-bot-webhook + image: stevanfreeborn/steves-bot:${WEBHOOK_VERSION} + ports: + - '34827:8080' + restart: always + environment: + - Logging__LogLevel__Default=Debug + - Logging__LogLevel__Microsoft=Information + - Logging__LogLevel__Microsoft.Hosting.Lifetime=Information + - YouTubeClientOptions__BaseUrl=${YOUTUBE_BASE_URL} + - YouTubeClientOptions__ApiKey=${YOUTUBE_API_KEY} + - SubscriptionOptions__CallbackUrl=${SUBSCRIPTION_CALLBACK_URL} + - SubscriptionOptions__TopicUrl=${SUBSCRIPTION_TOPIC_URL} + - PubSubClientOptions__BaseUrl=${PUBSUB_BASE_URL} + - DiscordClientOptions__ApiUrl=${DISCORD_API_URL} + - DiscordClientOptions__AppToken=${DISCORD_APP_TOKEN} + - DiscordClientOptions__Intents=${DISCORD_INTENTS} + - DiscordNotificationOptions__ChannelId=${DISCORD_NOTIFICATION_CHANNEL_ID} + - DiscordNotificationOptions__MessageFormat="${DISCORD_NOTIFICATION_MESSAGE_FORMAT}" + - SeqOptions__ServerUrl=${SEQ_SERVER_URL} + - SeqOptions__ApiKeyHeader=${SEQ_API_KEY_HEADER} + - SeqOptions__ApiKey=${SEQ_API_KEY} + - DOTNET_ENVIRONMENT=Production + worker: + container_name: steves-bot-worker + image: stevanfreeborn/steves-bot:${WORKER_VERSION} + environment: + - Logging__LogLevel__Default=Debug + - Logging__LogLevel__Microsoft=Information + - Logging__LogLevel__Microsoft.Hosting.Lifetime=Information + - DiscordClientOptions__ApiUrl=${DISCORD_API_URL} + - DiscordClientOptions__AppToken=${DISCORD_APP_TOKEN} + - DiscordClientOptions__Intents=${DISCORD_INTENTS} + - SeqOptions__ServerUrl=${SEQ_SERVER_URL} + - SeqOptions__ApiKeyHeader=${SEQ_API_KEY_HEADER} + - SeqOptions__ApiKey=${SEQ_API_KEY} + - DOTNET_ENVIRONMENT=Production diff --git a/src/src/StevesBot.Library/Telemetry/StevesBotWebhookInstrumentation.cs b/src/src/StevesBot.Webhook/Telemetry/StevesBotWebhookInstrumentation.cs similarity index 78% rename from src/src/StevesBot.Library/Telemetry/StevesBotWebhookInstrumentation.cs rename to src/src/StevesBot.Webhook/Telemetry/StevesBotWebhookInstrumentation.cs index e56aefa..eb7a13a 100644 --- a/src/src/StevesBot.Library/Telemetry/StevesBotWebhookInstrumentation.cs +++ b/src/src/StevesBot.Webhook/Telemetry/StevesBotWebhookInstrumentation.cs @@ -1,8 +1,8 @@ using System.Diagnostics; -namespace StevesBot.Library.Telemetry; +namespace StevesBot.Webhook.Telemetry; -public sealed class StevesBotWebhookInstrumentation : IInstrumentation +internal sealed class StevesBotWebhookInstrumentation : IInstrumentation { private const string SourceNameValue = "StevesBot.Webhook"; private const string SourceVersionValue = "0.0.0"; diff --git a/src/src/StevesBot.Library/Telemetry/StevesBotWorkerInstrumentation.cs b/src/src/StevesBot.Worker/Telemetry/StevesBotWorkerInstrumentation.cs similarity index 79% rename from src/src/StevesBot.Library/Telemetry/StevesBotWorkerInstrumentation.cs rename to src/src/StevesBot.Worker/Telemetry/StevesBotWorkerInstrumentation.cs index de63a27..5bc4663 100644 --- a/src/src/StevesBot.Library/Telemetry/StevesBotWorkerInstrumentation.cs +++ b/src/src/StevesBot.Worker/Telemetry/StevesBotWorkerInstrumentation.cs @@ -1,8 +1,8 @@ using System.Diagnostics; -namespace StevesBot.Library.Telemetry; +namespace StevesBot.Worker.Telemetry; -public sealed class StevesBotWorkerInstrumentation : IInstrumentation +internal sealed class StevesBotWorkerInstrumentation : IInstrumentation { private const string SourceNameValue = "StevesBot.Worker"; private const string SourceVersionValue = "0.0.0"; diff --git a/src/src/StevesBot.Worker/Usings.cs b/src/src/StevesBot.Worker/Usings.cs index 11b7725..7dce256 100644 --- a/src/src/StevesBot.Worker/Usings.cs +++ b/src/src/StevesBot.Worker/Usings.cs @@ -7,16 +7,16 @@ global using System.Text.Json.Serialization; global using Microsoft.Extensions.Options; global using StevesBot.Library.Discord; +global using StevesBot.Library.Discord.Common; +global using StevesBot.Library.Discord.Rest; +global using StevesBot.Library.Discord.Rest.Requests; global using StevesBot.Library.Telemetry; - global using StevesBot.Worker; global using StevesBot.Worker.Discord; global using StevesBot.Worker.Discord.Gateway; global using StevesBot.Worker.Discord.Gateway.Events; global using StevesBot.Worker.Discord.Gateway.Events.Data; -global using StevesBot.Library.Discord.Rest; -global using StevesBot.Library.Discord.Rest.Requests; -global using StevesBot.Library.Discord.Common; global using StevesBot.Worker.Handlers; +global using StevesBot.Worker.Telemetry; global using StevesBot.Worker.Threading; global using StevesBot.Worker.WebSockets; \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs b/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs index 48a9200..f2eb30d 100644 --- a/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs @@ -7,12 +7,12 @@ public class HostExtensionsTests { var builder = WebApplication.CreateBuilder(); - builder.AddTelemetry(static () => new StevesBotWebhookInstrumentation()); + builder.AddTelemetry(static () => new TestInstrumentation()); var app = builder.Build(); app.Services - .GetService() + .GetService() .Should() .BeNull(); @@ -42,7 +42,7 @@ public class HostExtensionsTests builder.Configuration.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))); - builder.AddTelemetry(static () => new StevesBotWebhookInstrumentation()); + builder.AddTelemetry(static () => new TestInstrumentation()); var app = builder.Build(); @@ -62,4 +62,16 @@ public class HostExtensionsTests .Should() .NotBeNull(); } + + private class TestInstrumentation : IInstrumentation + { + public string SourceName { get; } = "TestSource"; + public string SourceVersion { get; } = "1.0.0"; + public ActivitySource Source { get; } = new ActivitySource("TestSource", "1.0.0"); + + public void Dispose() + { + Source.Dispose(); + } + } } \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/Unit/StevesBotWebhookInstrumentationTests.cs b/src/tests/StevesBot.Webhook.Tests/Unit/StevesBotWebhookInstrumentationTests.cs similarity index 95% rename from src/tests/StevesBot.Library.Tests/Unit/StevesBotWebhookInstrumentationTests.cs rename to src/tests/StevesBot.Webhook.Tests/Unit/StevesBotWebhookInstrumentationTests.cs index 8eea5f8..723836b 100644 --- a/src/tests/StevesBot.Library.Tests/Unit/StevesBotWebhookInstrumentationTests.cs +++ b/src/tests/StevesBot.Webhook.Tests/Unit/StevesBotWebhookInstrumentationTests.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Library.Tests.Unit; +namespace StevesBot.Webhook.Tests.Unit; public sealed class StevesBotWebhookInstrumentationTests : IDisposable { diff --git a/src/tests/StevesBot.Webhook.Tests/Usings.cs b/src/tests/StevesBot.Webhook.Tests/Usings.cs index ed26c30..9cba30d 100644 --- a/src/tests/StevesBot.Webhook.Tests/Usings.cs +++ b/src/tests/StevesBot.Webhook.Tests/Usings.cs @@ -1,4 +1,5 @@ global using System.Collections.Concurrent; +global using System.Diagnostics; global using System.Net; global using System.Text; @@ -13,6 +14,7 @@ global using RichardSzalay.MockHttp; global using StevesBot.Library.Discord.Rest; global using StevesBot.Library.Discord.Rest.Requests; +global using StevesBot.Webhook.Telemetry; global using StevesBot.Webhook.YouTube; global using StevesBot.Webhook.YouTube.Data; global using StevesBot.Webhook.YouTube.Handlers; diff --git a/src/tests/StevesBot.Library.Tests/Unit/StevesBotWorkerInstrumentationTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/StevesBotWorkerInstrumentationTests.cs similarity index 95% rename from src/tests/StevesBot.Library.Tests/Unit/StevesBotWorkerInstrumentationTests.cs rename to src/tests/StevesBot.Worker.Tests/Unit/StevesBotWorkerInstrumentationTests.cs index 3d94aa6..6372b24 100644 --- a/src/tests/StevesBot.Library.Tests/Unit/StevesBotWorkerInstrumentationTests.cs +++ b/src/tests/StevesBot.Worker.Tests/Unit/StevesBotWorkerInstrumentationTests.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Library.Tests.Unit; +namespace StevesBot.Worker.Tests.Unit; public sealed class StevesBotWorkerInstrumentationTests : IDisposable { diff --git a/src/tests/StevesBot.Worker.Tests/Usings.cs b/src/tests/StevesBot.Worker.Tests/Usings.cs index eeafbcc..fff654f 100644 --- a/src/tests/StevesBot.Worker.Tests/Usings.cs +++ b/src/tests/StevesBot.Worker.Tests/Usings.cs @@ -1,4 +1,4 @@ -global using System.Net; +global using System.Diagnostics; global using System.Net.WebSockets; global using System.Text; global using System.Text.Json; @@ -14,9 +14,6 @@ global using Microsoft.Extensions.Logging; global using Moq; -global using RichardSzalay.MockHttp; - -global using StevesBot.Library.Discord; global using StevesBot.Library.Discord.Common; global using StevesBot.Library.Discord.Rest; global using StevesBot.Library.Discord.Rest.Requests; @@ -24,6 +21,7 @@ global using StevesBot.Worker.Discord; global using StevesBot.Worker.Discord.Gateway; global using StevesBot.Worker.Discord.Gateway.Events; global using StevesBot.Worker.Discord.Gateway.Events.Data; +global using StevesBot.Worker.Telemetry; global using StevesBot.Worker.Tests.Integration.Infrastructure; global using StevesBot.Worker.Threading; global using StevesBot.Worker.WebSockets; \ No newline at end of file From 8c800ce6580a8ddceefb4858e7bb3a2a03a8b9c2 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:46:21 -0500 Subject: [PATCH 27/31] chore: working on preparations for deployment --- .github/workflows/deploy.yml | 68 ++++++++++++------- .github/workflows/pull_request.yml | 6 +- src/compose.yml | 4 +- src/src/StevesBot.Webhook/Usings.cs | 1 + .../Unit/HostExtensionsTests.cs | 2 +- .../Unit/DiscordGatewayClientTests.cs | 2 - .../StevesBotWorkerInstrumentationTests.cs | 2 + src/tests/StevesBot.Worker.Tests/Usings.cs | 1 - 8 files changed, 54 insertions(+), 32 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1f1dac1..0d71e9d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,7 +8,7 @@ on: - 'src/**' jobs: build: - name: Build Docker Image + name: Build Docker Images runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.version }} @@ -25,14 +25,27 @@ jobs: run: echo "version=$(date +%Y.%m.%d.%H%M%S)" >> $GITHUB_OUTPUT - name: Build Docker image run: | - TAG=${{ secrets.DOCKERHUB_USERNAME }}/steves-bot:${{ steps.version.outputs.version }} - docker build -t $TAG src - docker push $TAG + WORKER_TAG=${{ secrets.DOCKERHUB_USERNAME }}/steves-bot:worker-${{ steps.version.outputs.version }} + docker build -f src/StevesBot.Worker.Dockerfile -t $WORKER_TAG src + docker push $WORKER_TAG + WEBHOOK_TAG=${{ secrets.DOCKERHUB_USERNAME }}/steves-bot:webhook-${{ steps.version.outputs.version }} + docker build -f src/StevesBot.Webhook.Dockerfile -t $WEBHOOK_TAG src + docker push $WEBHOOK_TAG deploy: - name: Deploy Docker Image + name: Deploy Docker Images runs-on: ubuntu-latest needs: build steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Copy compose file to server + uses: appleboy/scp-action@v1 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USERNAME }} + key: ${{ secrets.SSH_KEY }} + source: src/compose.yml + target: steves-bot - name: Run image on server uses: appleboy/ssh-action@v1 with: @@ -42,25 +55,32 @@ jobs: script: | cd steves-bot - echo 'Logging__LogLevel__Default=Debug' >> .env - echo 'Logging__LogLevel__Microsoft=Debug' >> .env - echo 'Logging__LogLevel__Microsoft.Hosting.Lifetime=Debug' >> .env + echo 'WORKER_VERSION=worker-${{ needs.build.outputs.version }}' > .env + echo 'WEBHOOK_VERSION=webhook-${{ needs.build.outputs.version }}' > .env + echo 'DiscordClientOptions__ApiUrl=${{ vars.DISCORD_API_URL }}' >> .env echo 'DiscordClientOptions__AppToken=${{ secrets.DISCORD_APP_TOKEN }}' >> .env - echo 'DiscordClientOptions__Intents=${{ vars.DISCORD_INTENTS }}' >> .env - echo 'SeqOptions__ServerUrl=${{ secrets.SEQ_SERVER_URL }}' >> .env - echo 'SeqOptions__ApiKeyHeader=${{ vars.SEQ_API_KEY_HEADER }}' >> .env - echo 'SeqOptions__ApiKey=${{ secrets.SEQ_API_KEY }}' >> .env - echo 'DOTNET_ENVIRONMENT=Production' >> .env + echo 'DiscordClientOptions__Intents=${{ vars.DISCORD_INTENTS }}' >> env - docker stop steves-bot - docker rm steves-bot - docker pull ${{ secrets.DOCKERHUB_USERNAME }}/steves-bot:${{ needs.build.outputs.version }} - docker run \ - --name steves-bot \ - --restart always \ - --env-file .env \ - -d \ - ${{ secrets.DOCKERHUB_USERNAME }}/steves-bot:${{ needs.build.outputs.version }} - - rm .env \ No newline at end of file + echo 'SEQ_SERVER_URL=${{ secrets.SEQ_SERVER_URL }}' >> .env + echo 'SEQ_API_KEY_HEADER=${{ vars.SEQ_API_KEY_HEADER }}' >> .env + echo 'SEQ_WORKER_API_KEY=${{ secrets.SEQ_WORKER_API_KEY }}' >> .env + echo 'SEQ_WEBHOOK_API_KEY=${{ secrets.SEQ_WEBHOOK_API_KEY }}' >> .env + + echo 'YOUTUBE_BASE_URL=${{ vars.YOUTUBE_BASE_URL }}' >> .env + echo 'YOUTUBE_API_KEY=${{ secrets.YOUTUBE_API_KEY }}' >> .env + + echo 'SUBSCRIPTION_CALLBACK_URL=${{ secrets.SUBSCRIPTION_CALLBACK_URL }}' >> .env + echo 'SUBSCRIPTION_TOPIC_URL=${{ secrets.SUBSCRIPTION_TOPIC_URL }}' >> .env + + echo 'PUBSUB_BASE_URL=${{ vars.PUBSUB_BASE_URL }}' >> .env + + echo 'DISCORD_NOTIFICATION_CHANNEL_ID=${{ vars.DISCORD_NOTIFICATION_CHANNEL_ID }}' >> .env + echo 'DISCORD_NOTIFICATION_MESSAGE_FORMAT="${{ vars.DISCORD_NOTIFICATION_MESSAGE_FORMAT }}"' >> .env + + docker compose -f compose.yml down + docker compose -f compose.yml pull + docker compose -f compose.yml up -d --wait + + rm compose.yml + rm .env diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index bf79a98..6e685ba 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -33,8 +33,10 @@ jobs: run: dotnet tool install -g dotnet-reportgenerator-globaltool - name: Run tests run: dotnet test src + - name: Merge coverage reports + run: reportgenerator "-reports:src/tests/*/TestResults/*/coverage.cobertura.xml" "-reporttypes:Html;Cobertura" "-targetdir:src/tests/TestResults/Coverage" - name: Upload code coverage uses: actions/upload-artifact@v4 with: - name: html-report - path: src/StevesBot.Worker.Tests/TestResults/Coverage/Report/ + name: coverage-reports + path: src/tests/TestResults/Coverage diff --git a/src/compose.yml b/src/compose.yml index dfa8537..c87c1c3 100644 --- a/src/compose.yml +++ b/src/compose.yml @@ -22,7 +22,7 @@ services: - DiscordNotificationOptions__MessageFormat="${DISCORD_NOTIFICATION_MESSAGE_FORMAT}" - SeqOptions__ServerUrl=${SEQ_SERVER_URL} - SeqOptions__ApiKeyHeader=${SEQ_API_KEY_HEADER} - - SeqOptions__ApiKey=${SEQ_API_KEY} + - SeqOptions__ApiKey=${SEQ_WEBHOOK_API_KEY} - DOTNET_ENVIRONMENT=Production worker: container_name: steves-bot-worker @@ -36,5 +36,5 @@ services: - DiscordClientOptions__Intents=${DISCORD_INTENTS} - SeqOptions__ServerUrl=${SEQ_SERVER_URL} - SeqOptions__ApiKeyHeader=${SEQ_API_KEY_HEADER} - - SeqOptions__ApiKey=${SEQ_API_KEY} + - SeqOptions__ApiKey=${SEQ_WORKER_API_KEY} - DOTNET_ENVIRONMENT=Production diff --git a/src/src/StevesBot.Webhook/Usings.cs b/src/src/StevesBot.Webhook/Usings.cs index 7694e5f..e15daf7 100644 --- a/src/src/StevesBot.Webhook/Usings.cs +++ b/src/src/StevesBot.Webhook/Usings.cs @@ -13,6 +13,7 @@ global using StevesBot.Library.Discord.Common; global using StevesBot.Library.Discord.Rest; global using StevesBot.Library.Discord.Rest.Requests; global using StevesBot.Library.Telemetry; +global using StevesBot.Webhook.Telemetry; global using StevesBot.Webhook.YouTube; global using StevesBot.Webhook.YouTube.Data; global using StevesBot.Webhook.YouTube.Handlers; diff --git a/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs b/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs index f2eb30d..76fc960 100644 --- a/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs +++ b/src/tests/StevesBot.Library.Tests/Unit/HostExtensionsTests.cs @@ -63,7 +63,7 @@ public class HostExtensionsTests .NotBeNull(); } - private class TestInstrumentation : IInstrumentation + private sealed class TestInstrumentation : IInstrumentation { public string SourceName { get; } = "TestSource"; public string SourceVersion { get; } = "1.0.0"; diff --git a/src/tests/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs index 2eec841..4e5fa3b 100644 --- a/src/tests/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs +++ b/src/tests/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs @@ -1,5 +1,3 @@ -using System.ComponentModel; - namespace StevesBot.Worker.Tests.Unit; public sealed class DiscordGatewayClientTests : IDisposable diff --git a/src/tests/StevesBot.Worker.Tests/Unit/StevesBotWorkerInstrumentationTests.cs b/src/tests/StevesBot.Worker.Tests/Unit/StevesBotWorkerInstrumentationTests.cs index 6372b24..9326314 100644 --- a/src/tests/StevesBot.Worker.Tests/Unit/StevesBotWorkerInstrumentationTests.cs +++ b/src/tests/StevesBot.Worker.Tests/Unit/StevesBotWorkerInstrumentationTests.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace StevesBot.Worker.Tests.Unit; public sealed class StevesBotWorkerInstrumentationTests : IDisposable diff --git a/src/tests/StevesBot.Worker.Tests/Usings.cs b/src/tests/StevesBot.Worker.Tests/Usings.cs index fff654f..7bbe18c 100644 --- a/src/tests/StevesBot.Worker.Tests/Usings.cs +++ b/src/tests/StevesBot.Worker.Tests/Usings.cs @@ -1,4 +1,3 @@ -global using System.Diagnostics; global using System.Net.WebSockets; global using System.Text; global using System.Text.Json; From 17b5bdea73e2f1b24a93e5ff009ea730995850b2 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:33:50 -0500 Subject: [PATCH 28/31] docs: update README.md --- README.md | 119 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 3c9d534..274080e 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,32 @@ # Steve's Bot πŸ€– -This is a Discord bot built with .NET 9, designed to be a full-featured and extensible solution for having my own assistant in my [Discord server](https://discord.stevanfreeborn.com). +This is a comprehensive Discord bot platform built with .NET 9, designed to be a full-featured and extensible solution for having my own assistant in my [Discord server](https://discord.stevanfreeborn.com). ## ✨ Features +### Core Bot Platform + - **Custom Discord Gateway Client**: Full-featured implementation with: - WebSocket connection management - Automatic heartbeat handling - Session resumption and reconnection logic - Event-driven architecture +- **Shared Library**: Common Discord REST client and telemetry components - **Observability**: Built-in telemetry with OpenTelemetry support - **Resilient Architecture**: Graceful error handling and automatic recovery -- **Containerized Deployment**: Ready for Docker deployment + +### YouTube Integration + +- **YouTube Webhook Service**: Web API for receiving YouTube notifications +- **Live Stream Detection**: Automatic detection and Discord notifications for live streams +- **PubSubHubbub Integration**: YouTube webhook subscription management +- **Stream Deduplication**: Prevents duplicate notifications using in-memory store + +### Deployment & Operations + +- **Multi-Service Architecture**: Separate worker and webhook services +- **Containerized Deployment**: Docker containers with Docker Compose orchestration +- **Production Ready**: Environment-specific configuration and logging ## πŸš€ Quick Start @@ -19,13 +34,16 @@ This is a Discord bot built with .NET 9, designed to be a full-featured and exte - [.NET 9 SDK](https://dotnet.microsoft.com/download) - Discord Bot Token (from [Discord Developer Portal](https://discord.com/developers/applications)) +- For YouTube integration: YouTube Data API v3 key from [Google Cloud Console](https://console.cloud.google.com/apis/credentials) ### Configuration +#### Worker Service (Discord Bot) + 1. Copy the example configuration: ```powershell - Copy-Item src/StevesBot.Worker/appsettings.Example.json src/StevesBot.Worker/appsettings.Development.json + Copy-Item src/src/StevesBot.Worker/appsettings.Example.json src/src/StevesBot.Worker/appsettings.Development.json ``` 2. Update `appsettings.Development.json` with your Discord bot credentials: @@ -40,26 +58,29 @@ This is a Discord bot built with .NET 9, designed to be a full-featured and exte } ``` -### Running the Bot +#### Webhook Service (YouTube Integration) + +1. Copy the example configuration: + + ```powershell + Copy-Item src/src/StevesBot.Webhook/appsettings.Example.json src/src/StevesBot.Webhook/appsettings.Development.json + ``` + +2. Update with your API keys and callback URLs for YouTube integration. + +### Running the Services #### Using VS Code Tasks ```powershell -# Build the project +# Build the entire solution dotnet build src/StevesBot.sln -# Run the bot -dotnet run --project src/StevesBot.Worker -``` +# Run the Discord bot worker +dotnet run --project src/src/StevesBot.Worker -#### Using Docker - -```powershell -# Build the Docker image -docker build -t steves-bot src/ - -# Run the container -docker run -d --name steves-bot steves-bot +# Run the YouTube webhook service (in separate terminal) +dotnet run --project src/src/StevesBot.Webhook ``` ## πŸ—οΈ Architecture @@ -68,28 +89,58 @@ docker run -d --name steves-bot steves-bot ```txt src/ -β”œβ”€β”€ StevesBot.Worker/ # Main bot application -β”‚ β”œβ”€β”€ Discord/ # Discord client implementation -β”‚ β”‚ β”œβ”€β”€ Gateway/ # WebSocket gateway client -β”‚ β”‚ β”œβ”€β”€ Rest/ # REST API client -β”‚ β”‚ └── Shared/ # Common Discord models -β”‚ β”œβ”€β”€ Handlers/ # Event handlers -β”‚ β”œβ”€β”€ Telemetry/ # Observability setup -β”‚ β”œβ”€β”€ Threading/ # Async utilities -β”‚ └── WebSockets/ # WebSocket abstractions -└── StevesBot.Worker.Tests/ # Comprehensive test suite +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ StevesBot.Library/ # Shared library components +β”‚ β”‚ β”œβ”€β”€ Discord/ # Common Discord REST client +β”‚ β”‚ └── Telemetry/ # Shared telemetry setup +β”‚ β”œβ”€β”€ StevesBot.Worker/ # Discord bot worker service +β”‚ β”‚ β”œβ”€β”€ Discord/ # Discord Gateway client implementation +β”‚ β”‚ β”‚ β”œβ”€β”€ Gateway/ # WebSocket gateway client +β”‚ β”‚ β”‚ β”œβ”€β”€ Rest/ # REST API client +β”‚ β”‚ β”‚ └── Shared/ # Common Discord models +β”‚ β”‚ β”œβ”€β”€ Handlers/ # Discord event handlers +β”‚ β”‚ β”œβ”€β”€ Telemetry/ # Worker-specific telemetry +β”‚ β”‚ β”œβ”€β”€ Threading/ # Async utilities +β”‚ β”‚ └── WebSockets/ # WebSocket abstractions +β”‚ └── StevesBot.Webhook/ # YouTube webhook service +β”‚ β”œβ”€β”€ YouTube/ # YouTube integration components +β”‚ β”‚ β”œβ”€β”€ Data/ # YouTube API models +β”‚ β”‚ β”œβ”€β”€ Handlers/ # Webhook request handlers +β”‚ β”‚ └── Tasks/ # Background tasks +β”‚ └── Telemetry/ # Webhook-specific telemetry +β”œβ”€β”€ tests/ # Comprehensive test suites +β”‚ β”œβ”€β”€ StevesBot.Library.Tests/ # Shared library tests +β”‚ β”œβ”€β”€ StevesBot.Worker.Tests/ # Worker service tests +β”‚ └── StevesBot.Webhook.Tests/ # Webhook service tests +β”œβ”€β”€ compose.yml # Docker Compose configuration +β”œβ”€β”€ StevesBot.Worker.Dockerfile # Worker service container +└── StevesBot.Webhook.Dockerfile # Webhook service container ``` ### Key Components +#### Discord Bot Worker + - **DiscordGatewayClient**: Custom WebSocket client for Discord Gateway API - **Worker**: Background service that manages the bot lifecycle - **WebSocket Management**: Custom WebSocket factory and connection handling - **AsyncLock**: Thread-safe async locking mechanism +#### YouTube Webhook Service + +- **NotificationHandler**: Processes YouTube webhook notifications +- **SubscriptionWorker**: Manages YouTube PubSubHubbub subscriptions +- **YouTubeDataApiClient**: Integrates with YouTube Data API v3 +- **LastPostedStreamStore**: Prevents duplicate stream notifications + +#### Shared Library + +- **DiscordRestClient**: Reusable Discord REST API client +- **Telemetry Infrastructure**: OpenTelemetry setup and instrumentation + ## πŸ”§ Configuration -### Discord Client Options +### Discord Worker Options | Setting | Description | Required | |----------------------------------|-----------------------------------------|----------| @@ -97,6 +148,17 @@ src/ | `DiscordClientOptions__AppToken` | Bot token from Discord Developer Portal | Yes | | `DiscordClientOptions__Intents` | Discord Gateway intents | Yes | +### YouTube Webhook Options + +| Setting | Description | Required | +|----------------------------------------|---------------------------------------|----------| +| `YouTubeClientOptions__BaseUrl` | YouTube Data API base URL | Yes | +| `YouTubeClientOptions__ApiKey` | YouTube Data API v3 key | Yes | +| `SubscriptionOptions__CallbackUrl` | Webhook callback URL | Yes | +| `SubscriptionOptions__TopicUrl` | YouTube channel topic URL | Yes | +| `PubSubClientOptions__BaseUrl` | PubSubHubbub hub URL | Yes | +| `DiscordNotificationOptions__ChannelId` | Discord channel for notifications | Yes | + ### Telemetry Options | Setting | Description | Required | @@ -112,9 +174,6 @@ The project includes a comprehensive test suite with both unit and integration t ```powershell # Run all tests dotnet test src/StevesBot.sln - -# Run with coverage -dotnet test src/StevesBot.sln --collect:"XPlat Code Coverage" ``` ### Test Coverage From 3fa6195e18e90d5d2a41a42aa3e647e9c5d44c95 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:34:35 -0500 Subject: [PATCH 29/31] chore: run dotnet format --- src/src/StevesBot.Library/Discord/Rest/DiscordRestClient.cs | 2 +- src/src/StevesBot.Library/Discord/Rest/IDiscordRestClient.cs | 2 +- src/src/StevesBot.Worker/Program.cs | 2 +- src/tests/StevesBot.Library.Tests/Usings.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/src/StevesBot.Library/Discord/Rest/DiscordRestClient.cs b/src/src/StevesBot.Library/Discord/Rest/DiscordRestClient.cs index 4e251e6..6995683 100644 --- a/src/src/StevesBot.Library/Discord/Rest/DiscordRestClient.cs +++ b/src/src/StevesBot.Library/Discord/Rest/DiscordRestClient.cs @@ -2,9 +2,9 @@ using System.Net.Http.Json; using Microsoft.Extensions.Logging; +using StevesBot.Library.Discord.Common; using StevesBot.Library.Discord.Rest.Requests; using StevesBot.Library.Discord.Rest.Responses; -using StevesBot.Library.Discord.Common; namespace StevesBot.Library.Discord.Rest; diff --git a/src/src/StevesBot.Library/Discord/Rest/IDiscordRestClient.cs b/src/src/StevesBot.Library/Discord/Rest/IDiscordRestClient.cs index dd057ef..8368ff5 100644 --- a/src/src/StevesBot.Library/Discord/Rest/IDiscordRestClient.cs +++ b/src/src/StevesBot.Library/Discord/Rest/IDiscordRestClient.cs @@ -1,5 +1,5 @@ -using StevesBot.Library.Discord.Rest.Requests; using StevesBot.Library.Discord.Common; +using StevesBot.Library.Discord.Rest.Requests; namespace StevesBot.Library.Discord.Rest; diff --git a/src/src/StevesBot.Worker/Program.cs b/src/src/StevesBot.Worker/Program.cs index 36654b0..c0c6111 100644 --- a/src/src/StevesBot.Worker/Program.cs +++ b/src/src/StevesBot.Worker/Program.cs @@ -26,4 +26,4 @@ builder.Services.AddHostedService(); var host = builder.Build(); -await host.RunAsync(); +await host.RunAsync(); \ No newline at end of file diff --git a/src/tests/StevesBot.Library.Tests/Usings.cs b/src/tests/StevesBot.Library.Tests/Usings.cs index ebcc03a..398cb8d 100644 --- a/src/tests/StevesBot.Library.Tests/Usings.cs +++ b/src/tests/StevesBot.Library.Tests/Usings.cs @@ -20,4 +20,4 @@ global using StevesBot.Library.Discord.Common; global using StevesBot.Library.Discord.Rest; global using StevesBot.Library.Discord.Rest.Requests; global using StevesBot.Library.Discord.Rest.Responses; -global using StevesBot.Library.Telemetry; +global using StevesBot.Library.Telemetry; \ No newline at end of file From 9d8f5269b8e0cfa4005edaa7ec8564c79b4ffddf Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:43:11 -0500 Subject: [PATCH 30/31] chore: rename file --- .../Directory.packages.props => testsDirectory.Packages.props} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{tests/Directory.packages.props => testsDirectory.Packages.props} (100%) diff --git a/src/tests/Directory.packages.props b/src/testsDirectory.Packages.props similarity index 100% rename from src/tests/Directory.packages.props rename to src/testsDirectory.Packages.props From 770995f22e33016f5f5a861398cc79275a7a7278 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:48:54 -0500 Subject: [PATCH 31/31] chore: fix bad file rename --- .../Directory.Packages.props} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{testsDirectory.Packages.props => tests/Directory.Packages.props} (100%) diff --git a/src/testsDirectory.Packages.props b/src/tests/Directory.Packages.props similarity index 100% rename from src/testsDirectory.Packages.props rename to src/tests/Directory.Packages.props