Skip to content

Commit

Permalink
Adding support for Events & related APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
soxtoby committed Dec 29, 2017
1 parent 2b694aa commit 44ac9ed
Show file tree
Hide file tree
Showing 70 changed files with 1,304 additions and 53 deletions.
29 changes: 29 additions & 0 deletions SlackNet.AspNetCore/AspNetCoreExtensions.cs
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);
}
}
}
19 changes: 19 additions & 0 deletions SlackNet.AspNetCore/HttpContextExtensions.cs
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;
}
}
}
33 changes: 33 additions & 0 deletions SlackNet.AspNetCore/ResolvedActionHandler.cs
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);
}
}
}
28 changes: 28 additions & 0 deletions SlackNet.AspNetCore/ResolvedEventHandler.cs
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);
}
}
}
}
33 changes: 33 additions & 0 deletions SlackNet.AspNetCore/ResolvedOptionProvider.cs
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);
}
}
}
19 changes: 19 additions & 0 deletions SlackNet.AspNetCore/SlackActionsService.cs
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);
}
}
32 changes: 32 additions & 0 deletions SlackNet.AspNetCore/SlackEndpointConfiguration.cs
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; }
}
}
123 changes: 123 additions & 0 deletions SlackNet.AspNetCore/SlackEventsMiddleware.cs
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)));
}
}
25 changes: 25 additions & 0 deletions SlackNet.AspNetCore/SlackEventsService.cs
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);
}
}
27 changes: 27 additions & 0 deletions SlackNet.AspNetCore/SlackNet.AspNetCore.csproj
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>
Loading

0 comments on commit 44ac9ed

Please sign in to comment.