Skip to content

Commit

Permalink
Added hub and flow tokens. Clean up and add more serialization bits.
Browse files Browse the repository at this point in the history
  • Loading branch information
IEvangelist committed Jul 3, 2024
1 parent 1a830c5 commit 0681e02
Show file tree
Hide file tree
Showing 13 changed files with 154 additions and 20 deletions.
3 changes: 2 additions & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"sdk": {
"version": "8.0.301"
"version": "8.0.302",
"rollForward": "latestFeature"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ internal sealed class DefaultProfaneContentFilterService(IMemoryCache cache) : I
private const string ProfaneListKey = nameof(ProfaneListKey);

/// <inheritdoc />
public async Task<Dictionary<string, ProfaneSourceFilter>> ReadAllProfaneWordsAsync()
public async Task<Dictionary<string, ProfaneSourceFilter>> ReadAllProfaneWordsAsync(
CancellationToken cancellationToken)
{
return await cache.GetOrCreateAsync(ProfaneListKey, async entry =>
{
Expand All @@ -26,6 +27,7 @@ public async Task<Dictionary<string, ProfaneSourceFilter>> ReadAllProfaneWordsAs
static fileName => new KeyValuePair<string, List<string>>(fileName, [])));

await Parallel.ForEachAsync(fileNames,
cancellationToken,
async (fileName, cancellationToken) =>
{
var content = await ProfaneContentReader.ReadAsync(
Expand Down Expand Up @@ -60,7 +62,8 @@ await Parallel.ForEachAsync(fileNames,
/// <inheritdoc />
public async ValueTask<FilterResult> FilterProfanityAsync(
string content,
FilterParameters parameters)
FilterParameters parameters,
CancellationToken cancellationToken)
{
var (strategy, target) = parameters;
FilterResult result = new(content, parameters);
Expand All @@ -71,7 +74,7 @@ public async ValueTask<FilterResult> FilterProfanityAsync(
}

var wordList =
await ReadAllProfaneWordsAsync().ConfigureAwait(false);
await ReadAllProfaneWordsAsync(cancellationToken).ConfigureAwait(false);

foreach (var profaneSourceFilter in parameters.AdditionalFilterSources ?? [])
{
Expand Down
8 changes: 6 additions & 2 deletions src/ProfanityFilter.Services/IProfaneContentFilterService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,20 @@ public interface IProfaneContentFilterService
/// </summary>
/// <param name="content">The content to filter.</param>
/// <param name="parameters">The parameters to employ when filtering content.</param>
/// <param name="cancellationToken">The cancellation token used to signal cancellation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> representing the asynchronous
/// operation, containing the filtered content.</returns>
ValueTask<FilterResult> FilterProfanityAsync(
string content,
FilterParameters parameters);
FilterParameters parameters,
CancellationToken cancellationToken = default);

/// <summary>
/// Reads all profane words from their respective sources asynchronously.
/// </summary>
/// <param name="cancellationToken">The cancellation token used to signal cancellation.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation that
/// returns a readonly dictionary of all profane words.</returns>
Task<Dictionary<string, ProfaneSourceFilter>> ReadAllProfaneWordsAsync();
Task<Dictionary<string, ProfaneSourceFilter>> ReadAllProfaneWordsAsync(
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,24 @@ internal static WebApplication MapProfanityFilterEndpoints(this WebApplication a
{
var profanity = app.MapGroup("profanity");

profanity.MapHub<ProfanityHub>("hub", options =>
{
options.AllowStatefulReconnects = true;
options.Transports =
HttpTransportType.WebSockets |
HttpTransportType.ServerSentEvents |
HttpTransportType.LongPolling;
})
// Doesn't actually work, consider AsyncAPI per Safia!
.WithOpenApi()
.WithSummary("""
The profanity filter hub endpoint, used for live bi-directional updates.
""");

profanity.MapPost("filter", OnApplyFilterAsync)
.WithOpenApi()
.Produces(200, typeof(ProfanityFilterResponse))
.ProducesValidationProblem()
.WithRequestTimeout(TimeSpan.FromSeconds(10))
.WithSummary("""
Use this endpoint to attempt applying a profanity-filter. The response is returned as Markdown.
Expand All @@ -19,17 +35,29 @@ Use this endpoint to attempt applying a profanity-filter. The response is return

profanity.MapGet("strategies", OnGetStrategies)
.WithOpenApi()
.Produces(200, typeof(StrategyResponse[]))
.WithRequestTimeout(TimeSpan.FromSeconds(10))
.CacheOutput()
.WithSummary("""
Returns an array of the possible replacement strategies available. See https://github.com/IEvangelist/profanity-filter?tab=readme-ov-file#-replacement-strategies
""")
.WithHttpLogging(HttpLoggingFields.All);

profanity.MapGet("targets", OnGetTargets)
.WithOpenApi()
.Produces(200, typeof(FilterTargetResponse[]))
.WithRequestTimeout(TimeSpan.FromSeconds(10))
.CacheOutput()
.WithSummary("""
Returns an array of the possible filter targets available.
""")
.WithHttpLogging(HttpLoggingFields.All);

var data = profanity.MapGroup("data");

data.MapGet("names", OnGetDataNamesAsync)
data.MapGet("", OnGetDataNamesAsync)
.WithOpenApi()
.Produces(200, typeof(string[]))
.WithRequestTimeout(TimeSpan.FromSeconds(10))
.CacheOutput()
.WithSummary("""
Expand All @@ -39,6 +67,7 @@ Returns an array of the data names.

data.MapGet("{name}", OnGetDataByNameAsync)
.WithOpenApi()
.Produces(200, typeof(string[]))
.WithRequestTimeout(TimeSpan.FromSeconds(10))
.CacheOutput()
.WithSummary("""
Expand Down Expand Up @@ -84,13 +113,20 @@ private static async Task<IResult> OnApplyFilterAsync(
SourceGenerationContext.Default.ProfanityFilterResponse);
}

private static IResult OnGetStrategies() =>
private static JsonHttpResult<StrategyResponse[]> OnGetStrategies() =>
TypedResults.Json([
.. Enum.GetValues<ReplacementStrategy>()
],
SourceGenerationContext.Default.StrategyResponseArray
);

private static JsonHttpResult<FilterTargetResponse[]> OnGetTargets() =>
TypedResults.Json([
.. Enum.GetValues<FilterTarget>()
],
SourceGenerationContext.Default.FilterTargetResponseArray
);

private static async Task<IResult> OnGetDataNamesAsync(
[FromServices] IProfaneContentFilterService filterService)
{
Expand Down
7 changes: 5 additions & 2 deletions src/ProfanityFilter.WebApi/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@
// Licensed under the MIT License.

global using System.Diagnostics.CodeAnalysis;

global using System.Runtime.CompilerServices;
global using System.Text.Json;
global using System.Text.Json.Serialization;

global using Microsoft.AspNetCore.Http.Connections;
global using Microsoft.AspNetCore.Http.HttpResults;
global using Microsoft.AspNetCore.HttpLogging;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.AspNetCore.Mvc.ModelBinding;
global using Microsoft.AspNetCore.SignalR;

global using ProfanityFilter.Services;
global using ProfanityFilter.Services.Extensions;
global using ProfanityFilter.Services.Filters;
global using ProfanityFilter.Services.Results;

global using ProfanityFilter.WebApi.Endpoints;
global using ProfanityFilter.WebApi.Hubs;
global using ProfanityFilter.WebApi.Models;
global using ProfanityFilter.WebApi.Serialization;
30 changes: 30 additions & 0 deletions src/ProfanityFilter.WebApi/Hubs/ProfanityHub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

namespace ProfanityFilter.WebApi.Hubs;

public sealed class ProfanityHub(IProfaneContentFilterService filter) : Hub
{
[HubMethodName("live")]
public async IAsyncEnumerable<ProfanityFilterResponse> LiveStream(
IAsyncEnumerable<ProfanityFilterRequest> liveRequests,
[EnumeratorCancellation]
CancellationToken cancellationToken)
{
await foreach (var request in liveRequests)
{
var parameters = new FilterParameters(
request.Strategy, request.Target);

var result = await filter.FilterProfanityAsync(
request.Text, parameters, cancellationToken);

if (result is null or { FinalOutput: null })
{
yield break;
}

yield return (result, request.Strategy);
}
}
}
25 changes: 25 additions & 0 deletions src/ProfanityFilter.WebApi/Models/FilterTargetResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

namespace ProfanityFilter.WebApi.Models;

public sealed record class FilterTargetResponse(
string Name,
int Value,
string Description)
{
public static implicit operator FilterTargetResponse(FilterTarget target)
{
var description = target switch
{
FilterTarget.Title => "Used to indicate that the target of the filter should not include escaped Markdown.",
FilterTarget.Body => "Used to indicate that the target of the filter should include escaped Markdown.",
_ => "Same as body, used to indicate that the target of the filter should include escaped Markdown.",
};

return new FilterTargetResponse(
Name: Enum.GetName(target) ?? target.ToString(),
Value: (int)target,
Description: description);
}
}
4 changes: 3 additions & 1 deletion src/ProfanityFilter.WebApi/Models/ProfanityFilterRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ namespace ProfanityFilter.WebApi.Models;
/// </summary>
/// <param name="Text">The text to evaluate for profanity.</param>
/// <param name="Strategy">The desired replacement strategy to use. Defaults to <c>*</c>.</param>
/// <param name="Target">The filter target to use. Defaults to body, which is Markdown escaped.</param>
public sealed record class ProfanityFilterRequest(
string Text,
ReplacementStrategy Strategy = ReplacementStrategy.Asterisk);
ReplacementStrategy Strategy = ReplacementStrategy.Asterisk,
FilterTarget Target = FilterTarget.Body);
18 changes: 17 additions & 1 deletion src/ProfanityFilter.WebApi/Models/ProfanityFilterResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,20 @@ public sealed record class ProfanityFilterResponse(
string? FilteredText,
ReplacementStrategy ReplacementStrategy,
ProfanityFilterStep[]? FiltrationSteps = default,
string[]? Matches = default);
string[]? Matches = default)
{
public static implicit operator ProfanityFilterResponse(
(FilterResult result, ReplacementStrategy strategy) pair)
{
var (result, strategy) = pair;

return new(
ContainsProfanity: result.IsFiltered,
InputText: result.Input,
FilteredText: result.FinalOutput,
ReplacementStrategy: strategy,
FiltrationSteps: [.. result.Steps?.Where(static s => s.IsFiltered)],
Matches: [.. result.Matches]
);
}
}
12 changes: 6 additions & 6 deletions src/ProfanityFilter.WebApi/Models/StrategyResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ namespace ProfanityFilter.WebApi.Models;
/// <summary>
/// A representation of a strategy response object.
/// </summary>
/// <param name="StrategyName">The name of the strategy.</param>
/// <param name="StrategyValue">The <c>int</c> value of the strategy.</param>
/// <param name="Name">The name of the strategy.</param>
/// <param name="Value">The <c>int</c> value of the strategy.</param>
/// <param name="Description">The description of the strategy.</param>
public sealed record class StrategyResponse(
string StrategyName,
int StrategyValue,
string Name,
int Value,
string Description)
{
public static implicit operator StrategyResponse(ReplacementStrategy strategy)
Expand All @@ -36,8 +36,8 @@ public static implicit operator StrategyResponse(ReplacementStrategy strategy)
};

return new StrategyResponse(
StrategyName: Enum.GetName(strategy) ?? strategy.ToString(),
StrategyValue: (int)strategy,
Name: Enum.GetName(strategy) ?? strategy.ToString(),
Value: (int)strategy,
Description: description);
}
}
12 changes: 12 additions & 0 deletions src/ProfanityFilter.WebApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,20 @@
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddSignalR();
builder.Services.AddProfanityFilterServices();

builder.Services.ConfigureHttpJsonOptions(
static options =>
options.SerializerOptions.TypeInfoResolverChain.Insert(
0,
SourceGenerationContext.Default));

builder.Services.Configure<JsonHubProtocolOptions>(
static options =>
options.PayloadSerializerOptions.TypeInfoResolverChain.Insert(
0, SourceGenerationContext.Default));

var app = builder.Build();

app.UseSwagger();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ namespace ProfanityFilter.WebApi.Serialization;
[JsonSerializable(typeof(StrategyResponse))]
[JsonSerializable(typeof(StrategyResponse[]))]
[JsonSerializable(typeof(ReplacementStrategy))]
[JsonSerializable(typeof(FilterTarget))]
[JsonSerializable(typeof(FilterTargetResponse[]))]
[JsonSerializable(typeof(FilterStep[]))]
internal partial class SourceGenerationContext : JsonSerializerContext
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ namespace ProfanityFilter.Action.Tests;

internal sealed class TestProfanityFilterService : IProfaneContentFilterService
{
public ValueTask<FilterResult> FilterProfanityAsync(string content, FilterParameters parameters)
public ValueTask<FilterResult> FilterProfanityAsync(string content, FilterParameters parameters, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}

public Task<Dictionary<string, ProfaneSourceFilter>> ReadAllProfaneWordsAsync()
public Task<Dictionary<string, ProfaneSourceFilter>> ReadAllProfaneWordsAsync(CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
Expand Down

0 comments on commit 0681e02

Please sign in to comment.