-
Notifications
You must be signed in to change notification settings - Fork 70
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding support for Events & related APIs
- Loading branch information
Showing
70 changed files
with
1,304 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<SlackServiceConfiguration> configure) | ||
{ | ||
var configuration = new SlackServiceConfiguration(serviceCollection); | ||
configure(configuration); | ||
Default.RegisterServices((serviceType, createService) => serviceCollection.AddTransient(serviceType, c => createService(c.GetService))); | ||
serviceCollection.AddSingleton<ISlackEvents, SlackEventsService>(); | ||
serviceCollection.AddSingleton<ISlackActions, SlackActionsService>(); | ||
serviceCollection.AddSingleton<ISlackOptions, SlackOptionsService>(); | ||
serviceCollection.AddTransient<ISlackApiClient>(c => new SlackApiClient(c.GetService<IHttp>(), c.GetService<ISlackUrlBuilder>(), c.GetService<SlackJsonSettings>(), configuration.ApiToken)); | ||
|
||
return serviceCollection; | ||
} | ||
|
||
public static IApplicationBuilder UseSlackNet(this IApplicationBuilder app, Action<SlackEndpointConfiguration> configure) | ||
{ | ||
var config = new SlackEndpointConfiguration(); | ||
configure(config); | ||
return app.UseMiddleware<SlackEventsMiddleware>(config); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<HttpResponse> 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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<MessageResponse> Handle(InteractiveMessage message); | ||
} | ||
|
||
class ResolvedActionHandler<T> : ResolvedActionHandler | ||
where T : IActionHandler | ||
{ | ||
private readonly IServiceProvider _serviceProvider; | ||
|
||
public ResolvedActionHandler(IServiceProvider serviceProvider, string actionName) | ||
: base(actionName) | ||
{ | ||
_serviceProvider = serviceProvider; | ||
} | ||
|
||
public override Task<MessageResponse> Handle(InteractiveMessage message) | ||
{ | ||
var handler = _serviceProvider.GetRequiredService<T>(); | ||
return handler.Handle(message); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
using System; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using SlackNet.Events; | ||
|
||
namespace SlackNet.AspNetCore | ||
{ | ||
class ResolvedEventHandler<TEvent, THandler> : IEventHandler<TEvent> | ||
where TEvent : Event | ||
where THandler : IEventHandler<TEvent> | ||
{ | ||
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<THandler>(); | ||
await handler.Handle(slackEvent).ConfigureAwait(false); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<OptionsResponse> GetOptions(OptionsRequest request); | ||
} | ||
|
||
class ResolvedOptionProvider<T> : ResolvedOptionProvider | ||
where T : IOptionProvider | ||
{ | ||
private readonly IServiceProvider _serviceProvider; | ||
|
||
public ResolvedOptionProvider(IServiceProvider serviceProvider, string actionName) | ||
: base(actionName) | ||
{ | ||
_serviceProvider = serviceProvider; | ||
} | ||
|
||
public override Task<OptionsResponse> GetOptions(OptionsRequest request) | ||
{ | ||
var handler = _serviceProvider.GetRequiredService<T>(); | ||
return handler.GetOptions(request); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ResolvedActionHandler> handlers) | ||
{ | ||
foreach (var handler in handlers) | ||
_actions.SetHandler(handler.ActionName, handler); | ||
} | ||
|
||
public Task<MessageResponse> Handle(InteractiveMessage request) => _actions.Handle(request); | ||
public void SetHandler(string actionName, IActionHandler handler) => _actions.SetHandler(actionName, handler); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
|
||
/// <summary> | ||
/// Path to receive Slack requests on. Defaults to "slack". | ||
/// Configures the following routes: | ||
/// <br /><c>/{RoutePrefix}/event</c> - Event subscriptions | ||
/// <br /><c>/{RoutePrefix}/action</c> - Interactive components requests | ||
/// <br /><c>/{RoutePrefix}/options</c> - Options loading (for message menus) | ||
/// </summary> | ||
public string RoutePrefix { get; private set; } = "slack"; | ||
|
||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
public string VerificationToken { get; private set; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<HttpResponse> 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<HttpResponse> HandleSlackAction(HttpContext context) | ||
{ | ||
if (context.Request.Method != "POST") | ||
return await context.Respond(HttpStatusCode.MethodNotAllowed).ConfigureAwait(false); | ||
|
||
var interactiveMessage = await DeserializePayload<InteractiveMessage>(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<HttpResponse> HandleSlackOptions(HttpContext context) | ||
{ | ||
if (context.Request.Method != "POST") | ||
return await context.Respond(HttpStatusCode.MethodNotAllowed).ConfigureAwait(false); | ||
|
||
var optionsRequest = await DeserializePayload<OptionsRequest>(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<T> DeserializePayload<T>(HttpContext context) | ||
{ | ||
var form = await context.Request.ReadFormAsync().ConfigureAwait(false); | ||
|
||
return form["payload"] | ||
.Select(p => JsonConvert.DeserializeObject<T>(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<Event>(new JsonTextReader(new StreamReader(context.Request.Body))); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<IEventHandler> eventHandlers) | ||
{ | ||
foreach (var handler in eventHandlers) | ||
AddHandler((dynamic)handler); | ||
} | ||
|
||
public void Handle(EventCallback eventCallback) => _events.Handle(eventCallback); | ||
|
||
public IObservable<EventCallback> RawEvents => _events.RawEvents; | ||
|
||
public IObservable<T> Events<T>() where T : Event => _events.Events<T>(); | ||
|
||
public void AddHandler<T>(IEventHandler<T> handler) where T : Event => _events.AddHandler(handler); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>netstandard2.0</TargetFramework> | ||
<Authors>Simon Oxtoby</Authors> | ||
<Description>ASP.NET Core integration for receiving requests from Slack</Description> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> | ||
<TreatWarningsAsErrors>True</TreatWarningsAsErrors> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> | ||
<TreatWarningsAsErrors>False</TreatWarningsAsErrors> | ||
<DocumentationFile>bin\Release\netstandard1.4\SlackNet.xml</DocumentationFile> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="1.1.0" /> | ||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="1.1.0" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\SlackNet\SlackNet.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Oops, something went wrong.