From b86481e0d3cea28df6a44d0a06068d139c6d8194 Mon Sep 17 00:00:00 2001 From: John Korsnes Date: Mon, 13 Sep 2021 15:12:03 +0200 Subject: [PATCH 1/5] From upstream --- .../Abstractions/IHandleAppMentions.cs | 2 +- .../Abstractions/INoOpAppMentions.cs | 7 +++++++ .../AppMentionEventHandlerSelector.cs | 13 ++++++++++--- .../Hosting/ISlackbotHandlersBuilder.cs | 2 +- .../Hosting/SlackBotHandlersBuilder.cs | 6 ++++++ 5 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 source/src/Slackbot.Net.Endpoints/Abstractions/INoOpAppMentions.cs diff --git a/source/src/Slackbot.Net.Endpoints/Abstractions/IHandleAppMentions.cs b/source/src/Slackbot.Net.Endpoints/Abstractions/IHandleAppMentions.cs index bd12ab9..57c5c02 100644 --- a/source/src/Slackbot.Net.Endpoints/Abstractions/IHandleAppMentions.cs +++ b/source/src/Slackbot.Net.Endpoints/Abstractions/IHandleAppMentions.cs @@ -6,7 +6,7 @@ namespace Slackbot.Net.Endpoints.Abstractions public interface IHandleAppMentions { Task Handle(EventMetaData eventMetadata, AppMentionEvent slackEvent); - bool ShouldHandle(AppMentionEvent slackEvent); + bool ShouldHandle(AppMentionEvent slackEvent) => true; (string HandlerTrigger, string Description) GetHelpDescription() => ("", ""); } } \ No newline at end of file diff --git a/source/src/Slackbot.Net.Endpoints/Abstractions/INoOpAppMentions.cs b/source/src/Slackbot.Net.Endpoints/Abstractions/INoOpAppMentions.cs new file mode 100644 index 0000000..d0c2844 --- /dev/null +++ b/source/src/Slackbot.Net.Endpoints/Abstractions/INoOpAppMentions.cs @@ -0,0 +1,7 @@ +namespace Slackbot.Net.Endpoints.Abstractions +{ + public interface INoOpAppMentions : IHandleAppMentions + { + + } +} \ No newline at end of file diff --git a/source/src/Slackbot.Net.Endpoints/AppMentionEventHandlerSelector.cs b/source/src/Slackbot.Net.Endpoints/AppMentionEventHandlerSelector.cs index 15e5485..5eb5e29 100644 --- a/source/src/Slackbot.Net.Endpoints/AppMentionEventHandlerSelector.cs +++ b/source/src/Slackbot.Net.Endpoints/AppMentionEventHandlerSelector.cs @@ -24,6 +24,7 @@ public async Task> GetAppMentionEventHandlerFor( { var allHandlers = _provider.GetServices(); var shortCutter = _provider.GetService(); + var noopHandler = _provider.GetService(); if (shortCutter != null && shortCutter.ShouldShortcut(slackEvent)) { @@ -31,17 +32,23 @@ public async Task> GetAppMentionEventHandlerFor( return new List(); } - return SelectHandler(allHandlers, slackEvent); + return SelectHandler(allHandlers, noopHandler, slackEvent); } - private IEnumerable SelectHandler(IEnumerable handlers, AppMentionEvent message) + private IEnumerable SelectHandler(IEnumerable handlers, INoOpAppMentions noOpAppMentions, AppMentionEvent message) { var matchingHandlers = handlers.Where(s => s.ShouldHandle(message)); if (matchingHandlers.Any()) return matchingHandlers; - return new List {new NoOpAppMentionEventHandler(_loggerFactory.CreateLogger())}; + if(noOpAppMentions != null) + return new List { noOpAppMentions }; + + return new List + { + new NoOpAppMentionEventHandler(_loggerFactory.CreateLogger()) + }; } } } \ No newline at end of file diff --git a/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs b/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs index 503de29..7832e5a 100644 --- a/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs +++ b/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs @@ -9,7 +9,7 @@ public interface ISlackbotHandlersBuilder public ISlackbotHandlersBuilder AddMemberJoinedChannelHandler() where T : class, IHandleMemberJoinedChannel; public ISlackbotHandlersBuilder AddViewSubmissionHandler() where T : class, IHandleViewSubmissions; public ISlackbotHandlersBuilder AddInteractiveBlockActionsHandler() where T : class, IHandleInteractiveBlockActions; - public ISlackbotHandlersBuilder AddAppHomeOpenedHandler() where T : class, IHandleAppHomeOpened; + public ISlackbotHandlersBuilder AddNoOpAppMentionHandler() where T : class, INoOpAppMentions; } } \ No newline at end of file diff --git a/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs b/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs index d8c2c29..a16c035 100644 --- a/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs +++ b/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs @@ -47,5 +47,11 @@ public ISlackbotHandlersBuilder AddShortcut() where T : class, IShortcutAppMe _services.AddSingleton(); return this; } + + public ISlackbotHandlersBuilder AddNoOpAppMentionHandler() where T : class, INoOpAppMentions + { + _services.AddSingleton(); + return this; + } } } \ No newline at end of file From 4ce141e070015fcce7d8cde84cb09e00012afca3 Mon Sep 17 00:00:00 2001 From: John Korsnes Date: Mon, 13 Sep 2021 15:44:48 +0200 Subject: [PATCH 2/5] Adds verification/authentication of slack events Uses the slack signature header --- .github/workflows/CI.yml | 4 + .github/workflows/PreRelease.yml | 4 + .github/workflows/Release.yml | 4 + .../EventHandlers/AppHomeOpenedHandler.cs | 19 ++-- .../AppHomeViewSubmissionHandler.cs | 14 ++- .../EventHandlers/HelloWorldHandler.cs | 23 ++-- .../EventHandlers/HiddenTestHandler.cs | 22 ++-- .../EventHandlers/ListPublicCommands.cs | 55 +++++----- .../MemberJoinedChannelHandler.cs | 17 ++- .../EventHandlers/PublicJokeHandler.cs | 23 ++-- Samples/HelloWorld/HelloWorld.csproj | 4 +- Samples/HelloWorld/MyTokenStore.cs | 71 ++++++------ Samples/HelloWorld/Program.cs | 45 +++----- Samples/HelloWorld/test.http | 4 +- source/.editorconfig | 3 + source/Slackbot.Net.sln | 3 +- .../AppMentionEventHandlerSelector.cs | 4 +- .../AuthenticationBuilderExtensions.cs | 27 +++++ ...entsAuthenticationAuthenticationHandler.cs | 102 ++++++++++++++++++ .../Hosting/IAppBuilderExtensions.cs | 10 +- .../Hosting/ISlackbotHandlersBuilder.cs | 3 +- .../Hosting/SlackBotHandlersBuilder.cs | 2 +- .../Middlewares/HttpItemsManager.cs | 12 +-- .../SlackbotEventAuthMiddleware.cs | 43 ++++++++ .../Slackbot.Net.Endpoints.csproj | 8 +- .../Slackbot.Net.SlackClients.Http.csproj | 13 ++- 26 files changed, 347 insertions(+), 192 deletions(-) create mode 100644 source/src/Slackbot.Net.Endpoints/Authentication/AuthenticationBuilderExtensions.cs create mode 100644 source/src/Slackbot.Net.Endpoints/Authentication/SlackbotEventsAuthenticationAuthenticationHandler.cs create mode 100644 source/src/Slackbot.Net.Endpoints/Middlewares/SlackbotEventAuthMiddleware.cs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2e0a96d..d5b4ce0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -20,6 +20,10 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: 5.0.x + - name: Setup .NET 6 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x - name: Restore dependencies run: dotnet restore source - name: Build diff --git a/.github/workflows/PreRelease.yml b/.github/workflows/PreRelease.yml index db37146..fc7c207 100644 --- a/.github/workflows/PreRelease.yml +++ b/.github/workflows/PreRelease.yml @@ -24,6 +24,10 @@ jobs: uses: gittools/actions/gitversion/setup@v0.9.7 with: versionSpec: "5.x" + - name: Setup .NET 6 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x - name: Determine Version id: gitversion uses: gittools/actions/gitversion/execute@v0.9.7 diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index d52be8f..9a603d0 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -24,6 +24,10 @@ jobs: uses: gittools/actions/gitversion/setup@v0.9.7 with: versionSpec: "5.x" + - name: Setup .NET 6 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x - name: Determine Version id: gitversion uses: gittools/actions/gitversion/execute@v0.9.7 diff --git a/Samples/HelloWorld/EventHandlers/AppHomeOpenedHandler.cs b/Samples/HelloWorld/EventHandlers/AppHomeOpenedHandler.cs index 9b8fea7..2bf5bbf 100644 --- a/Samples/HelloWorld/EventHandlers/AppHomeOpenedHandler.cs +++ b/Samples/HelloWorld/EventHandlers/AppHomeOpenedHandler.cs @@ -1,18 +1,15 @@ -using System; -using System.Threading.Tasks; using Newtonsoft.Json; using Slackbot.Net.Endpoints.Abstractions; using Slackbot.Net.Endpoints.Models.Events; -namespace HelloWorld.EventHandlers +namespace HelloWorld.EventHandlers; + +public class AppHomeOpenedHandler : IHandleAppHomeOpened { - public class AppHomeOpenedHandler : IHandleAppHomeOpened + public Task Handle(EventMetaData eventMetadata, AppHomeOpenedEvent payload) { - public Task Handle(EventMetaData eventMetadata, AppHomeOpenedEvent payload) - { - string json = JsonConvert.SerializeObject(payload); - Console.WriteLine(json); - return Task.FromResult(new EventHandledResponse(json)); - } + string json = JsonConvert.SerializeObject(payload); + Console.WriteLine(json); + return Task.FromResult(new EventHandledResponse(json)); } -} \ No newline at end of file +} diff --git a/Samples/HelloWorld/EventHandlers/AppHomeViewSubmissionHandler.cs b/Samples/HelloWorld/EventHandlers/AppHomeViewSubmissionHandler.cs index 4384514..9261f5c 100644 --- a/Samples/HelloWorld/EventHandlers/AppHomeViewSubmissionHandler.cs +++ b/Samples/HelloWorld/EventHandlers/AppHomeViewSubmissionHandler.cs @@ -1,14 +1,12 @@ -using System.Threading.Tasks; using Slackbot.Net.Endpoints.Abstractions; using Slackbot.Net.Endpoints.Models.Interactive.ViewSubmissions; -namespace HelloWorld.EventHandlers +namespace HelloWorld.EventHandlers; + +public class AppHomeViewSubmissionHandler : IHandleViewSubmissions { - public class AppHomeViewSubmissionHandler : IHandleViewSubmissions + public Task Handle(ViewSubmission payload) { - public Task Handle(ViewSubmission payload) - { - return Task.FromResult(new EventHandledResponse("YoLO!")); - } + return Task.FromResult(new EventHandledResponse("YoLO!")); } -} \ No newline at end of file +} diff --git a/Samples/HelloWorld/EventHandlers/HelloWorldHandler.cs b/Samples/HelloWorld/EventHandlers/HelloWorldHandler.cs index 2a1aea2..d10c165 100644 --- a/Samples/HelloWorld/EventHandlers/HelloWorldHandler.cs +++ b/Samples/HelloWorld/EventHandlers/HelloWorldHandler.cs @@ -1,20 +1,17 @@ -using System; -using System.Threading.Tasks; using Slackbot.Net.Endpoints.Abstractions; using Slackbot.Net.Endpoints.Models.Events; -namespace HelloWorld.EventHandlers +namespace HelloWorld.EventHandlers; + +internal class HelloWorldHandler : IHandleAppMentions { - internal class HelloWorldHandler : IHandleAppMentions + public Task Handle(EventMetaData eventMetadata, AppMentionEvent message) { - public Task Handle(EventMetaData eventMetadata, AppMentionEvent message) - { - Console.WriteLine($"Hello world, {message.User}\n"); - return Task.FromResult(new EventHandledResponse("Responded")); - } + Console.WriteLine($"Hello world, {message.User}\n"); + return Task.FromResult(new EventHandledResponse("Responded")); + } - public bool ShouldHandle(AppMentionEvent appMention) => appMention.Text.Contains("hw"); + public bool ShouldHandle(AppMentionEvent appMention) => appMention.Text.Contains("hw"); - public (string HandlerTrigger, string Description) GetHelpDescription() => ("hw", "Returns a hello world message"); - } -} \ No newline at end of file + public (string HandlerTrigger, string Description) GetHelpDescription() => ("hw", "Returns a hello world message"); +} diff --git a/Samples/HelloWorld/EventHandlers/HiddenTestHandler.cs b/Samples/HelloWorld/EventHandlers/HiddenTestHandler.cs index e5d786c..585cb65 100644 --- a/Samples/HelloWorld/EventHandlers/HiddenTestHandler.cs +++ b/Samples/HelloWorld/EventHandlers/HiddenTestHandler.cs @@ -1,20 +1,16 @@ -using System; -using System.Threading.Tasks; using Newtonsoft.Json; using Slackbot.Net.Endpoints.Abstractions; using Slackbot.Net.Endpoints.Models.Events; -namespace HelloWorld.EventHandlers +namespace HelloWorld.EventHandlers; + +public class HiddenTestHandler : IHandleAppMentions { - public class HiddenTestHandler : IHandleAppMentions + public Task Handle(EventMetaData eventMetadata, AppMentionEvent slackEvent) { - public Task Handle(EventMetaData eventMetadata, AppMentionEvent slackEvent) - { - Console.WriteLine("Doing stuff from OtherAppMentionHandler: " + JsonConvert.SerializeObject(slackEvent)); - return Task.FromResult(new EventHandledResponse("Wrote stuff to log")); - } - - public bool ShouldHandle(AppMentionEvent appMentionEvent) => appMentionEvent.Text == "test"; - + Console.WriteLine("Doing stuff from OtherAppMentionHandler: " + JsonConvert.SerializeObject(slackEvent)); + return Task.FromResult(new EventHandledResponse("Wrote stuff to log")); } -} \ No newline at end of file + + public bool ShouldHandle(AppMentionEvent appMentionEvent) => appMentionEvent.Text == "test"; +} diff --git a/Samples/HelloWorld/EventHandlers/ListPublicCommands.cs b/Samples/HelloWorld/EventHandlers/ListPublicCommands.cs index ce6ca6c..c80b70b 100644 --- a/Samples/HelloWorld/EventHandlers/ListPublicCommands.cs +++ b/Samples/HelloWorld/EventHandlers/ListPublicCommands.cs @@ -1,39 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Slackbot.Net.Endpoints.Abstractions; using Slackbot.Net.Endpoints.Models.Events; -namespace HelloWorld.EventHandlers +namespace HelloWorld.EventHandlers; + +public class ListPublicCommands : IShortcutAppMentions { - public class ListPublicCommands : IShortcutAppMentions - { - private readonly IEnumerable _handlers; - + private readonly IEnumerable _handlers; + - public ListPublicCommands(IEnumerable allHandlers) - { - _handlers = allHandlers; - } + public ListPublicCommands(IEnumerable allHandlers) + { + _handlers = allHandlers; + } - public Task Handle(EventMetaData eventMetadata, AppMentionEvent @event) - { - var publicHandlersToBeListedBack = _handlers.Where(handler => handler.GetHelpDescription().HandlerTrigger != string.Empty) - - .Aggregate("*Public trigger mentions*", (current, kvPair) => current + $"\n• `{kvPair.GetType()}. `{kvPair.GetHelpDescription().HandlerTrigger}` : _{kvPair.GetHelpDescription().Description}_"); - - - Console.WriteLine(publicHandlersToBeListedBack); - - - var ssh = _handlers.Where(handler => handler.GetHelpDescription().HandlerTrigger == string.Empty) - .Aggregate("*Private trigger handlers*", (current, kvPair) => current + $"\n• `{kvPair.GetType()}`"); - Console.WriteLine(ssh); + public Task Handle(EventMetaData eventMetadata, AppMentionEvent @event) + { + var publicHandlersToBeListedBack = _handlers.Where(handler => handler.GetHelpDescription().HandlerTrigger != string.Empty) - return Task.CompletedTask; - } + .Aggregate("*Public trigger mentions*", (current, kvPair) => current + $"\n• `{kvPair.GetType()}. `{kvPair.GetHelpDescription().HandlerTrigger}` : _{kvPair.GetHelpDescription().Description}_"); + + + Console.WriteLine(publicHandlersToBeListedBack); + + + var ssh = _handlers.Where(handler => handler.GetHelpDescription().HandlerTrigger == string.Empty) + .Aggregate("*Private trigger handlers*", (current, kvPair) => current + $"\n• `{kvPair.GetType()}`"); + Console.WriteLine(ssh); - public bool ShouldShortcut(AppMentionEvent appMention) => appMention.Text.Contains("help"); + return Task.CompletedTask; } -} \ No newline at end of file + + public bool ShouldShortcut(AppMentionEvent appMention) => appMention.Text.Contains("help"); +} diff --git a/Samples/HelloWorld/EventHandlers/MemberJoinedChannelHandler.cs b/Samples/HelloWorld/EventHandlers/MemberJoinedChannelHandler.cs index 54001fc..e3833db 100644 --- a/Samples/HelloWorld/EventHandlers/MemberJoinedChannelHandler.cs +++ b/Samples/HelloWorld/EventHandlers/MemberJoinedChannelHandler.cs @@ -1,17 +1,14 @@ -using System; -using System.Threading.Tasks; using Newtonsoft.Json; using Slackbot.Net.Endpoints.Abstractions; using Slackbot.Net.Endpoints.Models.Events; -namespace HelloWorld.EventHandlers +namespace HelloWorld.EventHandlers; + +public class MemberJoinedChannelHandler : IHandleMemberJoinedChannel { - public class MemberJoinedChannelHandler : IHandleMemberJoinedChannel + public Task Handle(EventMetaData eventMetadata, MemberJoinedChannelEvent slackEvent) { - public Task Handle(EventMetaData eventMetadata, MemberJoinedChannelEvent slackEvent) - { - Console.WriteLine("Doing stuff from MemberJoinedChannelHandler: " + JsonConvert.SerializeObject(slackEvent)); - return Task.FromResult(new EventHandledResponse("Wrote stuff to log")); - } + Console.WriteLine("Doing stuff from MemberJoinedChannelHandler: " + JsonConvert.SerializeObject(slackEvent)); + return Task.FromResult(new EventHandledResponse("Wrote stuff to log")); } -} \ No newline at end of file +} diff --git a/Samples/HelloWorld/EventHandlers/PublicJokeHandler.cs b/Samples/HelloWorld/EventHandlers/PublicJokeHandler.cs index 9de9939..a9ddc0a 100644 --- a/Samples/HelloWorld/EventHandlers/PublicJokeHandler.cs +++ b/Samples/HelloWorld/EventHandlers/PublicJokeHandler.cs @@ -1,21 +1,18 @@ -using System; -using System.Threading.Tasks; using Newtonsoft.Json; using Slackbot.Net.Endpoints.Abstractions; using Slackbot.Net.Endpoints.Models.Events; -namespace HelloWorld.EventHandlers +namespace HelloWorld.EventHandlers; + +public class PublicJokeHandler : IHandleAppMentions { - public class PublicJokeHandler : IHandleAppMentions + public Task Handle(EventMetaData eventMetadata, AppMentionEvent slackEvent) { - public Task Handle(EventMetaData eventMetadata, AppMentionEvent slackEvent) - { - Console.WriteLine("Doing stuff from AppmentionHandler: " + JsonConvert.SerializeObject(slackEvent)); - return Task.FromResult(new EventHandledResponse("Wrote stuff to log")); - } + Console.WriteLine("Doing stuff from AppmentionHandler: " + JsonConvert.SerializeObject(slackEvent)); + return Task.FromResult(new EventHandledResponse("Wrote stuff to log")); + } - public bool ShouldHandle(AppMentionEvent slackEvent) => slackEvent.Text == "test"; + public bool ShouldHandle(AppMentionEvent slackEvent) => slackEvent.Text == "test"; - public (string HandlerTrigger, string Description) GetHelpDescription() => ("telljoke", "bot tells a joke"); - } -} \ No newline at end of file + public (string HandlerTrigger, string Description) GetHelpDescription() => ("telljoke", "bot tells a joke"); +} diff --git a/Samples/HelloWorld/HelloWorld.csproj b/Samples/HelloWorld/HelloWorld.csproj index de80d6b..830fb4e 100644 --- a/Samples/HelloWorld/HelloWorld.csproj +++ b/Samples/HelloWorld/HelloWorld.csproj @@ -1,8 +1,10 @@ - net5.0 + net6.0 false + enable + preview diff --git a/Samples/HelloWorld/MyTokenStore.cs b/Samples/HelloWorld/MyTokenStore.cs index 89cd3ff..35d428f 100644 --- a/Samples/HelloWorld/MyTokenStore.cs +++ b/Samples/HelloWorld/MyTokenStore.cs @@ -1,48 +1,41 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Slackbot.Net.Abstractions.Hosting; -namespace HelloWorld +public class MyTokenStore : ITokenStore { - public class MyTokenStore : ITokenStore - { - private readonly List _workspaces; - public string SlackToken = Environment.GetEnvironmentVariable("Slackbot_SlackApiKey_BotUser"); + private readonly List _workspaces; + public string SlackToken = Environment.GetEnvironmentVariable("Slackbot_SlackApiKey_BotUser"); - public MyTokenStore() - { - _workspaces = new List() - { - new Workspace { - Token = SlackToken, - TeamId = "T0EC3DG3A" - } - }; - } - - public Task> GetTokens() + public MyTokenStore() + { + _workspaces = new List() { - return Task.FromResult(_workspaces.Select(c => c.Token)); - } + new Workspace { + Token = SlackToken, + TeamId = "T0EC3DG3A" + } + }; + } - public Task GetTokenByTeamId(string teamId) - { - return Task.FromResult(_workspaces.First(t => t.TeamId == teamId).Token); - } + public Task> GetTokens() + { + return Task.FromResult(_workspaces.Select(c => c.Token)); + } - public Task Delete(string token) - { - var workspaceToRemove = _workspaces.First(w => w.Token == token); - _workspaces.Remove(workspaceToRemove); - return Task.CompletedTask; - } + public Task GetTokenByTeamId(string teamId) + { + return Task.FromResult(_workspaces.First(t => t.TeamId == teamId).Token); + } - private class Workspace - { - public string Token { get; set; } - public string TeamId { get; set; } - } + public Task Delete(string token) + { + var workspaceToRemove = _workspaces.First(w => w.Token == token); + _workspaces.Remove(workspaceToRemove); + return Task.CompletedTask; + } + + private class Workspace + { + public string Token { get; set; } + public string TeamId { get; set; } } -} \ No newline at end of file +} diff --git a/Samples/HelloWorld/Program.cs b/Samples/HelloWorld/Program.cs index 5a9952a..b8f9828 100644 --- a/Samples/HelloWorld/Program.cs +++ b/Samples/HelloWorld/Program.cs @@ -1,35 +1,20 @@ using HelloWorld.EventHandlers; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; using Slackbot.Net.Endpoints.Hosting; using Slackbot.Net.SlackClients.Http.Extensions; +using Slackbot.Net.Endpoints.Authentication; -namespace HelloWorld -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddAuthentication().AddSlackbotEvents(c => c.SigningSecret = Environment.GetEnvironmentVariable("CLIENT_SIGNING_SECRET")); +builder.Services.AddSlackClientBuilder() + .AddSlackBotEvents() + .AddAppMentionHandler() + .AddAppMentionHandler() + .AddAppMentionHandler() + .AddMemberJoinedChannelHandler() + .AddShortcut() + .AddViewSubmissionHandler() + .AddAppHomeOpenedHandler(); - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.ConfigureServices(services => - { - services.AddSlackClientBuilder(); - services.AddSlackBotEvents() - .AddAppMentionHandler() - .AddAppMentionHandler() - .AddAppMentionHandler() - .AddMemberJoinedChannelHandler() - .AddShortcut() - .AddViewSubmissionHandler() - .AddAppHomeOpenedHandler(); - }); - webBuilder.Configure(app => app.UseSlackbot()); - }); - } -} \ No newline at end of file +var app = builder.Build(); +app.UseSlackbot(enableAuth: !app.Environment.IsDevelopment()); +app.Run(); diff --git a/Samples/HelloWorld/test.http b/Samples/HelloWorld/test.http index 4142799..e4000f6 100644 --- a/Samples/HelloWorld/test.http +++ b/Samples/HelloWorld/test.http @@ -1,5 +1,7 @@ # Mimicks a payload slack would send for app_mention events, like "@yourbot hw" in this case: POST http://localhost:1337/events +X-Slack-Request-Timestamp: 12331231 +X-Slack-Signature: v0:abc123etcetc { "team_id": "T0EC3DG3A", @@ -9,4 +11,4 @@ POST http://localhost:1337/events "text" : "<@BOT123> hw", "channel": "C92QZTVEF" } -} \ No newline at end of file +} diff --git a/source/.editorconfig b/source/.editorconfig index 3298f9b..750fc2f 100644 --- a/source/.editorconfig +++ b/source/.editorconfig @@ -1,6 +1,9 @@ # Remove the line below if you want to inherit .editorconfig settings from higher directories root = true +[*] +charset = utf-8 + # C# files [*.cs] diff --git a/source/Slackbot.Net.sln b/source/Slackbot.Net.sln index 7f58d44..4d72e79 100644 --- a/source/Slackbot.Net.sln +++ b/source/Slackbot.Net.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{53AD07F7-25C8-4F0F-A551-4979D2DD08F3}" EndProject @@ -14,7 +14,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{F22E EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{10CBAF56-F382-480E-B8BE-CBF270617AD8}" ProjectSection(SolutionItems) = preProject - ..\build.cake = ..\build.cake ..\CI.yml = ..\.github\workflows\CI.yml ..\Release.yml = ..\.github\workflows\Release.yml ..\PreRelease.yml = ..\.github\workflows\PreRelease.yml diff --git a/source/src/Slackbot.Net.Endpoints/AppMentionEventHandlerSelector.cs b/source/src/Slackbot.Net.Endpoints/AppMentionEventHandlerSelector.cs index 5eb5e29..c5bc05c 100644 --- a/source/src/Slackbot.Net.Endpoints/AppMentionEventHandlerSelector.cs +++ b/source/src/Slackbot.Net.Endpoints/AppMentionEventHandlerSelector.cs @@ -41,10 +41,10 @@ private IEnumerable SelectHandler(IEnumerable s.ShouldHandle(message)); if (matchingHandlers.Any()) return matchingHandlers; - + if(noOpAppMentions != null) return new List { noOpAppMentions }; - + return new List { new NoOpAppMentionEventHandler(_loggerFactory.CreateLogger()) diff --git a/source/src/Slackbot.Net.Endpoints/Authentication/AuthenticationBuilderExtensions.cs b/source/src/Slackbot.Net.Endpoints/Authentication/AuthenticationBuilderExtensions.cs new file mode 100644 index 0000000..818f06e --- /dev/null +++ b/source/src/Slackbot.Net.Endpoints/Authentication/AuthenticationBuilderExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; + +namespace Slackbot.Net.Endpoints.Authentication +{ + public static class AuthenticationBuilderExtensions + { + public static AuthenticationBuilder AddSlackbotEvents(this AuthenticationBuilder builder, Action optionsAction) + { + builder.Services.Configure(optionsAction); + return builder.AddScheme(SlackbotEventsAuthenticationConstants.AuthenticationScheme, optionsAction); + } + } + + public class SlackbotEventsAuthenticationOptions : AuthenticationSchemeOptions + { + [Required] + public string SigningSecret { get; set; } + } + + public static class SlackbotEventsAuthenticationConstants + { + public const string AuthenticationScheme = "SlacbotEventsAuthenticationScheme"; + } +} diff --git a/source/src/Slackbot.Net.Endpoints/Authentication/SlackbotEventsAuthenticationAuthenticationHandler.cs b/source/src/Slackbot.Net.Endpoints/Authentication/SlackbotEventsAuthenticationAuthenticationHandler.cs new file mode 100644 index 0000000..69340d7 --- /dev/null +++ b/source/src/Slackbot.Net.Endpoints/Authentication/SlackbotEventsAuthenticationAuthenticationHandler.cs @@ -0,0 +1,102 @@ +using System; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Slackbot.Net.Endpoints.Authentication +{ + internal class SlackbotEventsAuthenticationAuthenticationHandler : AuthenticationHandler + { + private const string TimestampHeaderName = "X-Slack-Request-Timestamp"; + private const string SignatureHeaderName = "X-Slack-Signature"; + + private readonly string _signingSecret; + + public SlackbotEventsAuthenticationAuthenticationHandler( + IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, + ISystemClock clock) : base(options, logger, encoder, clock) + { + if (string.IsNullOrEmpty(options.CurrentValue.SigningSecret)) + throw new ArgumentNullException(nameof(SlackbotEventsAuthenticationOptions.SigningSecret)); + _signingSecret = options.CurrentValue.SigningSecret; + } + + protected override async Task HandleAuthenticateAsync() + { + IHeaderDictionary headers = Request.Headers; + + string timestamp = headers[TimestampHeaderName].FirstOrDefault(); + string signature = headers[SignatureHeaderName].FirstOrDefault(); + + if (timestamp == null) + { + return HandleRequestResult.Fail($"Missing header {TimestampHeaderName}"); + } + + if (signature == null) + { + return HandleRequestResult.Fail($"Missing header {SignatureHeaderName}"); + } + + bool isNumber = long.TryParse(timestamp, out long timestampAsLong); + + if (!isNumber) + { + return HandleRequestResult.Fail($"Invalid header. Header {TimestampHeaderName} not a number"); + } + + Request.EnableBuffering(); + using var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); + string body = await reader.ReadToEndAsync(); + Request.Body.Position = 0; + + if (IsValidSlackSignature(signature, timestampAsLong, body)) + { + return HandleRequestResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), SlackbotEventsAuthenticationConstants.AuthenticationScheme)); + } + + return HandleRequestResult.Fail("Verification of Slack request failed."); + + } + + private static readonly DateTime Seventies = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + private static long Now => (long)DateTime.UtcNow.Subtract(Seventies).TotalSeconds; + + private bool IsValidSlackSignature(string incomingSignature, long timestamp, string body) + { + if (!IsWithinRange(timestamp, TimeSpan.FromMinutes(5))) + { + return false; + } + + return incomingSignature == GeneratedSignature(timestamp, body); + } + + private static bool IsWithinRange(long timestamp, TimeSpan tolerance) + { + return Now - timestamp <= tolerance.TotalSeconds; + } + + private string GeneratedSignature(long timestamp, string body) + { + string signature = $"v0:{timestamp}:{body}"; + var hasher = new HMACSHA256(Encoding.UTF8.GetBytes(_signingSecret)); + byte[] hash = hasher.ComputeHash(Encoding.UTF8.GetBytes(signature)); + var builder = new StringBuilder("v0="); + foreach (byte part in hash) + { + builder.Append(part.ToString("x2")); + } + + return builder.ToString(); + } + } +} diff --git a/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs b/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs index 0ac5169..b366da3 100644 --- a/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs +++ b/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs @@ -6,10 +6,13 @@ namespace Slackbot.Net.Endpoints.Hosting { public static class IAppBuilderExtensions { - public static IApplicationBuilder UseSlackbot(this IApplicationBuilder app, string path = "/events") + public static IApplicationBuilder UseSlackbot(this IApplicationBuilder app, string path = "/events", bool enableAuth = true) { app.MapWhen(c => IsSlackRequest(c, path), a => { + if(enableAuth) + a.UseMiddleware(); + a.UseMiddleware(); a.MapWhen(Challenge.ShouldRun, b => b.UseMiddleware()); a.MapWhen(Uninstall.ShouldRun, b => b.UseMiddleware()); @@ -18,7 +21,7 @@ public static IApplicationBuilder UseSlackbot(this IApplicationBuilder app, stri a.MapWhen(AppHomeOpenedEvents.ShouldRun, b => b.UseMiddleware()); a.MapWhen(InteractiveEvents.ShouldRun, b => b.UseMiddleware()); }); - + return app; } @@ -27,4 +30,5 @@ private static bool IsSlackRequest(HttpContext ctx, string path) return ctx.Request.Path == path && ctx.Request.Method == "POST"; } } -} \ No newline at end of file +} + diff --git a/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs b/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs index 7832e5a..6f8b454 100644 --- a/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs +++ b/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs @@ -9,7 +9,8 @@ public interface ISlackbotHandlersBuilder public ISlackbotHandlersBuilder AddMemberJoinedChannelHandler() where T : class, IHandleMemberJoinedChannel; public ISlackbotHandlersBuilder AddViewSubmissionHandler() where T : class, IHandleViewSubmissions; public ISlackbotHandlersBuilder AddInteractiveBlockActionsHandler() where T : class, IHandleInteractiveBlockActions; + public ISlackbotHandlersBuilder AddAppHomeOpenedHandler() where T : class, IHandleAppHomeOpened; public ISlackbotHandlersBuilder AddNoOpAppMentionHandler() where T : class, INoOpAppMentions; } -} \ No newline at end of file +} diff --git a/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs b/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs index a16c035..4c43dd8 100644 --- a/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs +++ b/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs @@ -47,7 +47,7 @@ public ISlackbotHandlersBuilder AddShortcut() where T : class, IShortcutAppMe _services.AddSingleton(); return this; } - + public ISlackbotHandlersBuilder AddNoOpAppMentionHandler() where T : class, INoOpAppMentions { _services.AddSingleton(); diff --git a/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs b/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs index 3c2fc23..ae52177 100644 --- a/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs +++ b/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs @@ -34,7 +34,7 @@ public async Task Invoke(HttpContext context) if (body.StartsWith("{")) { var jObject = JObject.Parse(body); - + _logger.LogTrace(body); if (jObject.ContainsKey("challenge")) { context.Items.Add(HttpItemKeys.ChallengeKey, jObject["challenge"]); @@ -63,7 +63,7 @@ public async Task Invoke(HttpContext context) var interactivePayloadTyped = ToInteractiveType(payload, body); context.Items.Add(HttpItemKeys.InteractivePayloadKey, interactivePayloadTyped); } - + context.Request.Body.Position = 0; } @@ -87,7 +87,7 @@ private static SlackEvent ToEventType(JObject eventJson, string raw) return unknownSlackEvent; } } - + private static Interaction ToInteractiveType(JObject payloadJson, string raw) { var eventType = GetEventType(payloadJson); @@ -95,7 +95,7 @@ private static Interaction ToInteractiveType(JObject payloadJson, string raw) { case InteractionTypes.ViewSubmission: var viewSubmission = payloadJson.ToObject(); - + var view = payloadJson["view"] as JObject; var viewState = view["state"] as JObject;; viewSubmission.ViewId = view.Value("id"); @@ -109,14 +109,14 @@ private static Interaction ToInteractiveType(JObject payloadJson, string raw) return unknownSlackEvent; } } - + public static string GetEventType(JObject eventJson) { if (eventJson != null) { return eventJson["type"].Value(); } - + return "unknown"; } } diff --git a/source/src/Slackbot.Net.Endpoints/Middlewares/SlackbotEventAuthMiddleware.cs b/source/src/Slackbot.Net.Endpoints/Middlewares/SlackbotEventAuthMiddleware.cs new file mode 100644 index 0000000..170f706 --- /dev/null +++ b/source/src/Slackbot.Net.Endpoints/Middlewares/SlackbotEventAuthMiddleware.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Slackbot.Net.Endpoints.Authentication; + +namespace Slackbot.Net.Endpoints.Middlewares +{ + internal class SlackbotEventAuthMiddleware + { + private readonly RequestDelegate _next; + + public SlackbotEventAuthMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext ctx, ILogger logger) + { + bool success = false; + try + { + var res = await ctx.AuthenticateAsync(SlackbotEventsAuthenticationConstants.AuthenticationScheme); + success = res.Succeeded; + } + catch (InvalidOperationException ioe) + { + throw new InvalidOperationException("Did you forget to call AddAuthentication().AddSlackbotEvents()", ioe); + } + + if (success) + { + await _next(ctx); + } + else + { + ctx.Response.StatusCode = StatusCodes.Status401Unauthorized; + await ctx.Response.WriteAsync("UNAUTHORIZED"); + } + } + } +} diff --git a/source/src/Slackbot.Net.Endpoints/Slackbot.Net.Endpoints.csproj b/source/src/Slackbot.Net.Endpoints/Slackbot.Net.Endpoints.csproj index c2d1c27..5d93435 100644 --- a/source/src/Slackbot.Net.Endpoints/Slackbot.Net.Endpoints.csproj +++ b/source/src/Slackbot.Net.Endpoints/Slackbot.Net.Endpoints.csproj @@ -1,7 +1,7 @@ - + - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 Slackbot.Net.Endpoints Slackbot.Net.Endpoints John Korsnes @@ -20,7 +20,7 @@ - + @@ -29,6 +29,6 @@ - + diff --git a/source/src/Slackbot.Net.SlackClients.Http/Slackbot.Net.SlackClients.Http.csproj b/source/src/Slackbot.Net.SlackClients.Http/Slackbot.Net.SlackClients.Http.csproj index 7cc411d..278a0be 100644 --- a/source/src/Slackbot.Net.SlackClients.Http/Slackbot.Net.SlackClients.Http.csproj +++ b/source/src/Slackbot.Net.SlackClients.Http/Slackbot.Net.SlackClients.Http.csproj @@ -1,7 +1,7 @@ - + - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 Slackbot.Net.SlackClients.Http Slackbot.Net.SlackClients.Http John Korsnes @@ -27,8 +27,13 @@ - - + + + + + + + From 2a4fc5eb5e4012b79abf4464e5086b5a6f8fb8c6 Mon Sep 17 00:00:00 2001 From: John Korsnes Date: Mon, 13 Sep 2021 15:52:18 +0200 Subject: [PATCH 3/5] Run CI on branch push +semver:major --- .github/workflows/CI.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d5b4ce0..b6822b3 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,7 +2,8 @@ name: CI on: push: - branches: [master] + branches: + - "*" pull_request: branches: [master] @@ -23,7 +24,7 @@ jobs: - name: Setup .NET 6 uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x + dotnet-version: 6.0.x - name: Restore dependencies run: dotnet restore source - name: Build From b4a940985f36d8da8685e4cf7b0d24c5058c6c60 Mon Sep 17 00:00:00 2001 From: John Korsnes Date: Mon, 13 Sep 2021 15:58:41 +0200 Subject: [PATCH 4/5] Update dotnet sdk in builds --- .github/workflows/CI.yml | 2 +- .github/workflows/PreRelease.yml | 3 ++- .github/workflows/Release.yml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b6822b3..d044763 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -24,7 +24,7 @@ jobs: - name: Setup .NET 6 uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x + dotnet-version: "6.0.100-rc.2.21458.9" - name: Restore dependencies run: dotnet restore source - name: Build diff --git a/.github/workflows/PreRelease.yml b/.github/workflows/PreRelease.yml index fc7c207..78cc01e 100644 --- a/.github/workflows/PreRelease.yml +++ b/.github/workflows/PreRelease.yml @@ -27,7 +27,8 @@ jobs: - name: Setup .NET 6 uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x + dotnet-version: "6.0.100-rc.2.21458.9" + include-prerelease: true - name: Determine Version id: gitversion uses: gittools/actions/gitversion/execute@v0.9.7 diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 9a603d0..a1803f2 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -27,7 +27,7 @@ jobs: - name: Setup .NET 6 uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x + dotnet-version: "6.0.100-rc.2.21458.9" - name: Determine Version id: gitversion uses: gittools/actions/gitversion/execute@v0.9.7 From 7ba5315cef67000ecfc67e805b8ddcd889bdf22a Mon Sep 17 00:00:00 2001 From: John Korsnes Date: Mon, 13 Sep 2021 20:51:35 +0200 Subject: [PATCH 5/5] Update Sample --- .../EventHandlers/AppHomeOpenedHandler.cs | 15 ----- .../AppHomeViewSubmissionHandler.cs | 12 ---- .../EventHandlers/HelloWorldHandler.cs | 17 ----- .../EventHandlers/HiddenTestHandler.cs | 16 ----- .../EventHandlers/ListPublicCommands.cs | 34 ---------- .../MemberJoinedChannelHandler.cs | 14 ----- .../EventHandlers/PublicJokeHandler.cs | 18 ------ Samples/HelloWorld/MyTokenStore.cs | 41 ------------ Samples/HelloWorld/Program.cs | 47 ++++++++++---- Samples/HelloWorld/test.http | 6 +- readme.md | 62 ++++++++++++++++--- .../Hosting/IAppBuilderExtensions.cs | 29 +++------ .../Middlewares/HttpItemsManager.cs | 1 - .../SlackbotEventAuthMiddleware.cs | 2 +- 14 files changed, 101 insertions(+), 213 deletions(-) delete mode 100644 Samples/HelloWorld/EventHandlers/AppHomeOpenedHandler.cs delete mode 100644 Samples/HelloWorld/EventHandlers/AppHomeViewSubmissionHandler.cs delete mode 100644 Samples/HelloWorld/EventHandlers/HelloWorldHandler.cs delete mode 100644 Samples/HelloWorld/EventHandlers/HiddenTestHandler.cs delete mode 100644 Samples/HelloWorld/EventHandlers/ListPublicCommands.cs delete mode 100644 Samples/HelloWorld/EventHandlers/MemberJoinedChannelHandler.cs delete mode 100644 Samples/HelloWorld/EventHandlers/PublicJokeHandler.cs delete mode 100644 Samples/HelloWorld/MyTokenStore.cs diff --git a/Samples/HelloWorld/EventHandlers/AppHomeOpenedHandler.cs b/Samples/HelloWorld/EventHandlers/AppHomeOpenedHandler.cs deleted file mode 100644 index 2bf5bbf..0000000 --- a/Samples/HelloWorld/EventHandlers/AppHomeOpenedHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Newtonsoft.Json; -using Slackbot.Net.Endpoints.Abstractions; -using Slackbot.Net.Endpoints.Models.Events; - -namespace HelloWorld.EventHandlers; - -public class AppHomeOpenedHandler : IHandleAppHomeOpened -{ - public Task Handle(EventMetaData eventMetadata, AppHomeOpenedEvent payload) - { - string json = JsonConvert.SerializeObject(payload); - Console.WriteLine(json); - return Task.FromResult(new EventHandledResponse(json)); - } -} diff --git a/Samples/HelloWorld/EventHandlers/AppHomeViewSubmissionHandler.cs b/Samples/HelloWorld/EventHandlers/AppHomeViewSubmissionHandler.cs deleted file mode 100644 index 9261f5c..0000000 --- a/Samples/HelloWorld/EventHandlers/AppHomeViewSubmissionHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Slackbot.Net.Endpoints.Abstractions; -using Slackbot.Net.Endpoints.Models.Interactive.ViewSubmissions; - -namespace HelloWorld.EventHandlers; - -public class AppHomeViewSubmissionHandler : IHandleViewSubmissions -{ - public Task Handle(ViewSubmission payload) - { - return Task.FromResult(new EventHandledResponse("YoLO!")); - } -} diff --git a/Samples/HelloWorld/EventHandlers/HelloWorldHandler.cs b/Samples/HelloWorld/EventHandlers/HelloWorldHandler.cs deleted file mode 100644 index d10c165..0000000 --- a/Samples/HelloWorld/EventHandlers/HelloWorldHandler.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Slackbot.Net.Endpoints.Abstractions; -using Slackbot.Net.Endpoints.Models.Events; - -namespace HelloWorld.EventHandlers; - -internal class HelloWorldHandler : IHandleAppMentions -{ - public Task Handle(EventMetaData eventMetadata, AppMentionEvent message) - { - Console.WriteLine($"Hello world, {message.User}\n"); - return Task.FromResult(new EventHandledResponse("Responded")); - } - - public bool ShouldHandle(AppMentionEvent appMention) => appMention.Text.Contains("hw"); - - public (string HandlerTrigger, string Description) GetHelpDescription() => ("hw", "Returns a hello world message"); -} diff --git a/Samples/HelloWorld/EventHandlers/HiddenTestHandler.cs b/Samples/HelloWorld/EventHandlers/HiddenTestHandler.cs deleted file mode 100644 index 585cb65..0000000 --- a/Samples/HelloWorld/EventHandlers/HiddenTestHandler.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; -using Slackbot.Net.Endpoints.Abstractions; -using Slackbot.Net.Endpoints.Models.Events; - -namespace HelloWorld.EventHandlers; - -public class HiddenTestHandler : IHandleAppMentions -{ - public Task Handle(EventMetaData eventMetadata, AppMentionEvent slackEvent) - { - Console.WriteLine("Doing stuff from OtherAppMentionHandler: " + JsonConvert.SerializeObject(slackEvent)); - return Task.FromResult(new EventHandledResponse("Wrote stuff to log")); - } - - public bool ShouldHandle(AppMentionEvent appMentionEvent) => appMentionEvent.Text == "test"; -} diff --git a/Samples/HelloWorld/EventHandlers/ListPublicCommands.cs b/Samples/HelloWorld/EventHandlers/ListPublicCommands.cs deleted file mode 100644 index c80b70b..0000000 --- a/Samples/HelloWorld/EventHandlers/ListPublicCommands.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Slackbot.Net.Endpoints.Abstractions; -using Slackbot.Net.Endpoints.Models.Events; - -namespace HelloWorld.EventHandlers; - -public class ListPublicCommands : IShortcutAppMentions -{ - private readonly IEnumerable _handlers; - - - public ListPublicCommands(IEnumerable allHandlers) - { - _handlers = allHandlers; - } - - public Task Handle(EventMetaData eventMetadata, AppMentionEvent @event) - { - var publicHandlersToBeListedBack = _handlers.Where(handler => handler.GetHelpDescription().HandlerTrigger != string.Empty) - - .Aggregate("*Public trigger mentions*", (current, kvPair) => current + $"\n• `{kvPair.GetType()}. `{kvPair.GetHelpDescription().HandlerTrigger}` : _{kvPair.GetHelpDescription().Description}_"); - - - Console.WriteLine(publicHandlersToBeListedBack); - - - var ssh = _handlers.Where(handler => handler.GetHelpDescription().HandlerTrigger == string.Empty) - .Aggregate("*Private trigger handlers*", (current, kvPair) => current + $"\n• `{kvPair.GetType()}`"); - Console.WriteLine(ssh); - - return Task.CompletedTask; - } - - public bool ShouldShortcut(AppMentionEvent appMention) => appMention.Text.Contains("help"); -} diff --git a/Samples/HelloWorld/EventHandlers/MemberJoinedChannelHandler.cs b/Samples/HelloWorld/EventHandlers/MemberJoinedChannelHandler.cs deleted file mode 100644 index e3833db..0000000 --- a/Samples/HelloWorld/EventHandlers/MemberJoinedChannelHandler.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Newtonsoft.Json; -using Slackbot.Net.Endpoints.Abstractions; -using Slackbot.Net.Endpoints.Models.Events; - -namespace HelloWorld.EventHandlers; - -public class MemberJoinedChannelHandler : IHandleMemberJoinedChannel -{ - public Task Handle(EventMetaData eventMetadata, MemberJoinedChannelEvent slackEvent) - { - Console.WriteLine("Doing stuff from MemberJoinedChannelHandler: " + JsonConvert.SerializeObject(slackEvent)); - return Task.FromResult(new EventHandledResponse("Wrote stuff to log")); - } -} diff --git a/Samples/HelloWorld/EventHandlers/PublicJokeHandler.cs b/Samples/HelloWorld/EventHandlers/PublicJokeHandler.cs deleted file mode 100644 index a9ddc0a..0000000 --- a/Samples/HelloWorld/EventHandlers/PublicJokeHandler.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Newtonsoft.Json; -using Slackbot.Net.Endpoints.Abstractions; -using Slackbot.Net.Endpoints.Models.Events; - -namespace HelloWorld.EventHandlers; - -public class PublicJokeHandler : IHandleAppMentions -{ - public Task Handle(EventMetaData eventMetadata, AppMentionEvent slackEvent) - { - Console.WriteLine("Doing stuff from AppmentionHandler: " + JsonConvert.SerializeObject(slackEvent)); - return Task.FromResult(new EventHandledResponse("Wrote stuff to log")); - } - - public bool ShouldHandle(AppMentionEvent slackEvent) => slackEvent.Text == "test"; - - public (string HandlerTrigger, string Description) GetHelpDescription() => ("telljoke", "bot tells a joke"); -} diff --git a/Samples/HelloWorld/MyTokenStore.cs b/Samples/HelloWorld/MyTokenStore.cs deleted file mode 100644 index 35d428f..0000000 --- a/Samples/HelloWorld/MyTokenStore.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Slackbot.Net.Abstractions.Hosting; - -public class MyTokenStore : ITokenStore -{ - private readonly List _workspaces; - public string SlackToken = Environment.GetEnvironmentVariable("Slackbot_SlackApiKey_BotUser"); - - public MyTokenStore() - { - _workspaces = new List() - { - new Workspace { - Token = SlackToken, - TeamId = "T0EC3DG3A" - } - }; - } - - public Task> GetTokens() - { - return Task.FromResult(_workspaces.Select(c => c.Token)); - } - - public Task GetTokenByTeamId(string teamId) - { - return Task.FromResult(_workspaces.First(t => t.TeamId == teamId).Token); - } - - public Task Delete(string token) - { - var workspaceToRemove = _workspaces.First(w => w.Token == token); - _workspaces.Remove(workspaceToRemove); - return Task.CompletedTask; - } - - private class Workspace - { - public string Token { get; set; } - public string TeamId { get; set; } - } -} diff --git a/Samples/HelloWorld/Program.cs b/Samples/HelloWorld/Program.cs index b8f9828..d7340c7 100644 --- a/Samples/HelloWorld/Program.cs +++ b/Samples/HelloWorld/Program.cs @@ -1,20 +1,41 @@ -using HelloWorld.EventHandlers; using Slackbot.Net.Endpoints.Hosting; -using Slackbot.Net.SlackClients.Http.Extensions; using Slackbot.Net.Endpoints.Authentication; +using Slackbot.Net.Endpoints.Abstractions; +using Slackbot.Net.Endpoints.Models.Events; +using Slackbot.Net.Abstractions.Hosting; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddAuthentication().AddSlackbotEvents(c => c.SigningSecret = Environment.GetEnvironmentVariable("CLIENT_SIGNING_SECRET")); -builder.Services.AddSlackClientBuilder() - .AddSlackBotEvents() - .AddAppMentionHandler() - .AddAppMentionHandler() - .AddAppMentionHandler() - .AddMemberJoinedChannelHandler() - .AddShortcut() - .AddViewSubmissionHandler() - .AddAppHomeOpenedHandler(); + +// Needed in production to verify that incoming event payloads are from Slack +builder.Services.AddAuthentication() + .AddSlackbotEvents(c => c.SigningSecret = Environment.GetEnvironmentVariable("CLIENT_SIGNING_SECRET")); + +// Setup event handlers +builder.Services.AddSlackBotEvents() + .AddAppMentionHandler(); + var app = builder.Build(); -app.UseSlackbot(enableAuth: !app.Environment.IsDevelopment()); +app.UseSlackbot(enableAuth:!app.Environment.IsDevelopment()); // disable during development for easier testing app.Run(); + +class DoStuff : IHandleAppMentions +{ + public bool ShouldHandle(AppMentionEvent slackEvent) => slackEvent.Text.Contains("dostuff"); + + public Task Handle(EventMetaData eventMetadata, AppMentionEvent slackEvent) + { + // handler code executed given ShouldHandle returns true + Console.WriteLine("Doing stuff!"); + return Task.FromResult(new EventHandledResponse("yolo")); + } +} + +class TokenStore : ITokenStore +{ + public string SlackToken = Environment.GetEnvironmentVariable("SLACK_TOKEN"); + + public Task> GetTokens() => Task.FromResult(new [] { SlackToken }.AsEnumerable()); + public Task GetTokenByTeamId(string teamId) => Task.FromResult(SlackToken); + public Task Delete(string token) => throw new NotImplementedException("Single workspace app"); +} diff --git a/Samples/HelloWorld/test.http b/Samples/HelloWorld/test.http index e4000f6..54c5478 100644 --- a/Samples/HelloWorld/test.http +++ b/Samples/HelloWorld/test.http @@ -1,5 +1,5 @@ -# Mimicks a payload slack would send for app_mention events, like "@yourbot hw" in this case: -POST http://localhost:1337/events +# Mimicks a payload slack would send for app_mention events, like "@yourbot dostuff" in this case: +GET http://localhost:5000/ X-Slack-Request-Timestamp: 12331231 X-Slack-Signature: v0:abc123etcetc @@ -8,7 +8,7 @@ X-Slack-Signature: v0:abc123etcetc "event": { "type": "app_mention", "user": "USRAR1YTV", - "text" : "<@BOT123> hw", + "text" : "<@BOT123> dostuff", "channel": "C92QZTVEF" } } diff --git a/readme.md b/readme.md index c4e47ea..02f687d 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ # Slackbot.NET -[![Build](https://github.com/slackbot-net/slackbot.net/workflows/CI/badge.svg)](https://github.com/slackbot.net/slackbot.net/actions) +[![Build](https://github.com/slackbot-net/slackbot.net/workflows/CI/badge.svg)](https://github.com/slackbot.net/slackbot.net/actions) ## What? @@ -16,24 +16,68 @@ Download it from NuGet:[![NuGet](https://img.shields.io/nuget/dt/slackbot.net.en - `challenge` - `app_mention` - `view_submission` -- `member_joined_channel` +- `member_joined_channel` ### Configuration + +A complete ASP.NET Core 6 Slackbot: ```csharp +var builder = WebApplication.CreateBuilder(args); -services.AddSlackBotEvents() - .AddAppMentionHandler(); -``` +// Needed to verify that incoming event payloads are from Slack +builder.Services.AddAuthentication() + .AddSlackbotEvents(c => c.SigningSecret = Environment.GetEnvironmentVariable("secret")); -and +// Setup event handlers +builder.Services.AddSlackBotEvents() + .AddAppMentionHandler() -```csharp - app.UseSlackbot("/events"); + +var app = builder.Build(); +app.UseSlackbot(); //or app.UseSlackbot(enableAuth:false) during development +app.Run(); + +class DoStuff : IHandleAppMentions +{ + public bool ShouldHandle(AppMentionEvent slackEvent) => slackEvent.Text.Contains("CLIENT_SIGNING_SECRET"); + + public Task Handle(EventMetaData eventMetadata, AppMentionEvent slackEvent) + { + Console.WriteLine("Doing stuff!"); + return Task.FromResult(new EventHandledResponse("yolo")); + } +} + +class MyTokenStore : ITokenStore +{ + public string SlackToken = Environment.GetEnvironmentVariable("SLACK_TOKEN"); + + public Task> GetTokens() => Task.FromResult(new[] { SlackToken }); + public Task GetTokenByTeamId(string teamId) => Task.FromResult(SlackToken); + public Task Delete(string token) => throw new NotImplementedException("Single workspace app"); +} ``` +A slack app can be distributed either as a single-workspace application (1 single Slack token), or as a _distributed_ Slack application where other workspaces can install it them self either via a web page, or via the Slack App Store. + ### Single workspace Slack app + + Implement the `ITokenStore`, and return a single workspace token from relevant methods (get/get-all) + + + ### Advanced: Distributed Slack app + + Implement the `ITokenStore` and store/retrieve tokens to any storage. The install and uninstall flows are currently not part of this library, so you would have to implement the relevant redirects triggers and callbacks endpoints yourself. Those callback endpoints needs to save the tokens to the storage used in your `ITokenStore` implementation. + + A typical callback endpoint would consist of: + 1) Receiving the OAuth2 code as query string as part of (the final redirect of the OAuth2 authorization code flow) + 2) On receive, a server-to-server API request to exchange the code for an Slack token for the installing workspace. Via your apps ClientId, ClientSecret and the OAuth2 code, use the Slack OAuthAccessV2 HTTP API here. + 3) On a successful exchange, save the WorkspaceId and the token to your storage. + + Providing an OOB solution for distributed apps is on the roadmap. + ### Samples - Check the [samples](/Samples/). \ No newline at end of file + Check the [samples](/Samples/). diff --git a/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs b/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs index b366da3..52a2f3e 100644 --- a/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs +++ b/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs @@ -1,34 +1,25 @@ using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Slackbot.Net.Endpoints.Middlewares; namespace Slackbot.Net.Endpoints.Hosting { public static class IAppBuilderExtensions { - public static IApplicationBuilder UseSlackbot(this IApplicationBuilder app, string path = "/events", bool enableAuth = true) + public static IApplicationBuilder UseSlackbot(this IApplicationBuilder app, bool enableAuth = true) { - app.MapWhen(c => IsSlackRequest(c, path), a => - { - if(enableAuth) - a.UseMiddleware(); + if (enableAuth) + app.UseMiddleware(); - a.UseMiddleware(); - a.MapWhen(Challenge.ShouldRun, b => b.UseMiddleware()); - a.MapWhen(Uninstall.ShouldRun, b => b.UseMiddleware()); - a.MapWhen(AppMentionEvents.ShouldRun, b => b.UseMiddleware()); - a.MapWhen(MemberJoinedEvents.ShouldRun, b => b.UseMiddleware()); - a.MapWhen(AppHomeOpenedEvents.ShouldRun, b => b.UseMiddleware()); - a.MapWhen(InteractiveEvents.ShouldRun, b => b.UseMiddleware()); - }); + app.UseMiddleware(); + app.MapWhen(Challenge.ShouldRun, b => b.UseMiddleware()); + app.MapWhen(Uninstall.ShouldRun, b => b.UseMiddleware()); + app.MapWhen(AppMentionEvents.ShouldRun, b => b.UseMiddleware()); + app.MapWhen(MemberJoinedEvents.ShouldRun, b => b.UseMiddleware()); + app.MapWhen(AppHomeOpenedEvents.ShouldRun, b => b.UseMiddleware()); + app.MapWhen(InteractiveEvents.ShouldRun, b => b.UseMiddleware()); return app; } - - private static bool IsSlackRequest(HttpContext ctx, string path) - { - return ctx.Request.Path == path && ctx.Request.Method == "POST"; - } } } diff --git a/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs b/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs index ae52177..89f0a06 100644 --- a/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs +++ b/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Security.Policy; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; diff --git a/source/src/Slackbot.Net.Endpoints/Middlewares/SlackbotEventAuthMiddleware.cs b/source/src/Slackbot.Net.Endpoints/Middlewares/SlackbotEventAuthMiddleware.cs index 170f706..bb539df 100644 --- a/source/src/Slackbot.Net.Endpoints/Middlewares/SlackbotEventAuthMiddleware.cs +++ b/source/src/Slackbot.Net.Endpoints/Middlewares/SlackbotEventAuthMiddleware.cs @@ -26,7 +26,7 @@ public async Task Invoke(HttpContext ctx, ILogger l } catch (InvalidOperationException ioe) { - throw new InvalidOperationException("Did you forget to call AddAuthentication().AddSlackbotEvents()", ioe); + throw new InvalidOperationException("Did you forget to call services.AddAuthentication().AddSlackbotEvents()?", ioe); } if (success)