From 44ac9ed0bb81b48ee6e869f2c0a7cca68ca144ff Mon Sep 17 00:00:00 2001 From: soxtoby Date: Fri, 29 Dec 2017 12:41:47 +1000 Subject: [PATCH] Adding support for Events & related APIs --- SlackNet.AspNetCore/AspNetCoreExtensions.cs | 29 +++++ SlackNet.AspNetCore/HttpContextExtensions.cs | 19 +++ SlackNet.AspNetCore/ResolvedActionHandler.cs | 33 +++++ SlackNet.AspNetCore/ResolvedEventHandler.cs | 28 ++++ SlackNet.AspNetCore/ResolvedOptionProvider.cs | 33 +++++ SlackNet.AspNetCore/SlackActionsService.cs | 19 +++ .../SlackEndpointConfiguration.cs | 32 +++++ SlackNet.AspNetCore/SlackEventsMiddleware.cs | 123 ++++++++++++++++++ SlackNet.AspNetCore/SlackEventsService.cs | 25 ++++ .../SlackNet.AspNetCore.csproj | 27 ++++ SlackNet.AspNetCore/SlackOptionsService.cs | 19 +++ .../SlackServiceConfiguration.cs | 48 +++++++ SlackNet.Bot/SlackMessage.cs | 3 + SlackNet.EventsExample/ColorSelector.cs | 49 +++++++ SlackNet.EventsExample/Counter.cs | 53 ++++++++ SlackNet.EventsExample/MessageHandler.cs | 39 ++++++ SlackNet.EventsExample/Program.cs | 18 +++ .../SlackNet.EventsExample.csproj | 20 +++ SlackNet.EventsExample/Startup.cs | 39 ++++++ .../appsettings.Development.json | 10 ++ SlackNet.EventsExample/appsettings.json | 19 +++ SlackNet.Tests/SerializationTests.cs | 2 +- SlackNet.Tests/SlackRtmClientTests.cs | 6 +- SlackNet.Tests/SlackUrlBuilderTests.cs | 2 +- SlackNet.sln | 17 ++- SlackNet.sln.DotSettings | 1 + SlackNet/Actions/AttachmentUpdateResponse.cs | 39 ++++++ SlackNet/Actions/IActionHandler.cs | 9 ++ SlackNet/Actions/MessageResponse.cs | 12 ++ SlackNet/Actions/MessageUpdateResponse.cs | 29 +++++ SlackNet/Actions/OptionsRequest.cs | 16 +++ SlackNet/Actions/SlackActions.cs | 23 ++++ SlackNet/Actions/SlackOptions.cs | 34 +++++ SlackNet/Default.cs | 32 ++++- SlackNet/Events/AppUninstalled.cs | 7 + SlackNet/Events/EventCallback.cs | 22 ++++ SlackNet/Events/GridMigrationFinished.cs | 10 ++ SlackNet/Events/GridMigrationStarted.cs | 10 ++ SlackNet/Events/LinkShared.cs | 21 +++ SlackNet/Events/Messages/MessageDeleted.cs | 2 + SlackNet/Events/Messages/MessageEvent.cs | 5 + SlackNet/Events/PresenceChange.cs | 2 + SlackNet/Events/TokensRevoked.cs | 15 +++ SlackNet/Events/UrlVerification.cs | 11 ++ SlackNet/Http.cs | 22 ++-- SlackNet/IEventHandler.cs | 12 ++ SlackNet/Objects/Action.cs | 18 +-- SlackNet/Objects/ActionType.cs | 8 ++ SlackNet/Objects/Attachment.cs | 34 ++++- SlackNet/Objects/DndStatus.cs | 3 + SlackNet/Objects/Hub.cs | 2 + SlackNet/Objects/InteractiveMessage.cs | 33 +++++ SlackNet/Objects/PinnedItem.cs | 2 + SlackNet/Objects/Reminder.cs | 3 + SlackNet/Objects/User.cs | 1 + SlackNet/Objects/UserGroup.cs | 4 + SlackNet/SlackApiClient.cs | 14 +- SlackNet/SlackEvents.cs | 48 +++++++ SlackNet/SlackJsonSettings.cs | 11 ++ SlackNet/SlackNet.csproj.DotSettings | 1 + SlackNet/SlackRtmClient.cs | 16 +-- SlackNet/SlackUrlBuilder.cs | 6 +- SlackNet/SyncedSubject.cs | 18 +++ SlackNet/WebApi/Message.cs | 70 +++++++++- SlackNet/WebApi/ResponseType.cs | 8 ++ SlackNet/WebApi/Responses/DndResponse.cs | 3 + .../WebApi/Responses/IntegrationLogRecord.cs | 2 + SlackNet/WebApi/Responses/OwnDndResponse.cs | 2 + SlackNet/WebApi/Responses/SnoozeResponse.cs | 2 + SlackNet/WebApi/Responses/UserListResponse.cs | 2 + 70 files changed, 1304 insertions(+), 53 deletions(-) create mode 100644 SlackNet.AspNetCore/AspNetCoreExtensions.cs create mode 100644 SlackNet.AspNetCore/HttpContextExtensions.cs create mode 100644 SlackNet.AspNetCore/ResolvedActionHandler.cs create mode 100644 SlackNet.AspNetCore/ResolvedEventHandler.cs create mode 100644 SlackNet.AspNetCore/ResolvedOptionProvider.cs create mode 100644 SlackNet.AspNetCore/SlackActionsService.cs create mode 100644 SlackNet.AspNetCore/SlackEndpointConfiguration.cs create mode 100644 SlackNet.AspNetCore/SlackEventsMiddleware.cs create mode 100644 SlackNet.AspNetCore/SlackEventsService.cs create mode 100644 SlackNet.AspNetCore/SlackNet.AspNetCore.csproj create mode 100644 SlackNet.AspNetCore/SlackOptionsService.cs create mode 100644 SlackNet.AspNetCore/SlackServiceConfiguration.cs create mode 100644 SlackNet.EventsExample/ColorSelector.cs create mode 100644 SlackNet.EventsExample/Counter.cs create mode 100644 SlackNet.EventsExample/MessageHandler.cs create mode 100644 SlackNet.EventsExample/Program.cs create mode 100644 SlackNet.EventsExample/SlackNet.EventsExample.csproj create mode 100644 SlackNet.EventsExample/Startup.cs create mode 100644 SlackNet.EventsExample/appsettings.Development.json create mode 100644 SlackNet.EventsExample/appsettings.json create mode 100644 SlackNet/Actions/AttachmentUpdateResponse.cs create mode 100644 SlackNet/Actions/IActionHandler.cs create mode 100644 SlackNet/Actions/MessageResponse.cs create mode 100644 SlackNet/Actions/MessageUpdateResponse.cs create mode 100644 SlackNet/Actions/OptionsRequest.cs create mode 100644 SlackNet/Actions/SlackActions.cs create mode 100644 SlackNet/Actions/SlackOptions.cs create mode 100644 SlackNet/Events/AppUninstalled.cs create mode 100644 SlackNet/Events/EventCallback.cs create mode 100644 SlackNet/Events/GridMigrationFinished.cs create mode 100644 SlackNet/Events/GridMigrationStarted.cs create mode 100644 SlackNet/Events/LinkShared.cs create mode 100644 SlackNet/Events/TokensRevoked.cs create mode 100644 SlackNet/Events/UrlVerification.cs create mode 100644 SlackNet/IEventHandler.cs create mode 100644 SlackNet/Objects/ActionType.cs create mode 100644 SlackNet/Objects/InteractiveMessage.cs create mode 100644 SlackNet/SlackEvents.cs create mode 100644 SlackNet/SlackJsonSettings.cs create mode 100644 SlackNet/SyncedSubject.cs create mode 100644 SlackNet/WebApi/ResponseType.cs diff --git a/SlackNet.AspNetCore/AspNetCoreExtensions.cs b/SlackNet.AspNetCore/AspNetCoreExtensions.cs new file mode 100644 index 0000000..cb6c5c8 --- /dev/null +++ b/SlackNet.AspNetCore/AspNetCoreExtensions.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace SlackNet.AspNetCore +{ + public static class AspNetCoreExtensions + { + public static IServiceCollection AddSlackNet(this IServiceCollection serviceCollection, Action configure) + { + var configuration = new SlackServiceConfiguration(serviceCollection); + configure(configuration); + Default.RegisterServices((serviceType, createService) => serviceCollection.AddTransient(serviceType, c => createService(c.GetService))); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddTransient(c => new SlackApiClient(c.GetService(), c.GetService(), c.GetService(), configuration.ApiToken)); + + return serviceCollection; + } + + public static IApplicationBuilder UseSlackNet(this IApplicationBuilder app, Action configure) + { + var config = new SlackEndpointConfiguration(); + configure(config); + return app.UseMiddleware(config); + } + } +} diff --git a/SlackNet.AspNetCore/HttpContextExtensions.cs b/SlackNet.AspNetCore/HttpContextExtensions.cs new file mode 100644 index 0000000..13448af --- /dev/null +++ b/SlackNet.AspNetCore/HttpContextExtensions.cs @@ -0,0 +1,19 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace SlackNet.AspNetCore +{ + static class HttpContextExtensions + { + public static async Task Respond(this HttpContext context, HttpStatusCode status, string contentType = null, string body = null) + { + context.Response.StatusCode = (int)status; + if (contentType != null) + context.Response.ContentType = contentType; + if (body != null) + await context.Response.WriteAsync(body).ConfigureAwait(false); + return context.Response; + } + } +} \ No newline at end of file diff --git a/SlackNet.AspNetCore/ResolvedActionHandler.cs b/SlackNet.AspNetCore/ResolvedActionHandler.cs new file mode 100644 index 0000000..d5f9fe8 --- /dev/null +++ b/SlackNet.AspNetCore/ResolvedActionHandler.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace SlackNet.AspNetCore +{ + abstract class ResolvedActionHandler : IActionHandler + { + protected ResolvedActionHandler(string actionName) => ActionName = actionName; + + public string ActionName { get; } + + public abstract Task Handle(InteractiveMessage message); + } + + class ResolvedActionHandler : ResolvedActionHandler + where T : IActionHandler + { + private readonly IServiceProvider _serviceProvider; + + public ResolvedActionHandler(IServiceProvider serviceProvider, string actionName) + : base(actionName) + { + _serviceProvider = serviceProvider; + } + + public override Task Handle(InteractiveMessage message) + { + var handler = _serviceProvider.GetRequiredService(); + return handler.Handle(message); + } + } +} \ No newline at end of file diff --git a/SlackNet.AspNetCore/ResolvedEventHandler.cs b/SlackNet.AspNetCore/ResolvedEventHandler.cs new file mode 100644 index 0000000..ef5b585 --- /dev/null +++ b/SlackNet.AspNetCore/ResolvedEventHandler.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SlackNet.Events; + +namespace SlackNet.AspNetCore +{ + class ResolvedEventHandler : IEventHandler + where TEvent : Event + where THandler : IEventHandler + { + private readonly IServiceProvider _serviceProvider; + + public ResolvedEventHandler(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task Handle(TEvent slackEvent) + { + using (var scope = _serviceProvider.CreateScope()) + { + var handler = scope.ServiceProvider.GetRequiredService(); + await handler.Handle(slackEvent).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/SlackNet.AspNetCore/ResolvedOptionProvider.cs b/SlackNet.AspNetCore/ResolvedOptionProvider.cs new file mode 100644 index 0000000..59558af --- /dev/null +++ b/SlackNet.AspNetCore/ResolvedOptionProvider.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace SlackNet.AspNetCore +{ + abstract class ResolvedOptionProvider : IOptionProvider + { + protected ResolvedOptionProvider(string actionName) => ActionName = actionName; + + public string ActionName { get; } + + public abstract Task GetOptions(OptionsRequest request); + } + + class ResolvedOptionProvider : ResolvedOptionProvider + where T : IOptionProvider + { + private readonly IServiceProvider _serviceProvider; + + public ResolvedOptionProvider(IServiceProvider serviceProvider, string actionName) + : base(actionName) + { + _serviceProvider = serviceProvider; + } + + public override Task GetOptions(OptionsRequest request) + { + var handler = _serviceProvider.GetRequiredService(); + return handler.GetOptions(request); + } + } +} \ No newline at end of file diff --git a/SlackNet.AspNetCore/SlackActionsService.cs b/SlackNet.AspNetCore/SlackActionsService.cs new file mode 100644 index 0000000..b79535d --- /dev/null +++ b/SlackNet.AspNetCore/SlackActionsService.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SlackNet.AspNetCore +{ + class SlackActionsService : ISlackActions + { + private readonly ISlackActions _actions = new SlackActions(); + + public SlackActionsService(IEnumerable handlers) + { + foreach (var handler in handlers) + _actions.SetHandler(handler.ActionName, handler); + } + + public Task Handle(InteractiveMessage request) => _actions.Handle(request); + public void SetHandler(string actionName, IActionHandler handler) => _actions.SetHandler(actionName, handler); + } +} \ No newline at end of file diff --git a/SlackNet.AspNetCore/SlackEndpointConfiguration.cs b/SlackNet.AspNetCore/SlackEndpointConfiguration.cs new file mode 100644 index 0000000..22a3c6e --- /dev/null +++ b/SlackNet.AspNetCore/SlackEndpointConfiguration.cs @@ -0,0 +1,32 @@ +namespace SlackNet.AspNetCore +{ + public class SlackEndpointConfiguration + { + public SlackEndpointConfiguration MapToPrefix(string routePrefix) + { + RoutePrefix = routePrefix; + return this; + } + + public SlackEndpointConfiguration VerifyWith(string verificationToken) + { + VerificationToken = verificationToken; + return this; + } + + /// + /// Path to receive Slack requests on. Defaults to "slack". + /// Configures the following routes: + ///
/{RoutePrefix}/event - Event subscriptions + ///
/{RoutePrefix}/action - Interactive components requests + ///
/{RoutePrefix}/options - Options loading (for message menus) + ///
+ public string RoutePrefix { get; private set; } = "slack"; + + /// + /// Use this token to verify that requests are actually coming from Slack. + /// You'll find this value in the "App Credentials" section of your app's application management interface. + /// + public string VerificationToken { get; private set; } + } +} \ No newline at end of file diff --git a/SlackNet.AspNetCore/SlackEventsMiddleware.cs b/SlackNet.AspNetCore/SlackEventsMiddleware.cs new file mode 100644 index 0000000..247770b --- /dev/null +++ b/SlackNet.AspNetCore/SlackEventsMiddleware.cs @@ -0,0 +1,123 @@ +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using SlackNet.Events; + +namespace SlackNet.AspNetCore +{ + class SlackEventsMiddleware + { + private readonly RequestDelegate _next; + private readonly SlackEndpointConfiguration _configuration; + private readonly ISlackEvents _slackEvents; + private readonly ISlackActions _slackActions; + private readonly ISlackOptions _slackOptions; + private readonly SlackJsonSettings _jsonSettings; + + public SlackEventsMiddleware( + RequestDelegate next, + SlackEndpointConfiguration configuration, + ISlackEvents slackEvents, + ISlackActions slackActions, + ISlackOptions slackOptions, + SlackJsonSettings jsonSettings) + { + _next = next; + _configuration = configuration; + _slackEvents = slackEvents; + _slackActions = slackActions; + _slackOptions = slackOptions; + _jsonSettings = jsonSettings; + } + + public async Task Invoke(HttpContext context) + { + if (context.Request.Path == $"/{_configuration.RoutePrefix}/event") + await HandleSlackEvent(context).ConfigureAwait(false); + else if (context.Request.Path == $"/{_configuration.RoutePrefix}/action") + await HandleSlackAction(context).ConfigureAwait(false); + else if (context.Request.Path == $"/{_configuration.RoutePrefix}/options") + await HandleSlackOptions(context).ConfigureAwait(false); + else + await _next(context).ConfigureAwait(false); + } + + private async Task HandleSlackEvent(HttpContext context) + { + if (context.Request.Method != "POST") + return await context.Respond(HttpStatusCode.MethodNotAllowed).ConfigureAwait(false); + + if (context.Request.ContentType != "application/json") + return await context.Respond(HttpStatusCode.UnsupportedMediaType).ConfigureAwait(false); + + var body = DeserializeRequestBody(context); + + if (body is UrlVerification urlVerification && IsValidToken(urlVerification.Token)) + return await context.Respond(HttpStatusCode.OK, "application/x-www-form-urlencoded", urlVerification.Challenge).ConfigureAwait(false); + + if (body is EventCallback eventCallback && IsValidToken(eventCallback.Token)) + { + _slackEvents.Handle(eventCallback); + return await context.Respond(HttpStatusCode.OK).ConfigureAwait(false); + } + + return await context.Respond(HttpStatusCode.BadRequest, body: "Invalid token or unrecognized content").ConfigureAwait(false); + } + + private async Task HandleSlackAction(HttpContext context) + { + if (context.Request.Method != "POST") + return await context.Respond(HttpStatusCode.MethodNotAllowed).ConfigureAwait(false); + + var interactiveMessage = await DeserializePayload(context).ConfigureAwait(false); + + if (interactiveMessage != null && IsValidToken(interactiveMessage.Token)) + { + var response = await _slackActions.Handle(interactiveMessage).ConfigureAwait(false); + + var responseJson = response == null ? null + : interactiveMessage.IsAppUnfurl ? Serialize(new AttachmentUpdateResponse(response)) + : Serialize(new MessageUpdateResponse(response)); + + return await context.Respond(HttpStatusCode.OK, "application/json", responseJson).ConfigureAwait(false); + } + + return await context.Respond(HttpStatusCode.BadRequest, body: "Invalid token or unrecognized content").ConfigureAwait(false); + } + + private async Task HandleSlackOptions(HttpContext context) + { + if (context.Request.Method != "POST") + return await context.Respond(HttpStatusCode.MethodNotAllowed).ConfigureAwait(false); + + var optionsRequest = await DeserializePayload(context).ConfigureAwait(false); + + if (optionsRequest != null && IsValidToken(optionsRequest.Token)) + { + var response = await _slackOptions.Handle(optionsRequest).ConfigureAwait(false); + return await context.Respond(HttpStatusCode.OK, "application/json", Serialize(response)).ConfigureAwait(false); + } + + return await context.Respond(HttpStatusCode.BadRequest, body: "Invalid token or unrecognized content").ConfigureAwait(false); + } + + private async Task DeserializePayload(HttpContext context) + { + var form = await context.Request.ReadFormAsync().ConfigureAwait(false); + + return form["payload"] + .Select(p => JsonConvert.DeserializeObject(p, _jsonSettings.SerializerSettings)) + .FirstOrDefault(); + } + + private bool IsValidToken(string token) => string.IsNullOrEmpty(_configuration.VerificationToken) || token == _configuration.VerificationToken; + + private string Serialize(object value) => JsonConvert.SerializeObject(value, _jsonSettings.SerializerSettings); + + private Event DeserializeRequestBody(HttpContext context) => + JsonSerializer.Create(_jsonSettings.SerializerSettings).Deserialize(new JsonTextReader(new StreamReader(context.Request.Body))); + } +} \ No newline at end of file diff --git a/SlackNet.AspNetCore/SlackEventsService.cs b/SlackNet.AspNetCore/SlackEventsService.cs new file mode 100644 index 0000000..6976d6e --- /dev/null +++ b/SlackNet.AspNetCore/SlackEventsService.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using SlackNet.Events; + +namespace SlackNet.AspNetCore +{ + class SlackEventsService : ISlackEvents + { + private readonly ISlackEvents _events = new SlackEvents(); + + public SlackEventsService(IEnumerable eventHandlers) + { + foreach (var handler in eventHandlers) + AddHandler((dynamic)handler); + } + + public void Handle(EventCallback eventCallback) => _events.Handle(eventCallback); + + public IObservable RawEvents => _events.RawEvents; + + public IObservable Events() where T : Event => _events.Events(); + + public void AddHandler(IEventHandler handler) where T : Event => _events.AddHandler(handler); + } +} \ No newline at end of file diff --git a/SlackNet.AspNetCore/SlackNet.AspNetCore.csproj b/SlackNet.AspNetCore/SlackNet.AspNetCore.csproj new file mode 100644 index 0000000..ffdd719 --- /dev/null +++ b/SlackNet.AspNetCore/SlackNet.AspNetCore.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0 + Simon Oxtoby + ASP.NET Core integration for receiving requests from Slack + + + + True + + + + False + bin\Release\netstandard1.4\SlackNet.xml + + + + + + + + + + + + diff --git a/SlackNet.AspNetCore/SlackOptionsService.cs b/SlackNet.AspNetCore/SlackOptionsService.cs new file mode 100644 index 0000000..55fa5c9 --- /dev/null +++ b/SlackNet.AspNetCore/SlackOptionsService.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SlackNet.AspNetCore +{ + class SlackOptionsService : ISlackOptions + { + private readonly ISlackOptions _options = new SlackOptions(); + + public SlackOptionsService(IEnumerable providers) + { + foreach (var provider in providers) + _options.SetProvider(provider.ActionName, provider); + } + + public Task Handle(OptionsRequest request) => _options.Handle(request); + public void SetProvider(string actionName, IOptionProvider handler) => _options.SetProvider(actionName, handler); + } +} \ No newline at end of file diff --git a/SlackNet.AspNetCore/SlackServiceConfiguration.cs b/SlackNet.AspNetCore/SlackServiceConfiguration.cs new file mode 100644 index 0000000..1c52c72 --- /dev/null +++ b/SlackNet.AspNetCore/SlackServiceConfiguration.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.DependencyInjection; +using SlackNet.Events; + +namespace SlackNet.AspNetCore +{ + public class SlackServiceConfiguration + { + private readonly IServiceCollection _serviceCollection; + + public SlackServiceConfiguration(IServiceCollection serviceCollection) + { + _serviceCollection = serviceCollection; + } + + public SlackServiceConfiguration UseApiToken(string token) + { + ApiToken = token; + return this; + } + + public SlackServiceConfiguration RegisterEventHandler() + where TEvent : Event + where THandler : class, IEventHandler + { + _serviceCollection.AddTransient(); + _serviceCollection.AddSingleton>(); + return this; + } + + public SlackServiceConfiguration RegisterActionHandler(string actionName) + where THandler : class, IActionHandler + { + _serviceCollection.AddTransient(); + _serviceCollection.AddSingleton(c => new ResolvedActionHandler(c, actionName)); + return this; + } + + public SlackServiceConfiguration RegisterOptionProvider(string actionName) + where TProvider : class, IOptionProvider + { + _serviceCollection.AddTransient(); + _serviceCollection.AddSingleton(c => new ResolvedOptionProvider(c, actionName)); + return this; + } + + public string ApiToken { get; private set; } + } +} \ No newline at end of file diff --git a/SlackNet.Bot/SlackMessage.cs b/SlackNet.Bot/SlackMessage.cs index 163dbdd..06f1f89 100644 --- a/SlackNet.Bot/SlackMessage.cs +++ b/SlackNet.Bot/SlackMessage.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Newtonsoft.Json; namespace SlackNet.Bot { @@ -17,8 +18,10 @@ public SlackMessage(ISlackBot bot) public User User { get; set; } public string Text { get; set; } public string Ts { get; set; } + [JsonIgnore] public DateTime Timestamp => Ts.ToDateTime().GetValueOrDefault(); public string ThreadTs { get; set; } + [JsonIgnore] public DateTime ThreadTimestamp => Ts.ToDateTime().GetValueOrDefault(); public IList Attachments { get; set; } = new List(); public bool IsInThread => ThreadTs != null; diff --git a/SlackNet.EventsExample/ColorSelector.cs b/SlackNet.EventsExample/ColorSelector.cs new file mode 100644 index 0000000..c00df94 --- /dev/null +++ b/SlackNet.EventsExample/ColorSelector.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Threading.Tasks; + +namespace SlackNet.EventsExample +{ + public class ColorSelector : IActionHandler, IOptionProvider + { + public static readonly string ActionName = "color_select"; + + public async Task Handle(InteractiveMessage message) + { + message.OriginalAttachment.Color = message.Action.SelectedValue; + message.OriginalAttachment.Actions[0].SelectedOptions = new List