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