├── docker-stop.bat ├── docker-pull-latest.bat ├── assets ├── gde.png ├── me.png ├── mvp.png ├── azure-logo.png ├── slide-one.png ├── creamcitycode.png ├── twilio-mark-red.png └── styles.css ├── profanity-filter.png ├── IEvangelist.GitHub.Webhooks ├── host.json ├── Validators │ ├── IGitHubPayloadValidator.cs │ └── GitHubPayloadValidator.cs ├── Extensions │ ├── HttpHeadersExtensions.cs │ └── ByteArrayExtensions.cs ├── IEvangelist.GitHub.Webhooks.csproj ├── Startup.cs ├── WebhookFunction.cs └── .gitignore ├── IEvangelist.GitHub.Services ├── Enums │ ├── ActivityType.cs │ └── BodyTextReplacerType.cs ├── Filters │ ├── IWordReplacer.cs │ ├── IProfanityFilter.cs │ ├── LintLickerWordReplacer.cs │ ├── ProfanityFilter.cs │ ├── GitHubEmojiWordReplacer.cs │ └── InappropriateLanguage.cs ├── Handlers │ ├── IIssueHandler.cs │ ├── IPullRequestHandler.cs │ ├── GitHubBaseHandler.cs │ ├── PullRequestHandler.cs │ └── IssueHandler.cs ├── IGitHubWebhookDispatcher.cs ├── Providers │ ├── IWordReplacerProvider.cs │ └── WordReplacerProvider.cs ├── Extensions │ ├── StringExtensions.cs │ ├── EnumerableExtensions.cs │ └── ServiceCollectionExtensions.cs ├── Options │ └── GitHubOptions.cs ├── Models │ ├── FilterActivity.cs │ └── FilterResult.cs ├── GitHubWebhookDispatcher.cs ├── IEvangelist.GitHub.Services.csproj └── GraphQL │ ├── IGitHubGraphQLClient.cs │ └── GitHubGraphQLClient.cs ├── IEvangelist.GitHub.Repository ├── ICosmosContainerProvider.cs ├── Options │ └── RepositoryOptions.cs ├── BaseDocument.cs ├── IRepository.cs ├── IEvangelist.GitHub.Repository.csproj ├── Extensions │ └── ServiceCollectionExtensions.cs ├── CosmosContainerProvider.cs └── Repository.cs ├── docker-start.bat ├── docker-start-fast.bat ├── docker-start-sweep.bat ├── README.md ├── PITCHME.yaml ├── IEvangelist.GitHub.Sandbox ├── IEvangelist.GitHub.Sandbox.csproj └── Program.cs ├── IEvangelist.GitHub.Tests ├── IEvangelist.GitHub.Tests.csproj └── ProfanityFilterTests.cs ├── IEvangelist.GitHub.ProfanityFilter.sln ├── .gitignore └── PITCHME.md /docker-stop.bat: -------------------------------------------------------------------------------- 1 | docker stop profanity-filter -------------------------------------------------------------------------------- /docker-pull-latest.bat: -------------------------------------------------------------------------------- 1 | docker pull gitpitch/desktop:pro -------------------------------------------------------------------------------- /assets/gde.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/GitHub.ProfanityFilter/HEAD/assets/gde.png -------------------------------------------------------------------------------- /assets/me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/GitHub.ProfanityFilter/HEAD/assets/me.png -------------------------------------------------------------------------------- /assets/mvp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/GitHub.ProfanityFilter/HEAD/assets/mvp.png -------------------------------------------------------------------------------- /assets/azure-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/GitHub.ProfanityFilter/HEAD/assets/azure-logo.png -------------------------------------------------------------------------------- /assets/slide-one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/GitHub.ProfanityFilter/HEAD/assets/slide-one.png -------------------------------------------------------------------------------- /profanity-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/GitHub.ProfanityFilter/HEAD/profanity-filter.png -------------------------------------------------------------------------------- /assets/creamcitycode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/GitHub.ProfanityFilter/HEAD/assets/creamcitycode.png -------------------------------------------------------------------------------- /assets/twilio-mark-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IEvangelist/GitHub.ProfanityFilter/HEAD/assets/twilio-mark-red.png -------------------------------------------------------------------------------- /IEvangelist.GitHub.Webhooks/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "logLevel": { 5 | "Default": "Information" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Enums/ActivityType.cs: -------------------------------------------------------------------------------- 1 | namespace IEvangelist.GitHub.Services.Enums 2 | { 3 | public enum ActivityType 4 | { 5 | Issue, 6 | PullRequest 7 | } 8 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Filters/IWordReplacer.cs: -------------------------------------------------------------------------------- 1 | namespace IEvangelist.GitHub.Services.Filters 2 | { 3 | public interface IWordReplacer 4 | { 5 | string ReplaceWord(string word); 6 | } 7 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Enums/BodyTextReplacerType.cs: -------------------------------------------------------------------------------- 1 | namespace IEvangelist.GitHub.Services.Enums 2 | { 3 | public enum BodyTextReplacerType 4 | { 5 | GitHubEmoji, 6 | LintLicker 7 | } 8 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Repository/ICosmosContainerProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Cosmos; 2 | 3 | namespace IEvangelist.GitHub.Repository 4 | { 5 | public interface ICosmosContainerProvider 6 | { 7 | Container GetContainer(); 8 | } 9 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Handlers/IIssueHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace IEvangelist.GitHub.Services.Handlers 4 | { 5 | public interface IIssueHandler 6 | { 7 | ValueTask HandleIssueAsync(string payloadJson); 8 | } 9 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Webhooks/Validators/IGitHubPayloadValidator.cs: -------------------------------------------------------------------------------- 1 | namespace IEvangelist.GitHub.Webhooks.Validators 2 | { 3 | public interface IGitHubPayloadValidator 4 | { 5 | bool IsPayloadSignatureValid(byte[] bytes, string receivedSignature); 6 | } 7 | } -------------------------------------------------------------------------------- /docker-start.bat: -------------------------------------------------------------------------------- 1 | "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" http://localhost:9000 2 | docker run --name profanity-filter -it -v C:\Users\dapine\source\repos\IEvangelist.GitHub.ProfanityFilter:/repo -p 9000:9000 -e PORT=9000 -e SWEEP=false --rm gitpitch/desktop:pro -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Handlers/IPullRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace IEvangelist.GitHub.Services.Handlers 4 | { 5 | public interface IPullRequestHandler 6 | { 7 | ValueTask HandlePullRequestAsync(string payloadJson); 8 | } 9 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/IGitHubWebhookDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace IEvangelist.GitHub.Services 4 | { 5 | public interface IGitHubWebhookDispatcher 6 | { 7 | ValueTask DispatchAsync(string eventName, string payloadJson); 8 | } 9 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Providers/IWordReplacerProvider.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Filters; 2 | 3 | namespace IEvangelist.GitHub.Services.Providers 4 | { 5 | public interface IWordReplacerProvider 6 | { 7 | IWordReplacer GetWordReplacer(); 8 | } 9 | } -------------------------------------------------------------------------------- /docker-start-fast.bat: -------------------------------------------------------------------------------- 1 | "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" http://localhost:9000 2 | docker run --name profanity-filter -it -v C:\Users\dapine\source\repos\IEvangelist.GitHub.ProfanityFilter:/repo -p 9000:9000 -e PORT=9000 -e SWEEP=fast --rm gitpitch/desktop:pro -------------------------------------------------------------------------------- /docker-start-sweep.bat: -------------------------------------------------------------------------------- 1 | "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" http://localhost:9000 2 | docker run --name profanity-filter -it -v C:\Users\dapine\source\repos\IEvangelist.GitHub.ProfanityFilter:/repo -p 9000:9000 -e PORT=9000 -e SWEEP=true --rm gitpitch/desktop:pro -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Filters/IProfanityFilter.cs: -------------------------------------------------------------------------------- 1 | namespace IEvangelist.GitHub.Services.Filters 2 | { 3 | public interface IProfanityFilter 4 | { 5 | bool IsProfane(string content); 6 | 7 | string ApplyFilter(string content, char? placeHolder = null); 8 | } 9 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Repository/Options/RepositoryOptions.cs: -------------------------------------------------------------------------------- 1 | namespace IEvangelist.GitHub.Repository.Options 2 | { 3 | public class RepositoryOptions 4 | { 5 | public string CosmosConnectionString { get; set; } 6 | 7 | public string DatabaseId { get; set; } 8 | 9 | public string ContainerId { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Repository/BaseDocument.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Cosmos; 2 | using Newtonsoft.Json; 3 | 4 | namespace IEvangelist.GitHub.Repository 5 | { 6 | public class BaseDocument 7 | { 8 | [JsonProperty("id")] 9 | public string Id { get; set; } 10 | 11 | internal PartitionKey PartitionKey => new PartitionKey(Id); 12 | } 13 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub - Profanity Filter 2 | 3 | This project is an Azure function to handle a GitHub webhook, which will take action on issues/pull requests that contain profanity. Currently a webhook on this repository calls the compiled version of this project - the Azure function wakes up and takes appropriate action by filtering out profanity on issue and pull request titles and body text. 4 | 5 | ![Profanity Filter](profanity-filter.png) -------------------------------------------------------------------------------- /IEvangelist.GitHub.Webhooks/Extensions/HttpHeadersExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System.Linq; 3 | 4 | namespace IEvangelist.GitHub.Webhooks.Extensions 5 | { 6 | static class HttpHeadersExtensions 7 | { 8 | internal static string GetValueOrDefault(this IHeaderDictionary headers, string name) => 9 | headers.TryGetValue(name, out var values) ? values.FirstOrDefault() : null; 10 | } 11 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Webhooks/Extensions/ByteArrayExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace IEvangelist.GitHub.Webhooks.Extensions 2 | { 3 | static class ByteArrayExtensions 4 | { 5 | internal static string ToHexString(this byte[] bytes) 6 | { 7 | var builder = new System.Text.StringBuilder(bytes.Length * 2); 8 | foreach (var b in bytes) 9 | { 10 | builder.AppendFormat("{0:x2}", b); 11 | } 12 | 13 | return builder.ToString(); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using Octokit.GraphQL; 2 | using Octokit.Internal; 3 | 4 | namespace IEvangelist.GitHub.Services.Extensions 5 | { 6 | static class StringExtensions 7 | { 8 | static readonly SimpleJsonSerializer Serializer = new SimpleJsonSerializer(); 9 | 10 | internal static ID ToGitHubId(this string value) => new ID(value); 11 | 12 | internal static string ToJson(this T value) => Serializer.Serialize(value); 13 | 14 | internal static T FromJson(this string json) => Serializer.Deserialize(json); 15 | } 16 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Options/GitHubOptions.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Enums; 2 | 3 | namespace IEvangelist.GitHub.Services.Options 4 | { 5 | public class GitHubOptions 6 | { 7 | public string ApiToken { get; set; } 8 | 9 | public string Owner { get; set; } = "IEvangelist"; 10 | 11 | public string Repo { get; set; } = "GitHub.ProfanityFilter"; 12 | 13 | public string WebhookSecret { get; set; } 14 | 15 | public string ProfaneLabelId { get; set; } = "MDU6TGFiZWwxNTI1MDA4Mzkz"; 16 | 17 | public BodyTextReplacerType BodyTextReplacerType { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Repository/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Threading.Tasks; 5 | 6 | namespace IEvangelist.GitHub.Repository 7 | { 8 | public interface IRepository where T : BaseDocument 9 | { 10 | ValueTask GetAsync(string id); 11 | 12 | ValueTask> GetAsync(Expression> predicate); 13 | 14 | ValueTask CreateAsync(T value); 15 | 16 | Task CreateAsync(IEnumerable values); 17 | 18 | ValueTask UpdateAsync(T value); 19 | 20 | ValueTask DeleteAsync(string id); 21 | } 22 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace IEvangelist.GitHub.Services.Extensions 6 | { 7 | static class EnumerableExtensions 8 | { 9 | public static T GetRandomElement(this IEnumerable source) => 10 | source.GetRandomElements(1).Single(); 11 | 12 | public static IEnumerable GetRandomElements(this IEnumerable source, int count) => 13 | source.Shuffle().Take(count); 14 | 15 | public static IEnumerable Shuffle(this IEnumerable source) => 16 | source.OrderBy(x => Guid.NewGuid()); 17 | } 18 | } -------------------------------------------------------------------------------- /PITCHME.yaml: -------------------------------------------------------------------------------- 1 | theme: template 2 | theme-background: [ "black" ] 3 | theme-headline: [ "Raleway", "white", "none" ] 4 | theme-byline: [ "Raleway", "#C0C0C0", "none" ] 5 | theme-text: [ "Ubuntu", "#F5FFFA", "none" ] 6 | theme-links: [ "#E27924", "#FF851B" ] 7 | theme-code: [ "Fira Code" ] 8 | theme-controls: [ "#C0C0C0" ] 9 | theme-margins: [ "0", "15px" ] 10 | highlight: ir-black 11 | code-line-numbers : true 12 | eager-loading : true 13 | logo: assets/azure-logo.png 14 | transition: fade 15 | title: "WTF GitHub" 16 | remote-control: true 17 | remote-control-prevkey: 38 18 | remote-control-nextkey: 40 19 | theme-override : assets/styles.css 20 | published: false 21 | footnote : "davidpine.net • @davidpine7" -------------------------------------------------------------------------------- /IEvangelist.GitHub.Sandbox/IEvangelist.GitHub.Sandbox.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Models/FilterActivity.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Enums; 2 | using Microsoft.Azure.CosmosRepository; 3 | using System; 4 | 5 | namespace IEvangelist.GitHub.Services.Models 6 | { 7 | public class FilterActivity : Item 8 | { 9 | public string MutationOrNodeId { get; set; } 10 | 11 | public ActivityType ActivityType { get; set; } 12 | 13 | public DateTime WorkedOn { get; set; } 14 | 15 | public bool WasProfane { get; set; } 16 | 17 | public string OriginalTitleText { get; set; } 18 | 19 | public string ModifiedTitleText { get; set; } 20 | 21 | public string OriginalBodyText { get; set; } 22 | 23 | public string ModifiedBodyText { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Repository/IEvangelist.GitHub.Repository.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /IEvangelist.GitHub.Repository/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Repository.Options; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace IEvangelist.GitHub.Repository.Extensions 6 | { 7 | public static class ServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddGitHubRepository( 10 | this IServiceCollection services, 11 | IConfiguration configuration) => 12 | services.AddSingleton() 13 | .AddSingleton(typeof(IRepository<>), typeof(Repository<>)) 14 | .Configure( 15 | configuration.GetSection(nameof(RepositoryOptions))); 16 | } 17 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Models/FilterResult.cs: -------------------------------------------------------------------------------- 1 | namespace IEvangelist.GitHub.Services.Models 2 | { 3 | internal readonly struct FilterResult 4 | { 5 | internal static FilterResult NotFiltered { get; } = new FilterResult(); 6 | 7 | internal string Title { get; } 8 | internal bool IsTitleFiltered { get; } 9 | 10 | internal string Body { get; } 11 | internal bool IsBodyFiltered { get; } 12 | 13 | internal bool IsFiltered => IsTitleFiltered || IsBodyFiltered; 14 | 15 | internal FilterResult( 16 | string title, 17 | bool isTitleFiltered, 18 | string body, 19 | bool isBodyFiltered) => 20 | (Title, IsTitleFiltered, Body, IsBodyFiltered) = 21 | (title, isTitleFiltered, body, isBodyFiltered); 22 | } 23 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Tests/IEvangelist.GitHub.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /IEvangelist.GitHub.Webhooks/IEvangelist.GitHub.Webhooks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | v3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | PreserveNewest 15 | 16 | 17 | PreserveNewest 18 | Never 19 | 20 | 21 | -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Filters/LintLickerWordReplacer.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Extensions; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace IEvangelist.GitHub.Services.Filters 6 | { 7 | public class LintLickerWordReplacer : IWordReplacer 8 | { 9 | // In homage to https://www.youtube.com/watch?v=5ssytWYn6nY 10 | internal static ISet Words { get; } = new HashSet 11 | { 12 | "biscuit-eating-bulldog", 13 | "french-toast", 14 | "doodoo-head", 15 | "cootie-queen", 16 | "lint-licker", 17 | "pickle", 18 | "kumquat", 19 | "stinky-mc-stinkface" 20 | }; 21 | 22 | public string ReplaceWord(string word) => Words.Shuffle() 23 | .OrderByDescending(x => x.StartsWith(word[0].ToString()) ? 1 : 0) 24 | .FirstOrDefault(); 25 | } 26 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/GitHubWebhookDispatcher.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Handlers; 2 | using System.Threading.Tasks; 3 | 4 | namespace IEvangelist.GitHub.Services 5 | { 6 | public class GitHubWebhookDispatcher : IGitHubWebhookDispatcher 7 | { 8 | static readonly ValueTask NoopTask = new ValueTask(); 9 | 10 | readonly IIssueHandler _issueHandler; 11 | readonly IPullRequestHandler _pullRequestHandler; 12 | 13 | public GitHubWebhookDispatcher( 14 | IIssueHandler issueHandler, 15 | IPullRequestHandler pullRequestHandler) => 16 | (_issueHandler, _pullRequestHandler) = (issueHandler, pullRequestHandler); 17 | 18 | public ValueTask DispatchAsync(string eventName, string payloadJson) 19 | => eventName switch 20 | { 21 | "issues" => _issueHandler.HandleIssueAsync(payloadJson), 22 | "pull_request" => _pullRequestHandler.HandlePullRequestAsync(payloadJson), 23 | 24 | _ => NoopTask, 25 | }; 26 | } 27 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Repository/CosmosContainerProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using IEvangelist.GitHub.Repository.Options; 3 | using Microsoft.Azure.Cosmos; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace IEvangelist.GitHub.Repository 7 | { 8 | public class CosmosContainerProvider : ICosmosContainerProvider, IDisposable 9 | { 10 | readonly RepositoryOptions _options; 11 | 12 | CosmosClient _client; 13 | Container _container; 14 | 15 | public CosmosContainerProvider( 16 | IOptions options) => 17 | _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); 18 | 19 | public Container GetContainer() 20 | { 21 | if (_container is null) 22 | { 23 | _client = new CosmosClient(_options.CosmosConnectionString); 24 | var database = _client.GetDatabase(_options.DatabaseId); 25 | _container = database.GetContainer(_options.ContainerId); 26 | } 27 | 28 | return _container; 29 | } 30 | 31 | public void Dispose() => _client?.Dispose(); 32 | } 33 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/IEvangelist.GitHub.Services.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 8.0 6 | 1.1.0.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/GraphQL/IGitHubGraphQLClient.cs: -------------------------------------------------------------------------------- 1 | using Octokit; 2 | using Octokit.GraphQL.Model; 3 | using System.Threading.Tasks; 4 | 5 | namespace IEvangelist.GitHub.Services.GraphQL 6 | { 7 | public interface IGitHubGraphQLClient 8 | { 9 | ValueTask AddLabelAsync(string issueOrPullRequestId, string[] labelIds, string clientId); 10 | 11 | ValueTask RemoveLabelAsync(string issueOrPullRequestId, string clientId); 12 | 13 | ValueTask AddReactionAsync(string issueOrPullRequestId, ReactionContent reaction, string clientId); 14 | 15 | ValueTask RemoveReactionAsync(string issueOrPullRequestId, ReactionContent reaction, string clientId); 16 | 17 | ValueTask UpdateIssueAsync(UpdateIssueInput input); 18 | 19 | ValueTask UpdateIssueAsync(int number, IssueUpdate input); 20 | 21 | ValueTask UpdatePullRequestAsync(UpdatePullRequestInput input); 22 | 23 | ValueTask UpdatePullRequestAsync(int number, PullRequestUpdate input); 24 | 25 | ValueTask<(string title, string body)> GetIssueTitleAndBodyAsync(int issueNumber); 26 | 27 | ValueTask<(string title, string body)> GetPullRequestTitleAndBodyAsync(int pullRequestNumber); 28 | } 29 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Providers/WordReplacerProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using IEvangelist.GitHub.Services.Enums; 5 | using IEvangelist.GitHub.Services.Filters; 6 | using IEvangelist.GitHub.Services.Options; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace IEvangelist.GitHub.Services.Providers 10 | { 11 | public class WordReplacerProvider : IWordReplacerProvider 12 | { 13 | readonly IEnumerable _wordReplacers; 14 | readonly GitHubOptions _config; 15 | 16 | public WordReplacerProvider( 17 | IEnumerable wordReplacers, 18 | IOptions options) => 19 | (_wordReplacers, _config) = (wordReplacers, options.Value); 20 | 21 | public IWordReplacer GetWordReplacer() => 22 | _config.BodyTextReplacerType switch 23 | { 24 | BodyTextReplacerType.GitHubEmoji => _wordReplacers.FirstOrDefault(wr => wr is GitHubEmojiWordReplacer), 25 | BodyTextReplacerType.LintLicker => _wordReplacers.FirstOrDefault(wr => wr is LintLickerWordReplacer), 26 | _ => throw new Exception("There is no corresponding word replacer available.") 27 | }; 28 | } 29 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Webhooks/Startup.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Extensions; 2 | using IEvangelist.GitHub.Webhooks; 3 | using IEvangelist.GitHub.Webhooks.Validators; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using System; 9 | using System.Linq; 10 | 11 | [assembly: WebJobsStartup(typeof(Startup))] 12 | namespace IEvangelist.GitHub.Webhooks 13 | { 14 | internal class Startup : IWebJobsStartup 15 | { 16 | public void Configure(IWebJobsBuilder builder) 17 | { 18 | var descriptor = builder.Services.FirstOrDefault(d => d.ServiceType == typeof(IConfiguration)); 19 | if (descriptor?.ImplementationInstance is IConfigurationRoot configuration) 20 | { 21 | ConfigureServices(builder.Services, configuration).BuildServiceProvider(true); 22 | } 23 | else 24 | { 25 | throw new ApplicationException("The function requires a valid IConfigurationRoot instance."); 26 | } 27 | } 28 | 29 | IServiceCollection ConfigureServices( 30 | IServiceCollection services, IConfiguration configuration) => 31 | services.AddLogging() 32 | .AddGitHubServices(configuration) 33 | .AddSingleton(); 34 | } 35 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Webhooks/Validators/GitHubPayloadValidator.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Options; 2 | using IEvangelist.GitHub.Webhooks.Extensions; 3 | using Microsoft.Extensions.Options; 4 | using System; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | 8 | namespace IEvangelist.GitHub.Webhooks.Validators 9 | { 10 | public class GitHubPayloadValidator : IGitHubPayloadValidator 11 | { 12 | readonly GitHubOptions _options; 13 | 14 | public GitHubPayloadValidator(IOptions options) => 15 | _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); 16 | 17 | public bool IsPayloadSignatureValid(byte[] bytes, string receivedSignature) 18 | { 19 | if (string.IsNullOrWhiteSpace(receivedSignature)) 20 | { 21 | return false; 22 | } 23 | 24 | using var hmac = new HMACSHA1(Encoding.ASCII.GetBytes(_options.WebhookSecret)); 25 | var hash = hmac.ComputeHash(bytes); 26 | var actualSignature = $"sha1={hash.ToHexString()}"; 27 | 28 | return IsSignatureValid(actualSignature, receivedSignature); 29 | } 30 | 31 | static bool IsSignatureValid(string a, string b) 32 | { 33 | var length = Math.Min(a.Length, b.Length); 34 | var equals = a.Length == b.Length; 35 | for (var i = 0; i < length; ++ i) 36 | { 37 | equals &= a[i] == b[i]; 38 | } 39 | 40 | return equals; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Filters; 2 | using IEvangelist.GitHub.Services.GraphQL; 3 | using IEvangelist.GitHub.Services.Handlers; 4 | using IEvangelist.GitHub.Services.Options; 5 | using IEvangelist.GitHub.Services.Providers; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace IEvangelist.GitHub.Services.Extensions 11 | { 12 | public static class ServiceCollectionExtensions 13 | { 14 | public static IServiceCollection AddGitHubServices( 15 | this IServiceCollection services, 16 | IConfiguration configuration) => 17 | services.AddLogging(logging => logging.AddFilter(level => true)) 18 | .AddOptions() 19 | .Configure(configuration.GetSection(nameof(GitHubOptions))) 20 | .AddSingleton() 21 | .AddSingleton() 22 | .AddSingleton() 23 | .AddSingleton() 24 | .AddSingleton() 25 | .AddSingleton() 26 | .AddSingleton() 27 | .AddSingleton() 28 | .AddCosmosRepository(); 29 | } 30 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Filters/ProfanityFilter.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Providers; 2 | using System.Linq; 3 | 4 | namespace IEvangelist.GitHub.Services.Filters 5 | { 6 | public class ProfanityFilter : IProfanityFilter 7 | { 8 | readonly IWordReplacer _wordReplacer; 9 | 10 | public ProfanityFilter(IWordReplacerProvider provider) => 11 | _wordReplacer = provider.GetWordReplacer(); 12 | 13 | public bool IsProfane(string content) => 14 | string.IsNullOrWhiteSpace(content) 15 | ? false 16 | : InappropriateLanguage.Expressions 17 | .Value 18 | .Any(exp => exp.IsMatch(content)); 19 | 20 | public string ApplyFilter(string content, char? placeHolder = null) => 21 | ReplaceProfanity(content, placeHolder); 22 | 23 | string ReplaceProfanity(string content, char? placeHolder = null) 24 | { 25 | var words = 26 | content.Split(new[] { ' ' }) 27 | .Select(word => (isProfane: IsProfane(word), word)) 28 | .ToList(); 29 | 30 | return string.Join( 31 | " ", 32 | words.Select(_ => 33 | _.isProfane 34 | ? GetReplacement(_.word, placeHolder) 35 | : _.word)); 36 | } 37 | 38 | string GetReplacement(string word, char? placeHolder = null) => 39 | placeHolder.HasValue 40 | ? new string(placeHolder.Value, word.Length) 41 | : _wordReplacer.ReplaceWord(word); 42 | } 43 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Handlers/GitHubBaseHandler.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Filters; 2 | using IEvangelist.GitHub.Services.GraphQL; 3 | using IEvangelist.GitHub.Services.Models; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace IEvangelist.GitHub.Services.Hanlders 7 | { 8 | public class GitHubBaseHandler 9 | { 10 | protected readonly IGitHubGraphQLClient _client; 11 | protected readonly IProfanityFilter _profanityFilter; 12 | protected readonly ILogger _logger; 13 | 14 | public GitHubBaseHandler( 15 | IGitHubGraphQLClient client, 16 | IProfanityFilter profanityFilter, 17 | ILogger logger) => 18 | (_client, _profanityFilter, _logger) = (client, profanityFilter, logger); 19 | 20 | internal FilterResult TryApplyProfanityFilter( 21 | string title, 22 | string body) 23 | { 24 | if (string.IsNullOrWhiteSpace(title) && 25 | string.IsNullOrWhiteSpace(body)) 26 | { 27 | return FilterResult.NotFiltered; 28 | } 29 | 30 | var (resultingTitle, isTitleFiltered) = TryApplyFilter(title, '*'); 31 | var (resultingBody, isBodyFiltered) = TryApplyFilter(body); 32 | 33 | return new FilterResult( 34 | resultingTitle, 35 | isTitleFiltered, 36 | resultingBody, 37 | isBodyFiltered); 38 | } 39 | 40 | (string text, bool isFiltered) TryApplyFilter( 41 | string text, 42 | char? placeHolder = null) 43 | { 44 | var filterText = _profanityFilter?.IsProfane(text) ?? false; 45 | var resultingText = 46 | filterText 47 | ? _profanityFilter?.ApplyFilter(text, placeHolder) 48 | : text; 49 | 50 | if (filterText) 51 | { 52 | _logger.LogInformation($"Replaced text: {resultingText}"); 53 | } 54 | 55 | return (resultingText, filterText); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /assets/styles.css: -------------------------------------------------------------------------------- 1 | a:hover { 2 | text-decoration: underline; 3 | } 4 | 5 | img.clear { 6 | border: none !important; 7 | box-shadow: none !important; 8 | background: none !important; 9 | filter: none; 10 | } 11 | 12 | img.rounded { 13 | border: 10px solid #4e4d4f !important; 14 | padding: 2px; 15 | border-radius: 50% !important; 16 | } 17 | 18 | img.glow { 19 | filter: drop-shadow(0px 0px 5px white); 20 | } 21 | 22 | 23 | .no-bullets { 24 | list-style-type: none !important; 25 | } 26 | 27 | .clear > img, .clear > a > img 28 | { 29 | border: none !important; 30 | box-shadow: none !important; 31 | background: none !important; 32 | filter: none; 33 | } 34 | 35 | .fit > img, 36 | .fit > a > img { 37 | max-width: 180px !important; 38 | max-height: 180px !important; 39 | } 40 | 41 | .twitter { 42 | color: #0084b4; 43 | } 44 | 45 | div.south-east > a:nth-child(1) { 46 | right: -12px; 47 | } 48 | 49 | .az { 50 | color: #4DB0FF; 51 | } 52 | 53 | .dark-bg > blockquote { 54 | background: rgba(0,0,0,0.7) !important; 55 | } 56 | 57 | .reveal blockquote { 58 | border-radius: 17px; 59 | padding: 17px; 60 | background: rgba(255,255,255,0.17); 61 | width: 85%; 62 | } 63 | 64 | .reveal blockquote p { 65 | font-style: normal; 66 | } 67 | 68 | .reveal blockquote p i.fa-quote-left { 69 | float: left; 70 | margin-left: -52px; 71 | font-size: 77px; 72 | } 73 | 74 | i.fa.fa-arrow-right { 75 | transition: visibility 0s, opacity 0.3s ease-in-out; 76 | } 77 | 78 | .animate-left-to-right { 79 | animation-name: move-left-to-right; 80 | animation-duration: 3s; 81 | animation-delay: 1s; 82 | animation-iteration-count: infinite; 83 | animation-direction: alternative; 84 | } 85 | 86 | @keyframes move-left-to-right { 87 | 0% { 88 | transform: translateX(-9%); 89 | } 90 | 50% { 91 | transform: translateX(2%); 92 | } 93 | 100% { 94 | transform: translateX(-9%); 95 | } 96 | } 97 | 98 | .rotate-180 { 99 | transform: rotate(180deg) !important; 100 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Filters/GitHubEmojiWordReplacer.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Extensions; 2 | using System.Collections.Generic; 3 | 4 | namespace IEvangelist.GitHub.Services.Filters 5 | { 6 | public class GitHubEmojiWordReplacer : IWordReplacer 7 | { 8 | // Borrowed from: https://www.webfx.com/tools/emoji-cheat-sheet/ 9 | internal static ISet Emoji { get; } = new HashSet 10 | { 11 | ":rage:", 12 | ":boom:", 13 | ":poop:", 14 | ":cherry_blossom:", 15 | ":fire:", 16 | ":facepunch:", 17 | ":weary:", 18 | ":laughing:", 19 | ":octopus:", 20 | ":hurtrealbad:", 21 | ":baby:", 22 | ":baby_bottle:", 23 | ":warning:", 24 | ":revolving_hearts:", 25 | ":see_no_evil:", 26 | ":snowflake:", 27 | ":zap:", 28 | ":ribbon:", 29 | ":angel:", 30 | ":monkey:", 31 | ":bug:", 32 | ":rose:", 33 | ":sparkles:", 34 | ":heartpulse:", 35 | ":cry:", 36 | ":floppy_disk:", 37 | ":question:", 38 | ":speak_no_evil:", 39 | ":tada:", 40 | ":crying_cat_face:", 41 | ":rage2:", 42 | ":ok:", 43 | ":sparkling_heart:", 44 | ":love_letter:", 45 | ":scream:", 46 | ":gift_heart:", 47 | ":grin:", 48 | ":hear_no_evil:", 49 | ":flushed:", 50 | ":sunflower:", 51 | ":anguished:", 52 | ":expressionless:", 53 | ":cold_sweat:", 54 | ":triumph:", 55 | ":confetti_ball:", 56 | ":toilet:", 57 | ":disappointed:", 58 | ":baby_bottle:", 59 | ":confounded:", 60 | ":broken_heart:", 61 | ":exclamation:", 62 | ":boom:", 63 | ":fire:", 64 | ":dancer:", 65 | ":japanese_goblin:", 66 | ":stuck_out_tongue_winking_eye:", 67 | ":cool:", 68 | ":whale:", 69 | ":hibiscus:", 70 | ":rabbit2:", 71 | ":baby_chick:", 72 | ":frog:", 73 | ":dog:", 74 | ":neckbeard:", 75 | ":cyclone:" 76 | }; 77 | 78 | public string ReplaceWord(string word) => Emoji.GetRandomElement(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /IEvangelist.GitHub.Tests/ProfanityFilterTests.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Filters; 2 | using IEvangelist.GitHub.Services.Options; 3 | using IEvangelist.GitHub.Services.Providers; 4 | using Microsoft.Extensions.Options; 5 | using System; 6 | using Xunit; 7 | 8 | namespace IEvangelist.GitHub.Tests 9 | { 10 | public class ProfanityFilterTests 11 | { 12 | readonly IProfanityFilter _sut = 13 | new ProfanityFilter( 14 | new WordReplacerProvider( 15 | new IWordReplacer[] 16 | { 17 | new GitHubEmojiWordReplacer(), 18 | new LintLickerWordReplacer() 19 | }, 20 | Options.Create(new GitHubOptions()))); 21 | 22 | [Fact] 23 | public void CorrectlyReplacesSimpleTextProfanity() 24 | { 25 | var input = "fuck this issue"; 26 | 27 | Assert.True(_sut.IsProfane(input)); 28 | 29 | var actual = _sut.ApplyFilter(input); 30 | 31 | Assert.False(_sut.IsProfane(actual)); 32 | Assert.False(actual.Contains("fuck", StringComparison.OrdinalIgnoreCase)); 33 | } 34 | 35 | [Fact] 36 | public void CorrectlyReplacesMultilineProfanity() 37 | { 38 | var input = @"I'm extremely pissed off by this non-sense. All this project ever does 39 | is fuck around and it's bullshit! Seriously, when are you assholes going to get it together?!"; 40 | 41 | Assert.True(_sut.IsProfane(input)); 42 | 43 | var actual = _sut.ApplyFilter(input); 44 | 45 | Assert.False(_sut.IsProfane(actual)); 46 | Assert.False(actual.Contains("fuck", StringComparison.OrdinalIgnoreCase)); 47 | Assert.False(actual.Contains("bullshit", StringComparison.OrdinalIgnoreCase)); 48 | Assert.False(actual.Contains("assholes", StringComparison.OrdinalIgnoreCase)); 49 | } 50 | 51 | [Fact] 52 | public void CorrectlyReplacesProfanityWithPlaceHolders() 53 | { 54 | var input = "What the fuck! I'm baffled by you basterds - you're a sorry s.o.b."; 55 | 56 | Assert.True(_sut.IsProfane(input)); 57 | 58 | var actual = _sut.ApplyFilter(input, '*'); 59 | 60 | Assert.False(_sut.IsProfane(actual)); 61 | Assert.True(actual.Contains("****", StringComparison.OrdinalIgnoreCase)); 62 | Assert.True(actual.Contains("******", StringComparison.OrdinalIgnoreCase)); 63 | Assert.True(actual.Contains("********", StringComparison.OrdinalIgnoreCase)); 64 | } 65 | 66 | [Fact] 67 | public void CorrectlyReportsFalseToNegativeSentimentText() => 68 | Assert.False( 69 | _sut.IsProfane( 70 | "I don't see how to run this tutorial and stitch together a complete system.")); 71 | } 72 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Webhooks/WebhookFunction.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services; 2 | using IEvangelist.GitHub.Webhooks.Extensions; 3 | using IEvangelist.GitHub.Webhooks.Validators; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Azure.WebJobs; 7 | using Microsoft.Azure.WebJobs.Extensions.Http; 8 | using Microsoft.Extensions.Logging; 9 | using System; 10 | using System.IO; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace IEvangelist.GitHub.ProfanityFilter 15 | { 16 | public class WebhookFunction 17 | { 18 | const string EventHeader = "X-GitHub-Event"; 19 | const string DeliveryHeader = "X-GitHub-Delivery"; 20 | const string SignatureHeader = "X-Hub-Signature"; 21 | 22 | readonly IGitHubPayloadValidator _payloadValidator; 23 | readonly IGitHubWebhookDispatcher _webhookDispatcher; 24 | readonly ILogger _logger; 25 | 26 | public WebhookFunction( 27 | IGitHubPayloadValidator payloadValidator, 28 | IGitHubWebhookDispatcher webhookDispatcher, 29 | ILogger logger) => 30 | (_payloadValidator, _webhookDispatcher, _logger) = 31 | (payloadValidator, webhookDispatcher, logger); 32 | 33 | [FunctionName(nameof(ProcessWebhook))] 34 | public async Task ProcessWebhook( 35 | [HttpTrigger(AuthorizationLevel.Function, "POST")] HttpRequest request) 36 | { 37 | try 38 | { 39 | if (request is null) 40 | { 41 | _logger.LogError("Opps... something went terribly wrong!"); 42 | } 43 | 44 | var signature = request.Headers.GetValueOrDefault(SignatureHeader); 45 | using var reader = new StreamReader(request.Body); 46 | var payloadJson = await reader.ReadToEndAsync(); 47 | _logger.LogInformation(payloadJson); 48 | 49 | if (!_payloadValidator.IsPayloadSignatureValid( 50 | Encoding.UTF8.GetBytes(payloadJson), 51 | signature)) 52 | { 53 | _logger.LogError("Invalid GitHub webhook signature!"); 54 | return new StatusCodeResult(500); 55 | } 56 | 57 | var eventName = request.Headers.GetValueOrDefault(EventHeader); 58 | var deliveryId = request.Headers.GetValueOrDefault(DeliveryHeader); 59 | _logger.LogInformation($"Processing {eventName} ({deliveryId})"); 60 | 61 | await _webhookDispatcher.DispatchAsync(eventName, payloadJson); 62 | 63 | return new OkObjectResult($"Successfully handled the {eventName} event."); 64 | } 65 | catch (Exception ex) 66 | { 67 | _logger.LogError(ex.Message, ex); 68 | return new StatusCodeResult(500); 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Repository/Repository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Cosmos; 2 | using Microsoft.Azure.Cosmos.Linq; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Net; 8 | using System.Threading.Tasks; 9 | 10 | namespace IEvangelist.GitHub.Repository 11 | { 12 | public class Repository : IRepository where T : BaseDocument 13 | { 14 | readonly ICosmosContainerProvider _containerProvider; 15 | 16 | public Repository( 17 | ICosmosContainerProvider containerProvider) => 18 | _containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider)); 19 | 20 | public async ValueTask GetAsync(string id) 21 | { 22 | try 23 | { 24 | var container = _containerProvider.GetContainer(); 25 | var response = await container.ReadItemAsync(id, new PartitionKey(id)); 26 | 27 | return response.Resource; 28 | } 29 | catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) 30 | { 31 | return default; 32 | } 33 | } 34 | 35 | public async ValueTask> GetAsync(Expression> predicate) 36 | { 37 | try 38 | { 39 | var iterator = 40 | _containerProvider.GetContainer() 41 | .GetItemLinqQueryable() 42 | .Where(predicate) 43 | .ToFeedIterator(); 44 | 45 | var results = new List(); 46 | while (iterator.HasMoreResults) 47 | { 48 | foreach (var result in await iterator.ReadNextAsync()) 49 | { 50 | results.Add(result); 51 | } 52 | } 53 | 54 | return results; 55 | } 56 | catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) 57 | { 58 | return Enumerable.Empty(); 59 | } 60 | } 61 | 62 | public async ValueTask CreateAsync(T value) 63 | { 64 | var container = _containerProvider.GetContainer(); 65 | var response = await container.CreateItemAsync(value, value.PartitionKey); 66 | 67 | return response.Resource; 68 | } 69 | 70 | public Task CreateAsync(IEnumerable values) => 71 | Task.WhenAll(values.Select(v => CreateAsync(v).AsTask())); 72 | 73 | public async ValueTask UpdateAsync(T value) 74 | { 75 | var container = _containerProvider.GetContainer(); 76 | var response = await container.UpsertItemAsync(value, value.PartitionKey); 77 | 78 | return response.Resource; 79 | } 80 | 81 | public async ValueTask DeleteAsync(string id) 82 | { 83 | var container = _containerProvider.GetContainer(); 84 | var response = await container.DeleteItemAsync(id, new PartitionKey(id)); 85 | 86 | return response.Resource; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.ProfanityFilter.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29209.152 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IEvangelist.GitHub.Webhooks", "IEvangelist.GitHub.Webhooks\IEvangelist.GitHub.Webhooks.csproj", "{8647BA85-06DC-47D2-8806-2A4CE43F3796}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IEvangelist.GitHub.Services", "IEvangelist.GitHub.Services\IEvangelist.GitHub.Services.csproj", "{9C39BF96-BA63-45D2-B615-B0D3C19554F4}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{27837060-E9DA-46C3-840E-03CCA810AE1C}" 11 | ProjectSection(SolutionItems) = preProject 12 | docker-pull-latest.bat = docker-pull-latest.bat 13 | docker-start-fast.bat = docker-start-fast.bat 14 | docker-start-sweep.bat = docker-start-sweep.bat 15 | docker-start.bat = docker-start.bat 16 | docker-stop.bat = docker-stop.bat 17 | PITCHME.md = PITCHME.md 18 | PITCHME.yaml = PITCHME.yaml 19 | profanity-filter.png = profanity-filter.png 20 | README.md = README.md 21 | EndProjectSection 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IEvangelist.GitHub.Sandbox", "IEvangelist.GitHub.Sandbox\IEvangelist.GitHub.Sandbox.csproj", "{3CC269A9-246D-4E4A-B44E-C0C04C163C20}" 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IEvangelist.GitHub.Tests", "IEvangelist.GitHub.Tests\IEvangelist.GitHub.Tests.csproj", "{4C1C68CE-5B54-4179-B70F-E0173D37EBF5}" 26 | EndProject 27 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{A5803FF2-B14A-4601-B23E-2DD23DD8942D}" 28 | ProjectSection(SolutionItems) = preProject 29 | assets\azure-logo.png = assets\azure-logo.png 30 | assets\creamcitycode.png = assets\creamcitycode.png 31 | assets\gde.png = assets\gde.png 32 | assets\me.png = assets\me.png 33 | assets\mvp.png = assets\mvp.png 34 | assets\slide-one.png = assets\slide-one.png 35 | assets\styles.css = assets\styles.css 36 | assets\twilio-mark-red.png = assets\twilio-mark-red.png 37 | EndProjectSection 38 | EndProject 39 | Global 40 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 41 | Debug|Any CPU = Debug|Any CPU 42 | Release|Any CPU = Release|Any CPU 43 | EndGlobalSection 44 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 45 | {8647BA85-06DC-47D2-8806-2A4CE43F3796}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {8647BA85-06DC-47D2-8806-2A4CE43F3796}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {8647BA85-06DC-47D2-8806-2A4CE43F3796}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {8647BA85-06DC-47D2-8806-2A4CE43F3796}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {9C39BF96-BA63-45D2-B615-B0D3C19554F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {9C39BF96-BA63-45D2-B615-B0D3C19554F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {9C39BF96-BA63-45D2-B615-B0D3C19554F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {9C39BF96-BA63-45D2-B615-B0D3C19554F4}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {3CC269A9-246D-4E4A-B44E-C0C04C163C20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {3CC269A9-246D-4E4A-B44E-C0C04C163C20}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {3CC269A9-246D-4E4A-B44E-C0C04C163C20}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {3CC269A9-246D-4E4A-B44E-C0C04C163C20}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {4C1C68CE-5B54-4179-B70F-E0173D37EBF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {4C1C68CE-5B54-4179-B70F-E0173D37EBF5}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {4C1C68CE-5B54-4179-B70F-E0173D37EBF5}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {4C1C68CE-5B54-4179-B70F-E0173D37EBF5}.Release|Any CPU.Build.0 = Release|Any CPU 61 | EndGlobalSection 62 | GlobalSection(SolutionProperties) = preSolution 63 | HideSolutionNode = FALSE 64 | EndGlobalSection 65 | GlobalSection(NestedProjects) = preSolution 66 | {A5803FF2-B14A-4601-B23E-2DD23DD8942D} = {27837060-E9DA-46C3-840E-03CCA810AE1C} 67 | EndGlobalSection 68 | GlobalSection(ExtensibilityGlobals) = postSolution 69 | SolutionGuid = {9DC5B3FD-8F9A-44E7-A64F-2CCECA03558E} 70 | EndGlobalSection 71 | EndGlobal 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | bin/ 23 | Bin/ 24 | obj/ 25 | Obj/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | *_i.c 44 | *_p.c 45 | *_i.h 46 | *.ilk 47 | *.meta 48 | *.obj 49 | *.pch 50 | *.pdb 51 | *.pgc 52 | *.pgd 53 | *.rsp 54 | *.sbr 55 | *.tlb 56 | *.tli 57 | *.tlh 58 | *.tmp 59 | *.tmp_proj 60 | *.log 61 | *.vspscc 62 | *.vssscc 63 | .builds 64 | *.pidb 65 | *.svclog 66 | *.scc 67 | 68 | # Chutzpah Test files 69 | _Chutzpah* 70 | 71 | # Visual C++ cache files 72 | ipch/ 73 | *.aps 74 | *.ncb 75 | *.opendb 76 | *.opensdf 77 | *.sdf 78 | *.cachefile 79 | 80 | # Visual Studio profiler 81 | *.psess 82 | *.vsp 83 | *.vspx 84 | *.sap 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding add-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | nCrunchTemp_* 110 | 111 | # MightyMoose 112 | *.mm.* 113 | AutoTest.Net/ 114 | 115 | # Web workbench (sass) 116 | .sass-cache/ 117 | 118 | # Installshield output folder 119 | [Ee]xpress/ 120 | 121 | # DocProject is a documentation generator add-in 122 | DocProject/buildhelp/ 123 | DocProject/Help/*.HxT 124 | DocProject/Help/*.HxC 125 | DocProject/Help/*.hhc 126 | DocProject/Help/*.hhk 127 | DocProject/Help/*.hhp 128 | DocProject/Help/Html2 129 | DocProject/Help/html 130 | 131 | # Click-Once directory 132 | publish/ 133 | 134 | # Publish Web Output 135 | *.[Pp]ublish.xml 136 | *.azurePubxml 137 | # TODO: Comment the next line if you want to checkin your web deploy settings 138 | # but database connection strings (with potential passwords) will be unencrypted 139 | *.pubxml 140 | *.publishproj 141 | 142 | # NuGet Packages 143 | *.nupkg 144 | # The packages folder can be ignored because of Package Restore 145 | **/packages/* 146 | # except build/, which is used as an MSBuild target. 147 | !**/packages/build/ 148 | # Uncomment if necessary however generally it will be regenerated when needed 149 | #!**/packages/repositories.config 150 | 151 | # Microsoft Azure Build Output 152 | csx/ 153 | *.build.csdef 154 | 155 | # Microsoft Azure Emulator 156 | ecf/ 157 | rcf/ 158 | 159 | # Microsoft Azure ApplicationInsights config file 160 | ApplicationInsights.config 161 | 162 | # Windows Store app package directory 163 | AppPackages/ 164 | BundleArtifacts/ 165 | 166 | # Visual Studio cache files 167 | # files ending in .cache can be ignored 168 | *.[Cc]ache 169 | # but keep track of directories ending in .cache 170 | !*.[Cc]ache/ 171 | 172 | # Others 173 | ClientBin/ 174 | ~$* 175 | *~ 176 | *.dbmdl 177 | *.dbproj.schemaview 178 | *.pfx 179 | *.publishsettings 180 | orleans.codegen.cs 181 | 182 | /node_modules 183 | 184 | # RIA/Silverlight projects 185 | Generated_Code/ 186 | 187 | # Backup & report files from converting an old project file 188 | # to a newer Visual Studio version. Backup files are not needed, 189 | # because we have git ;-) 190 | _UpgradeReport_Files/ 191 | Backup*/ 192 | UpgradeLog*.XML 193 | UpgradeLog*.htm 194 | 195 | # SQL Server files 196 | *.mdf 197 | *.ldf 198 | 199 | # Business Intelligence projects 200 | *.rdl.data 201 | *.bim.layout 202 | *.bim_*.settings 203 | 204 | # Microsoft Fakes 205 | FakesAssemblies/ 206 | 207 | # GhostDoc plugin setting file 208 | *.GhostDoc.xml 209 | 210 | # Node.js Tools for Visual Studio 211 | .ntvs_analysis.dat 212 | 213 | # Visual Studio 6 build log 214 | *.plg 215 | 216 | # Visual Studio 6 workspace options file 217 | *.opt 218 | 219 | # Visual Studio LightSwitch build output 220 | **/*.HTMLClient/GeneratedArtifacts 221 | **/*.DesktopClient/GeneratedArtifacts 222 | **/*.DesktopClient/ModelManifest.xml 223 | **/*.Server/GeneratedArtifacts 224 | **/*.Server/ModelManifest.xml 225 | _Pvt_Extensions 226 | 227 | # Paket dependency manager 228 | .paket/paket.exe 229 | 230 | # FAKE - F# Make 231 | .fake/ 232 | -------------------------------------------------------------------------------- /PITCHME.md: -------------------------------------------------------------------------------- 1 | # @css[az](WTF) @color[grey](GitHub) 2 | ## ... don't take me
@color[orange](so seriously) 3 | 4 | --- 5 | 6 | 7 | 8 | @snap[north span-80] 9 | @emoji[wave text-14] @css[text-14](Hi, I'm David Pine) 10 | @snapend 11 | 12 | --- 13 | 14 | 15 | 16 | @snap[south-west] 17 | [@css[twitter](@fa[twitter]) @davidpine7](https://twitter.com/davidpine7) 18 | [@color[#f5f5f5](@fab[github]) github.com/IEvangelist](https://github.com/IEvangelist) 19 | @snapend 20 | 21 | @snap[west clear fit] 22 | 23 | 24 | 25 | @snapend 26 | 27 | @snap[north-west clear fit] 28 | 29 | @snapend 30 | 31 | @snap[south-east] 32 | [davidpine.net @color[red](@fa[globe])](https://davidpine.net/) 33 | [docs.microsoft.com @color[#008AD7](@fab[microsoft])](https://docs.microsoft.com/azure) 34 | @snapend 35 | 36 | @snap[east clear fit] 37 | 38 | @snapend 39 | 40 | @snap[north-east clear fit] 41 | 42 | @snapend 43 | 44 | --- 45 | 46 | # @color[white](I) @color[red](@fa[heart]) @color[white](The) 47 | ### #DeveloperCommunity @css[twitter](@fa[twitter]) 48 | 49 | ---?image=profanity-filter.png&size=contain 50 | --- 51 | 52 | # GitHub @color[grey](@fab[github]) 53 | # @color[green](Webhooks) 54 | 55 | --- 56 | 57 | ```cs zoom-11 58 | public bool IsPayloadSignatureValid( 59 | byte[] bytes, 60 | string receivedSignature) 61 | { 62 | using var hmac = new HMACSHA1( 63 | _options.WebhookSecret.ToByteArray()); 64 | 65 | var hash = hmac.ComputeHash(bytes); 66 | var actualSignature = 67 | $"sha1={hash.ToHexString()}"; 68 | 69 | return IsSignatureValid( 70 | actualSignature, receivedSignature); 71 | } 72 | ``` 73 | 74 | @snap[south span-100] 75 | @[1-3, zoom-12](Method signature, `bool` return, parameters `byte[]` and signature) 76 | @[5-6, zoom-12](C# 8, simplified `using`) 77 | @[8-13, zoom-12](Compute the hash and call `IsSignatureValid`) 78 | @snapend 79 | 80 | --- 81 | 82 | ```cs zoom-11 83 | static bool IsSignatureValid( 84 | string a, 85 | string b) 86 | { 87 | var length = Math.Min(a.Length, b.Length); 88 | var equals = a.Length == b.Length; 89 | for (var i = 0; i < length; ++ i) 90 | { 91 | equals &= a[i] == b[i]; 92 | } 93 | 94 | return equals; 95 | } 96 | ``` 97 | 98 | @snap[south span-100] 99 | @[1-3, zoom-12](A `bool` return, two `string` parameters) 100 | @[5, zoom-12](Determine the shortest `length`) 101 | @[6, zoom-12](Declare and assign `equals`) 102 | @[7-12, zoom-12](Compare each `char` in both `string` instances for equality) 103 | @snapend 104 | 105 | --- 106 | 107 | ```cs zoom-11 108 | public ValueTask DispatchAsync( 109 | string eventName, 110 | string payloadJson) => 111 | eventName switch 112 | { 113 | "issues" => 114 | _issueHandler.HandleIssueAsync( 115 | payloadJson), 116 | "pull_request" => 117 | _pullHandler.HandlePullRequestAsync( 118 | payloadJson), 119 | 120 | _ => new ValueTask(), 121 | }; 122 | ``` 123 | 124 | @snap[south span-100] 125 | @[1-3, zoom-14](A `ValueTask` return, `eventName` and JSON parameters) 126 | @[4-5,14, zoom-14](C# 8, `switch` expressions) 127 | @[4-5,14,6-8, zoom-12](Handle `issues`) 128 | @[4-5,14,9-11, zoom-12](Handle `pull requests`) 129 | @[4-5,14,13, zoom-12](Handle `default` case, "catch-all") 130 | @snapend 131 | 132 | --- 133 | 134 | ## @color[magenta](Issue) Handler 135 | 136 | @ul[no-bullets text-14] 137 | 138 | - @color[cyan](@fa[filter])   Conditionally Apply Filtering 139 | - @color[red](@fa[clock-o])   Add 😕 Reaction 140 | - @color[green](@fa[tag])   Add @color[lightblue]("Profane Content") Label 141 | 142 | @ulend 143 | 144 | --- 145 | 146 | # GitHub @color[grey](@fab[github]) 147 | # @color[red](Labels) 148 | 149 | --- 150 | 151 | # [Labels @fa[external-link-alt]](https://github.com/IEvangelist/GitHub.ProfanityFilter/labels) 152 | 153 | --- 154 | 155 | # GitHub @color[grey](@fab[github]) 156 | # @color[magenta](GraphQL) 157 | 158 | --- 159 | 160 | # [GraphQL @fa[external-link-alt]](https://developer.github.com/v4/explorer/) 161 | 162 | --- 163 | 164 | # @color[magenta](`{`) [@color[cyan](demo)](https://github.com/IEvangelist/GitHub.ProfanityFilter/issues) @color[magenta](`}`) 165 | 166 | --- 167 | 168 | ![Lint Licker](https://www.youtube.com/embed/sf4VC-xNsP8) 169 | 170 | ---?image=assets/slide-one.png&size=contain 171 | 172 | @snap[north-west] 173 | ### 🔎 @color[teal](overview) 174 | @snapend 175 | 176 | --- 177 | 178 | # @color[grey](@fab[github]) Source @color[cyan](@fa[code]) 179 |
180 | ## [bit.ly/ProfanityFilter](https://bit.ly/ProfanityFilter) 181 | 182 | --- 183 | 184 | 185 | 186 | @snap[south-west] 187 | [@css[twitter](@fa[twitter]) @davidpine7](https://twitter.com/davidpine7) 188 | [@color[#f5f5f5](@fab[github]) github.com/IEvangelist](https://github.com/IEvangelist) 189 | @snapend 190 | 191 | @snap[north] 192 | @emoji[clap text-14] @css[text-14](Thank You) 193 | @snapend 194 | 195 | @snap[south-east] 196 | [davidpine.net @color[red](@fa[globe])](https://davidpine.net/) 197 | [docs.microsoft.com @color[#008AD7](@fab[microsoft])](https://docs.microsoft.com/azure) 198 | @snapend -------------------------------------------------------------------------------- /IEvangelist.GitHub.Webhooks/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Handlers/PullRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Enums; 2 | using IEvangelist.GitHub.Services.Extensions; 3 | using IEvangelist.GitHub.Services.Filters; 4 | using IEvangelist.GitHub.Services.GraphQL; 5 | using IEvangelist.GitHub.Services.Hanlders; 6 | using IEvangelist.GitHub.Services.Models; 7 | using IEvangelist.GitHub.Services.Options; 8 | using Microsoft.Azure.CosmosRepository; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.Options; 11 | using Octokit; 12 | using Octokit.GraphQL.Model; 13 | using System; 14 | using System.Threading.Tasks; 15 | 16 | namespace IEvangelist.GitHub.Services.Handlers 17 | { 18 | public class PullRequestHandler : GitHubBaseHandler, IPullRequestHandler 19 | { 20 | readonly GitHubOptions _options; 21 | readonly IRepository _repository; 22 | 23 | public PullRequestHandler( 24 | IGitHubGraphQLClient client, 25 | ILogger logger, 26 | IOptions options, 27 | IProfanityFilter profanityFilter, 28 | IRepository repository) 29 | : base(client, profanityFilter, logger) => 30 | (_options, _repository) = (options.Value, repository); 31 | 32 | public async ValueTask HandlePullRequestAsync(string payloadJson) 33 | { 34 | try 35 | { 36 | var payload = payloadJson.FromJson(); 37 | if (payload is null) 38 | { 39 | _logger.LogWarning("GitHub pull request payload is null."); 40 | return; 41 | } 42 | 43 | _logger.LogInformation($"Handling pull request: {payload.Action}, {payload.PullRequest.NodeId}"); 44 | 45 | switch (payload.Action) 46 | { 47 | case "opened": 48 | await HandlePullRequestAsync(payload); 49 | break; 50 | 51 | case "reopened": 52 | case "edited": 53 | var activity = await _repository.GetAsync(payload.PullRequest.NodeId); 54 | if (activity?.WorkedOn.Subtract(DateTime.Now).TotalSeconds <= 1) 55 | { 56 | _logger.LogInformation($"Just worked on this pull request {payload.PullRequest.NodeId}..."); 57 | } 58 | 59 | await HandlePullRequestAsync(payload, activity); 60 | break; 61 | 62 | case "closed": 63 | await _repository.DeleteAsync(payload.PullRequest.NodeId); 64 | break; 65 | 66 | case "assigned": 67 | case "labeled": 68 | case "locked": 69 | case "ready_for_review": 70 | case "review_request_removed": 71 | case "review_requested": 72 | case "unassigned": 73 | case "unlabeled": 74 | case "unlocked": 75 | break; 76 | } 77 | } 78 | catch (Exception ex) 79 | { 80 | _logger.LogError(ex.Message, ex); 81 | } 82 | } 83 | 84 | async ValueTask HandlePullRequestAsync(PullRequestEventPayload payload, FilterActivity activity = null) 85 | { 86 | try 87 | { 88 | var pullRequest = payload.PullRequest; 89 | var (title, body) = (pullRequest.Title, pullRequest.Body); 90 | var wasJustOpened = activity is null; 91 | if (!wasJustOpened) 92 | { 93 | (title, body) = await _client.GetPullRequestTitleAndBodyAsync(pullRequest.Number); 94 | } 95 | 96 | var filterResult = TryApplyProfanityFilter(title, body); 97 | if (filterResult.IsFiltered) 98 | { 99 | await _client.UpdatePullRequestAsync(pullRequest.Number, new PullRequestUpdate 100 | { 101 | Title = title, 102 | Body = body 103 | }); 104 | 105 | var clientId = Guid.NewGuid().ToString(); 106 | if (wasJustOpened) 107 | { 108 | await _repository.CreateAsync(new FilterActivity 109 | { 110 | Id = pullRequest.NodeId, 111 | WasProfane = true, 112 | ActivityType = ActivityType.Issue, 113 | MutationOrNodeId = clientId, 114 | WorkedOn = DateTime.Now, 115 | OriginalTitleText = title, 116 | OriginalBodyText = body, 117 | ModifiedTitleText = filterResult.Title, 118 | ModifiedBodyText = filterResult.Body 119 | }); 120 | } 121 | else 122 | { 123 | activity.WasProfane = true; 124 | activity.WorkedOn = DateTime.Now; 125 | await _repository.UpdateAsync(activity); 126 | } 127 | 128 | await _client.AddReactionAsync(pullRequest.NodeId, ReactionContent.Confused, clientId); 129 | await _client.AddLabelAsync(pullRequest.NodeId, new[] { _options.ProfaneLabelId }, clientId); 130 | } 131 | } 132 | catch (Exception ex) 133 | { 134 | _logger.LogError($"Error while attempting to filter issue: {ex.Message}\n{ex.StackTrace}", ex); 135 | } 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Handlers/IssueHandler.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Enums; 2 | using IEvangelist.GitHub.Services.Extensions; 3 | using IEvangelist.GitHub.Services.Filters; 4 | using IEvangelist.GitHub.Services.GraphQL; 5 | using IEvangelist.GitHub.Services.Hanlders; 6 | using IEvangelist.GitHub.Services.Models; 7 | using IEvangelist.GitHub.Services.Options; 8 | using Microsoft.Azure.CosmosRepository; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.Options; 11 | using Octokit; 12 | using Octokit.GraphQL.Model; 13 | using System; 14 | using System.Threading.Tasks; 15 | 16 | namespace IEvangelist.GitHub.Services.Handlers 17 | { 18 | public class IssueHandler 19 | : GitHubBaseHandler, IIssueHandler 20 | { 21 | readonly GitHubOptions _options; 22 | readonly IRepository _repository; 23 | 24 | public IssueHandler( 25 | IGitHubGraphQLClient client, 26 | ILogger logger, 27 | IOptions options, 28 | IProfanityFilter profanityFilter, 29 | IRepository repository) 30 | : base(client, profanityFilter, logger) => 31 | (_options, _repository) = (options.Value, repository); 32 | 33 | public async ValueTask HandleIssueAsync(string payloadJson) 34 | { 35 | try 36 | { 37 | var payload = payloadJson.FromJson(); 38 | if (payload is null) 39 | { 40 | _logger.LogWarning("GitHub issue payload is null."); 41 | return; 42 | } 43 | 44 | _logger.LogInformation( 45 | $"Handling issue: {payload.Action}, {payload.Issue.NodeId}"); 46 | 47 | switch (payload.Action) 48 | { 49 | case "opened": 50 | await HandleIssueAsync(payload); 51 | break; 52 | 53 | case "reopened": 54 | case "edited": 55 | var activity = 56 | await _repository.GetAsync(payload.Issue.NodeId); 57 | if (activity?.WorkedOn 58 | .Subtract(DateTime.Now) 59 | .TotalSeconds <= 1) 60 | { 61 | _logger.LogInformation( 62 | $"Just worked on this issue {payload.Issue.NodeId}..."); 63 | } 64 | 65 | await HandleIssueAsync(payload, activity); 66 | break; 67 | 68 | case "closed": 69 | case "deleted": 70 | await _repository.DeleteAsync(payload.Issue.NodeId); 71 | break; 72 | 73 | case "assigned": 74 | case "demilestoned": 75 | case "labeled": 76 | case "locked": 77 | case "milestoned": 78 | case "pinned": 79 | case "transferred": 80 | case "unassigned": 81 | case "unlabeled": 82 | case "unlocked": 83 | case "unpinned": 84 | break; 85 | } 86 | } 87 | catch (Exception ex) 88 | { 89 | _logger.LogError($"{ex.Message}\n{ex.StackTrace}", ex); 90 | } 91 | } 92 | 93 | async ValueTask HandleIssueAsync( 94 | IssueEventPayload payload, 95 | FilterActivity activity = null) 96 | { 97 | try 98 | { 99 | var issue = payload.Issue; 100 | var (title, body) = (issue.Title, issue.Body); 101 | var wasJustOpened = activity is null; 102 | if (!wasJustOpened) 103 | { 104 | (title, body) = 105 | await _client.GetIssueTitleAndBodyAsync(issue.Number); 106 | } 107 | 108 | var filterResult = TryApplyProfanityFilter(title, body); 109 | if (filterResult.IsFiltered) 110 | { 111 | var updateIssue = issue.ToUpdate(); 112 | updateIssue.Title = filterResult.Title; 113 | updateIssue.Body = filterResult.Body; 114 | await _client.UpdateIssueAsync(issue.Number, updateIssue); 115 | 116 | var clientId = Guid.NewGuid().ToString(); 117 | if (wasJustOpened) 118 | { 119 | await _repository.CreateAsync(new FilterActivity 120 | { 121 | Id = issue.NodeId, 122 | WasProfane = true, 123 | ActivityType = ActivityType.Issue, 124 | MutationOrNodeId = clientId, 125 | WorkedOn = DateTime.Now, 126 | OriginalTitleText = title, 127 | OriginalBodyText = body, 128 | ModifiedTitleText = filterResult.Title, 129 | ModifiedBodyText = filterResult.Body 130 | }); 131 | } 132 | else 133 | { 134 | activity.WasProfane = true; 135 | activity.WorkedOn = DateTime.Now; 136 | await _repository.UpdateAsync(activity); 137 | } 138 | 139 | await _client.AddReactionAsync( 140 | issue.NodeId, 141 | ReactionContent.Confused, 142 | clientId); 143 | await _client.AddLabelAsync( 144 | issue.NodeId, 145 | new[] { _options.ProfaneLabelId }, 146 | clientId); 147 | } 148 | } 149 | catch (Exception ex) 150 | { 151 | _logger.LogError( 152 | $"Error while attempting to filter issue: {ex.Message}\n{ex.StackTrace}", ex); 153 | } 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/GraphQL/GitHubGraphQLClient.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Extensions; 2 | using IEvangelist.GitHub.Services.Options; 3 | using Microsoft.Extensions.Options; 4 | using Octokit; 5 | using Octokit.GraphQL; 6 | using Octokit.GraphQL.Model; 7 | using System; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | using IGraphQLConnection = Octokit.GraphQL.IConnection; 12 | using GraphQLConnection = Octokit.GraphQL.Connection; 13 | using GraphQLProductHeaderValue = Octokit.GraphQL.ProductHeaderValue; 14 | 15 | using Connection = Octokit.Connection; 16 | using ProductHeaderValue = Octokit.ProductHeaderValue; 17 | 18 | namespace IEvangelist.GitHub.Services.GraphQL 19 | { 20 | public class GitHubGraphQLClient : IGitHubGraphQLClient 21 | { 22 | const string ProductID = "GitHub.ProfanityFilter"; 23 | const string ProductVersion = "1.0"; 24 | 25 | readonly IGraphQLConnection _connection; 26 | readonly IGitHubClient _client; 27 | readonly GitHubOptions _config; 28 | 29 | public GitHubGraphQLClient(IOptions config) 30 | { 31 | _config = config?.Value ?? throw new ArgumentNullException(nameof(config)); 32 | _connection = new GraphQLConnection(new GraphQLProductHeaderValue(ProductID, ProductVersion), _config.ApiToken); 33 | _client = new GitHubClient(new Connection(new ProductHeaderValue(ProductID, ProductVersion)) 34 | { 35 | Credentials = new Credentials(_config.ApiToken) 36 | }); 37 | } 38 | 39 | public async ValueTask AddLabelAsync(string issueOrPullRequestId, string[] labelIds, string clientId) 40 | { 41 | var mutation = 42 | new Mutation() 43 | .AddLabelsToLabelable(new AddLabelsToLabelableInput 44 | { 45 | ClientMutationId = clientId, 46 | LabelableId = issueOrPullRequestId.ToGitHubId(), 47 | LabelIds = labelIds.Select(id => id.ToGitHubId()).ToArray() 48 | }) 49 | .Select(payload => new 50 | { 51 | payload.ClientMutationId 52 | }) 53 | .Compile(); 54 | 55 | var result = await _connection.Run(mutation); 56 | return result.ClientMutationId; 57 | } 58 | 59 | public async ValueTask AddReactionAsync(string issueOrPullRequestId, ReactionContent reaction, string clientId) 60 | { 61 | var mutation = 62 | new Mutation() 63 | .AddReaction(new AddReactionInput 64 | { 65 | ClientMutationId = clientId, 66 | SubjectId = issueOrPullRequestId.ToGitHubId(), 67 | Content = reaction 68 | }) 69 | .Select(payload => new 70 | { 71 | payload.ClientMutationId 72 | }) 73 | .Compile(); 74 | 75 | var result = await _connection.Run(mutation); 76 | return result.ClientMutationId; 77 | } 78 | 79 | public async ValueTask RemoveLabelAsync(string issueOrPullRequestId, string clientId) 80 | { 81 | var mutation = 82 | new Mutation() 83 | .ClearLabelsFromLabelable(new ClearLabelsFromLabelableInput 84 | { 85 | ClientMutationId = clientId, 86 | LabelableId = issueOrPullRequestId.ToGitHubId() 87 | }) 88 | .Select(payload => new 89 | { 90 | payload.ClientMutationId 91 | }) 92 | .Compile(); 93 | 94 | var result = await _connection.Run(mutation); 95 | return result.ClientMutationId; 96 | } 97 | 98 | public async ValueTask RemoveReactionAsync(string issueOrPullRequestId, ReactionContent reaction, string clientId) 99 | { 100 | var mutation = 101 | new Mutation() 102 | .RemoveReaction(new RemoveReactionInput 103 | { 104 | ClientMutationId = clientId, 105 | SubjectId = issueOrPullRequestId.ToGitHubId(), 106 | Content = reaction 107 | }) 108 | .Select(payload => new 109 | { 110 | payload.ClientMutationId 111 | }) 112 | .Compile(); 113 | 114 | var result = await _connection.Run(mutation); 115 | return result.ClientMutationId; 116 | } 117 | 118 | public async ValueTask UpdateIssueAsync(UpdateIssueInput input) 119 | { 120 | var mutation = 121 | new Mutation() 122 | .UpdateIssue(input) 123 | .Select(payload => new 124 | { 125 | payload.ClientMutationId 126 | }) 127 | .Compile(); 128 | 129 | var result = await _connection.Run(mutation); 130 | return result.ClientMutationId; 131 | } 132 | 133 | public async ValueTask UpdateIssueAsync(int number, IssueUpdate input) 134 | { 135 | var result = await _client.Issue.Update(_config.Owner, _config.Repo, number, input); 136 | return result.NodeId; 137 | } 138 | 139 | public async ValueTask UpdatePullRequestAsync(UpdatePullRequestInput input) 140 | { 141 | var mutation = 142 | new Mutation() 143 | .UpdatePullRequest(input) 144 | .Select(payload => new 145 | { 146 | payload.ClientMutationId 147 | }) 148 | .Compile(); 149 | 150 | var result = await _connection.Run(mutation); 151 | return result.ClientMutationId; 152 | } 153 | 154 | public async ValueTask UpdatePullRequestAsync(int number, PullRequestUpdate input) 155 | { 156 | var result = await _client.PullRequest.Update(_config.Owner, _config.Repo, number, input); 157 | return result.NodeId; 158 | } 159 | 160 | public async ValueTask<(string title, string body)> GetIssueTitleAndBodyAsync(int issueNumber) 161 | { 162 | var query = 163 | new Query() 164 | .Repository(_config.Repo, _config.Owner) 165 | .Issue(issueNumber) 166 | .Select(issue => new 167 | { 168 | issue.Title, 169 | issue.Body 170 | }) 171 | .Compile(); 172 | 173 | var result = await _connection.Run(query); 174 | return (result.Title, result.Body); 175 | } 176 | 177 | public async ValueTask<(string title, string body)> GetPullRequestTitleAndBodyAsync(int pullRequestNumber) 178 | { 179 | var query = 180 | new Query() 181 | .Repository(_config.Repo, _config.Owner) 182 | .PullRequest(pullRequestNumber) 183 | .Select(pullRequest => new 184 | { 185 | pullRequest.Title, 186 | pullRequest.Body 187 | }) 188 | .Compile(); 189 | 190 | var result = await _connection.Run(query); 191 | return (result.Title, result.Body); 192 | } 193 | } 194 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Sandbox/Program.cs: -------------------------------------------------------------------------------- 1 | using IEvangelist.GitHub.Services.Extensions; 2 | using IEvangelist.GitHub.Services.Handlers; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace IEvangelist.GitHub.Sandbox 9 | { 10 | class Program 11 | { 12 | static IServiceProvider _serviceProvider; 13 | 14 | static async Task Main(string[] args) 15 | { 16 | try 17 | { 18 | RegisterServices(); 19 | 20 | var handler = _serviceProvider.GetService(); 21 | await handler.HandleIssueAsync(null); 22 | } 23 | catch (Exception ex) 24 | { 25 | Console.WriteLine(ex.Message); 26 | } 27 | finally 28 | { 29 | DisposeServices(); 30 | } 31 | } 32 | 33 | static void RegisterServices() 34 | { 35 | var configBuilder = new ConfigurationBuilder().AddEnvironmentVariables(); 36 | var configuration = configBuilder.Build(); 37 | var services = new ServiceCollection().AddGitHubServices(configuration); 38 | 39 | _serviceProvider = services.BuildServiceProvider(); 40 | } 41 | 42 | static void DisposeServices() => (_serviceProvider as IDisposable)?.Dispose(); 43 | 44 | const string IssueJsonPayload = @"{""action"":""edited"",""issue"":{""url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/issues/1"",""repository_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter"",""labels_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/issues/1/labels{/name}"",""comments_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/issues/1/comments"",""events_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/issues/1/events"",""html_url"":""https://github.com/IEvangelist/GitHub.ProfanityFilter/issues/1"",""id"":487247937,""node_id"":""MDU6SXNzdWU0ODcyNDc5Mzc="",""number"":1,""title"":""Fuck this project"",""user"":{""login"":""IEvangelist"",""id"":7679720,""node_id"":""MDQ6VXNlcjc2Nzk3MjA="",""avatar_url"":""https://avatars0.githubusercontent.com/u/7679720?v=4"",""gravatar_id"":"""",""url"":""https://api.github.com/users/IEvangelist"",""html_url"":""https://github.com/IEvangelist"",""followers_url"":""https://api.github.com/users/IEvangelist/followers"",""following_url"":""https://api.github.com/users/IEvangelist/following{/other_user}"",""gists_url"":""https://api.github.com/users/IEvangelist/gists{/gist_id}"",""starred_url"":""https://api.github.com/users/IEvangelist/starred{/owner}{/repo}"",""subscriptions_url"":""https://api.github.com/users/IEvangelist/subscriptions"",""organizations_url"":""https://api.github.com/users/IEvangelist/orgs"",""repos_url"":""https://api.github.com/users/IEvangelist/repos"",""events_url"":""https://api.github.com/users/IEvangelist/events{/privacy}"",""received_events_url"":""https://api.github.com/users/IEvangelist/received_events"",""type"":""User"",""site_admin"":false},""labels"":[],""state"":""open"",""locked"":false,""assignee"":null,""assignees"":[],""milestone"":null,""comments"":0,""created_at"":""2019-08-30T01:58:22Z"",""updated_at"":""2019-08-30T13:08:51Z"",""closed_at"":null,""author_association"":""OWNER"",""body"":""I think it's completely bullshit - this must not work? It's so fucked up... fuck this shit 👎 I can't believe it's not working. God damnit""},""changes"":{""body"":{""from"":""I think it's completely bullshit - this must not work? It's so fucked up... fuck this shit 👎 I can't believe it's not working.""}},""repository"":{""id"":205049285,""node_id"":""MDEwOlJlcG9zaXRvcnkyMDUwNDkyODU="",""name"":""GitHub.ProfanityFilter"",""full_name"":""IEvangelist/GitHub.ProfanityFilter"",""private"":false,""owner"":{""login"":""IEvangelist"",""id"":7679720,""node_id"":""MDQ6VXNlcjc2Nzk3MjA="",""avatar_url"":""https://avatars0.githubusercontent.com/u/7679720?v=4"",""gravatar_id"":"""",""url"":""https://api.github.com/users/IEvangelist"",""html_url"":""https://github.com/IEvangelist"",""followers_url"":""https://api.github.com/users/IEvangelist/followers"",""following_url"":""https://api.github.com/users/IEvangelist/following{/other_user}"",""gists_url"":""https://api.github.com/users/IEvangelist/gists{/gist_id}"",""starred_url"":""https://api.github.com/users/IEvangelist/starred{/owner}{/repo}"",""subscriptions_url"":""https://api.github.com/users/IEvangelist/subscriptions"",""organizations_url"":""https://api.github.com/users/IEvangelist/orgs"",""repos_url"":""https://api.github.com/users/IEvangelist/repos"",""events_url"":""https://api.github.com/users/IEvangelist/events{/privacy}"",""received_events_url"":""https://api.github.com/users/IEvangelist/received_events"",""type"":""User"",""site_admin"":false},""html_url"":""https://github.com/IEvangelist/GitHub.ProfanityFilter"",""description"":""An Azure function to handle a GitHub webhook, which will take action on issues/pull requests that contain profanity."",""fork"":false,""url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter"",""forks_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/forks"",""keys_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/keys{/key_id}"",""collaborators_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/collaborators{/collaborator}"",""teams_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/teams"",""hooks_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/hooks"",""issue_events_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/issues/events{/number}"",""events_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/events"",""assignees_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/assignees{/user}"",""branches_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/branches{/branch}"",""tags_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/tags"",""blobs_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/git/blobs{/sha}"",""git_tags_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/git/tags{/sha}"",""git_refs_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/git/refs{/sha}"",""trees_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/git/trees{/sha}"",""statuses_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/statuses/{sha}"",""languages_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/languages"",""stargazers_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/stargazers"",""contributors_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/contributors"",""subscribers_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/subscribers"",""subscription_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/subscription"",""commits_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/commits{/sha}"",""git_commits_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/git/commits{/sha}"",""comments_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/comments{/number}"",""issue_comment_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/issues/comments{/number}"",""contents_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/contents/{+path}"",""compare_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/compare/{base}...{head}"",""merges_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/merges"",""archive_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/{archive_format}{/ref}"",""downloads_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/downloads"",""issues_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/issues{/number}"",""pulls_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/pulls{/number}"",""milestones_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/milestones{/number}"",""notifications_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/notifications{?since,all,participating}"",""labels_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/labels{/name}"",""releases_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/releases{/id}"",""deployments_url"":""https://api.github.com/repos/IEvangelist/GitHub.ProfanityFilter/deployments"",""created_at"":""2019-08-29T00:43:38Z"",""updated_at"":""2019-08-29T14:01:35Z"",""pushed_at"":""2019-08-29T01:05:12Z"",""git_url"":""git://github.com/IEvangelist/GitHub.ProfanityFilter.git"",""ssh_url"":""git@github.com:IEvangelist/GitHub.ProfanityFilter.git"",""clone_url"":""https://github.com/IEvangelist/GitHub.ProfanityFilter.git"",""svn_url"":""https://github.com/IEvangelist/GitHub.ProfanityFilter"",""homepage"":"""",""size"":18,""stargazers_count"":0,""watchers_count"":0,""language"":""C#"",""has_issues"":true,""has_projects"":true,""has_downloads"":true,""has_wiki"":true,""has_pages"":false,""forks_count"":0,""mirror_url"":null,""archived"":false,""disabled"":false,""open_issues_count"":1,""license"":null,""forks"":0,""open_issues"":1,""watchers"":0,""default_branch"":""master""},""sender"":{""login"":""IEvangelist"",""id"":7679720,""node_id"":""MDQ6VXNlcjc2Nzk3MjA="",""avatar_url"":""https://avatars0.githubusercontent.com/u/7679720?v=4"",""gravatar_id"":"""",""url"":""https://api.github.com/users/IEvangelist"",""html_url"":""https://github.com/IEvangelist"",""followers_url"":""https://api.github.com/users/IEvangelist/followers"",""following_url"":""https://api.github.com/users/IEvangelist/following{/other_user}"",""gists_url"":""https://api.github.com/users/IEvangelist/gists{/gist_id}"",""starred_url"":""https://api.github.com/users/IEvangelist/starred{/owner}{/repo}"",""subscriptions_url"":""https://api.github.com/users/IEvangelist/subscriptions"",""organizations_url"":""https://api.github.com/users/IEvangelist/orgs"",""repos_url"":""https://api.github.com/users/IEvangelist/repos"",""events_url"":""https://api.github.com/users/IEvangelist/events{/privacy}"",""received_events_url"":""https://api.github.com/users/IEvangelist/received_events"",""type"":""User"",""site_admin"":false}}"; 45 | } 46 | } -------------------------------------------------------------------------------- /IEvangelist.GitHub.Services/Filters/InappropriateLanguage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace IEvangelist.GitHub.Services.Filters 7 | { 8 | static class InappropriateLanguage 9 | { 10 | #region Borrowed from several locations: 11 | 12 | // https://raw.githubusercontent.com/web-mech/badwords/master/lib/lang.json 13 | // https://www.freewebheaders.com/full-list-of-bad-words-banned-by-google/ 14 | 15 | static string[] WordsAndPhrases { get; } = new[] 16 | { 17 | "$h!t", 18 | "*damn", 19 | "*dyke", 20 | "*fuck*", 21 | "*shit*", 22 | "@$$", 23 | "2 girls 1 cup", 24 | "2g1c", 25 | "4r5e", 26 | "5h1t", 27 | "5hit", 28 | "a$$", 29 | "a$$hole", 30 | "a_s_s", 31 | "a2m", 32 | "a54", 33 | "a55", 34 | "a55hole", 35 | "acrotomophilia", 36 | "aeolus", 37 | "ahole", 38 | "alabama hot pocket", 39 | "alaskan pipeline", 40 | "amcik", 41 | "anal", 42 | "anal impaler", 43 | "anal leakage", 44 | "analprobe", 45 | "andskota", 46 | "anilingus", 47 | "anus", 48 | "apeshit", 49 | "ar5e", 50 | "areola", 51 | "areole", 52 | "arian", 53 | "arrse", 54 | "arschloch", 55 | "arse", 56 | "arse*", 57 | "arsehole", 58 | "aryan", 59 | "ash0le", 60 | "ash0les", 61 | "asholes", 62 | "ass", 63 | "ass fuck", 64 | "ass hole", 65 | "Ass Monkey", 66 | "assbag", 67 | "assbandit", 68 | "assbang", 69 | "assbanged", 70 | "assbanger", 71 | "assbangs", 72 | "assbite", 73 | "assclown", 74 | "asscock", 75 | "asscracker", 76 | "asses", 77 | "assface", 78 | "assfaces", 79 | "assfuck", 80 | "assfucker", 81 | "ass-fucker", 82 | "assfukka", 83 | "assgoblin", 84 | "assh0le", 85 | "assh0lez", 86 | "asshat", 87 | "ass-hat", 88 | "asshead", 89 | "assho1e", 90 | "asshole", 91 | "assholes", 92 | "assholz", 93 | "asshopper", 94 | "ass-jabber", 95 | "assjacker", 96 | "asslick", 97 | "asslicker", 98 | "assmaster", 99 | "assmonkey", 100 | "assmucus", 101 | "assmunch", 102 | "assmuncher", 103 | "assnigger", 104 | "asspirate", 105 | "ass-pirate", 106 | "assrammer", 107 | "assshit", 108 | "assshole", 109 | "asssucker", 110 | "asswad", 111 | "asswhole", 112 | "asswipe", 113 | "asswipes", 114 | "auto erotic", 115 | "autoerotic", 116 | "axwound", 117 | "ayir", 118 | "azazel", 119 | "azz", 120 | "azzhole", 121 | "b!+ch", 122 | "b!tch", 123 | "b00b*", 124 | "b00bs", 125 | "b17ch", 126 | "b1tch", 127 | "babeland", 128 | "baby batter", 129 | "baby juice", 130 | "ball gag", 131 | "ball gravy", 132 | "ball kicking", 133 | "ball licking", 134 | "ball sack", 135 | "ball sucking", 136 | "ballbag", 137 | "balls", 138 | "ballsack", 139 | "bampot", 140 | "bang (one's) box", 141 | "bangbros", 142 | "bareback", 143 | "barely legal", 144 | "barenaked", 145 | "barf", 146 | "bassterds", 147 | "bastard", 148 | "bastardo", 149 | "bastards", 150 | "bastardz", 151 | "basterds", 152 | "basterdz", 153 | "bastinado", 154 | "batty boy", 155 | "bawdy", 156 | "bbw", 157 | "bdsm", 158 | "beaner", 159 | "beaners", 160 | "beardedclam", 161 | "beastial", 162 | "beastiality", 163 | "beatch", 164 | "beaver", 165 | "beaver cleaver", 166 | "beaver lips", 167 | "beef curtain", 168 | "beef curtains", 169 | "beeyotch", 170 | "bellend", 171 | "bender", 172 | "beotch", 173 | "bescumber", 174 | "bestial", 175 | "bestiality", 176 | "bi+ch", 177 | "bi7ch", 178 | "Biatch", 179 | "big black", 180 | "big breasts", 181 | "big knockers", 182 | "big tits", 183 | "bigtits", 184 | "bimbo", 185 | "bimbos", 186 | "bint", 187 | "birdlock", 188 | "bitch", 189 | "bitch tit", 190 | "bitch*", 191 | "bitchass", 192 | "bitched", 193 | "bitcher", 194 | "bitchers", 195 | "bitches", 196 | "bitchin", 197 | "bitching", 198 | "bitchtits", 199 | "bitchy", 200 | "black cock", 201 | "blonde action", 202 | "blonde on blonde action", 203 | "bloodclaat", 204 | "bloody", 205 | "bloody hell", 206 | "blow job", 207 | "blow me", 208 | "blow mud", 209 | "blow your load", 210 | "blowjob", 211 | "blowjobs", 212 | "blue waffle", 213 | "blumpkin", 214 | "bod", 215 | "bodily", 216 | "boffing", 217 | "boink", 218 | "boiolas", 219 | "bollock", 220 | "bollock*", 221 | "bollocks", 222 | "bollok", 223 | "bollox", 224 | "bondage", 225 | "boned", 226 | "boner", 227 | "boners", 228 | "bong", 229 | "boob", 230 | "boobies", 231 | "boobs", 232 | "booby", 233 | "booger", 234 | "bookie", 235 | "boong", 236 | "booobs", 237 | "boooobs", 238 | "booooobs", 239 | "booooooobs", 240 | "bootee", 241 | "bootie", 242 | "booty", 243 | "booty call", 244 | "booze", 245 | "boozer", 246 | "boozy", 247 | "bosom", 248 | "bosomy", 249 | "breasts", 250 | "Breeder", 251 | "brotherfucker", 252 | "brown showers", 253 | "brunette action", 254 | "buceta", 255 | "bugger", 256 | "bukkake", 257 | "bull shit", 258 | "bulldyke", 259 | "bullet vibe", 260 | "bullshit", 261 | "bullshits", 262 | "bullshitted", 263 | "bullturds", 264 | "bum", 265 | "bum boy", 266 | "bumblefuck", 267 | "bumclat", 268 | "bummer", 269 | "buncombe", 270 | "bung", 271 | "bung hole", 272 | "bunghole", 273 | "bunny fucker", 274 | "bust a load", 275 | "busty", 276 | "butt", 277 | "butt fuck", 278 | "butt plug", 279 | "buttcheeks", 280 | "buttfuck", 281 | "buttfucka", 282 | "buttfucker", 283 | "butthole", 284 | "buttmuch", 285 | "buttmunch", 286 | "butt-pirate", 287 | "buttplug", 288 | "buttwipe", 289 | "c.0.c.k", 290 | "c.o.c.k.", 291 | "c.u.n.t", 292 | "c0ck", 293 | "c-0-c-k", 294 | "c0cks", 295 | "c0cksucker", 296 | "c0k", 297 | "cabron", 298 | "caca", 299 | "cacafuego", 300 | "cahone", 301 | "camel toe", 302 | "cameltoe", 303 | "camgirl", 304 | "camslut", 305 | "camwhore", 306 | "carpet muncher", 307 | "carpetmuncher", 308 | "cawk", 309 | "cawks", 310 | "cazzo", 311 | "cervix", 312 | "chesticle", 313 | "chi-chi man", 314 | "chick with a dick", 315 | "child-fucker", 316 | "chinc", 317 | "chincs", 318 | "chink", 319 | "chinky", 320 | "choad", 321 | "choade", 322 | "choc ice", 323 | "chocolate rosebuds", 324 | "chode", 325 | "chodes", 326 | "chota bags", 327 | "chraa", 328 | "chuj", 329 | "cipa", 330 | "circlejerk", 331 | "cl1t", 332 | "cleveland steamer", 333 | "climax", 334 | "clit", 335 | "clit licker", 336 | "clitface", 337 | "clitfuck", 338 | "clitoris", 339 | "clitorus", 340 | "clits", 341 | "clitty", 342 | "clitty litter", 343 | "clover clamps", 344 | "clunge", 345 | "clusterfuck", 346 | "cnts", 347 | "cntz", 348 | "cnut", 349 | "cocain", 350 | "cocaine", 351 | "coccydynia", 352 | "cock", 353 | "c-o-c-k", 354 | "cock pocket", 355 | "cock snot", 356 | "cock sucker", 357 | "Cock*", 358 | "cockass", 359 | "cockbite", 360 | "cockblock", 361 | "cockburger", 362 | "cockeye", 363 | "cockface", 364 | "cockfucker", 365 | "cockhead", 366 | "cock-head", 367 | "cockholster", 368 | "cockjockey", 369 | "cockknocker", 370 | "cockknoker", 371 | "Cocklump", 372 | "cockmaster", 373 | "cockmongler", 374 | "cockmongruel", 375 | "cockmonkey", 376 | "cockmunch", 377 | "cockmuncher", 378 | "cocknose", 379 | "cocknugget", 380 | "cocks", 381 | "cockshit", 382 | "cocksmith", 383 | "cocksmoke", 384 | "cocksmoker", 385 | "cocksniffer", 386 | "cocksuck", 387 | "cocksucked", 388 | "cocksucker", 389 | "cock-sucker", 390 | "cocksuckers", 391 | "cocksucking", 392 | "cocksucks", 393 | "cocksuka", 394 | "cocksukka", 395 | "cockwaffle", 396 | "coffin dodger", 397 | "coital", 398 | "cok", 399 | "cokmuncher", 400 | "coksucka", 401 | "commie", 402 | "condom", 403 | "coochie", 404 | "coochy", 405 | "coon", 406 | "coonnass", 407 | "coons", 408 | "cooter", 409 | "cop some wood", 410 | "coprolagnia", 411 | "coprophilia", 412 | "corksucker", 413 | "cornhole", 414 | "corp whore", 415 | "corpulent", 416 | "cox", 417 | "crabs", 418 | "crack", 419 | "cracker", 420 | "crackwhore", 421 | "crap", 422 | "crappy", 423 | "creampie", 424 | "cretin", 425 | "crikey", 426 | "cripple", 427 | "crotte", 428 | "cum", 429 | "cum chugger", 430 | "cum dumpster", 431 | "cum freak", 432 | "cum guzzler", 433 | "cumbubble", 434 | "cumdump", 435 | "cumdumpster", 436 | "cumguzzler", 437 | "cumjockey", 438 | "cummer", 439 | "cummin", 440 | "cumming", 441 | "cums", 442 | "cumshot", 443 | "cumshots", 444 | "cumslut", 445 | "cumstain", 446 | "cumtart", 447 | "cunilingus", 448 | "cunillingus", 449 | "cunnie", 450 | "cunnilingus", 451 | "cunny", 452 | "cunt", 453 | "c-u-n-t", 454 | "cunt hair", 455 | "cunt*", 456 | "cuntass", 457 | "cuntbag", 458 | "cuntface", 459 | "cunthole", 460 | "cunthunter", 461 | "cuntlick", 462 | "cuntlicker", 463 | "cuntlicking", 464 | "cuntrag", 465 | "cunts", 466 | "cuntsicle", 467 | "cuntslut", 468 | "cunt-struck", 469 | "cuntz", 470 | "cus", 471 | "cut rope", 472 | "cyalis", 473 | "cyberfuc", 474 | "cyberfuck", 475 | "cyberfucked", 476 | "cyberfucker", 477 | "cyberfuckers", 478 | "cyberfucking", 479 | "d!ck", 480 | "d!cks", 481 | "d0ng", 482 | "d0uch3", 483 | "d0uche", 484 | "d1ck", 485 | "d1ld0", 486 | "d1ldo", 487 | "d4mn", 488 | "dago", 489 | "dagos", 490 | "dammit", 491 | "damn", 492 | "damned", 493 | "damnit", 494 | "darkie", 495 | "darn", 496 | "date rape", 497 | "daterape", 498 | "dawgie-style", 499 | "daygo", 500 | "deep throat", 501 | "deepthroat", 502 | "deggo", 503 | "dego", 504 | "dendrophilia", 505 | "dick", 506 | "dick head", 507 | "dick hole", 508 | "dick shy", 509 | "dick*", 510 | "dickbag", 511 | "dickbeaters", 512 | "dickdipper", 513 | "dickface", 514 | "dickflipper", 515 | "dickfuck", 516 | "dickfucker", 517 | "dickhead", 518 | "dickheads", 519 | "dickhole", 520 | "dickish", 521 | "dick-ish", 522 | "dickjuice", 523 | "dickmilk", 524 | "dickmonger", 525 | "dickripper", 526 | "dicks", 527 | "dicksipper", 528 | "dickslap", 529 | "dick-sneeze", 530 | "dicksucker", 531 | "dicksucking", 532 | "dicktickler", 533 | "dickwad", 534 | "dickweasel", 535 | "dickweed", 536 | "dickwhipper", 537 | "dickwod", 538 | "dickzipper", 539 | "diddle", 540 | "dike", 541 | "dike*", 542 | "dild0", 543 | "dild0s", 544 | "dildo", 545 | "dildos", 546 | "diligaf", 547 | "dilld0", 548 | "dilld0s", 549 | "dillweed", 550 | "dimwit", 551 | "dingle", 552 | "dingleberries", 553 | "dingleberry", 554 | "dink", 555 | "dinks", 556 | "dipship", 557 | "dipshit", 558 | "dirsa", 559 | "dirty", 560 | "dirty pillows", 561 | "dirty sanchez", 562 | "div", 563 | "dlck", 564 | "dog style", 565 | "dog-fucker", 566 | "doggie style", 567 | "doggiestyle", 568 | "doggie-style", 569 | "doggin", 570 | "dogging", 571 | "doggy style", 572 | "doggystyle", 573 | "doggy-style", 574 | "dolcett", 575 | "domination", 576 | "dominatricks", 577 | "dominatrics", 578 | "dominatrix", 579 | "dommes", 580 | "dong", 581 | "donkey punch", 582 | "donkeypunch", 583 | "donkeyribber", 584 | "doochbag", 585 | "doofus", 586 | "dookie", 587 | "doosh", 588 | "dopey", 589 | "double dong", 590 | "double penetration", 591 | "Doublelift", 592 | "douch3", 593 | "douche", 594 | "douchebag", 595 | "douchebags", 596 | "douche-fag", 597 | "douchewaffle", 598 | "douchey", 599 | "dp action", 600 | "drunk", 601 | "dry hump", 602 | "duche", 603 | "dumass", 604 | "dumb ass", 605 | "dumbass", 606 | "dumbasses", 607 | "Dumbcunt", 608 | "dumbfuck", 609 | "dumbshit", 610 | "dummy", 611 | "dumshit", 612 | "dupa", 613 | "dvda", 614 | "dyke", 615 | "dykes", 616 | "dziwka", 617 | "eat a dick", 618 | "eat hair pie", 619 | "eat my ass", 620 | "ecchi", 621 | "ejackulate", 622 | "ejaculate", 623 | "ejaculated", 624 | "ejaculates", 625 | "ejaculating", 626 | "ejaculatings", 627 | "ejaculation", 628 | "ejakulate", 629 | "Ekrem*", 630 | "Ekto", 631 | "enculer", 632 | "enema", 633 | "erect", 634 | "erection", 635 | "erotic", 636 | "erotism", 637 | "escort", 638 | "essohbee", 639 | "eunuch", 640 | "extacy", 641 | "extasy", 642 | "f u c k", 643 | "f u c k e r", 644 | "f.u.c.k", 645 | "f_u_c_k", 646 | "f4nny", 647 | "facial", 648 | "fack", 649 | "faen", 650 | "fag", 651 | "fag*", 652 | "fag1t", 653 | "fagbag", 654 | "faget", 655 | "fagfucker", 656 | "fagg", 657 | "fagg0t", 658 | "fagg1t", 659 | "fagged", 660 | "fagging", 661 | "faggit", 662 | "faggitt", 663 | "faggot", 664 | "faggotcock", 665 | "faggots", 666 | "faggs", 667 | "fagit", 668 | "fagot", 669 | "fagots", 670 | "fags", 671 | "fagtard", 672 | "fagz", 673 | "faig", 674 | "faigs", 675 | "faigt", 676 | "fanculo", 677 | "fanny", 678 | "fannybandit", 679 | "fannyflaps", 680 | "fannyfucker", 681 | "fanyy", 682 | "fart", 683 | "fartknocker", 684 | "fatass", 685 | "fcuk", 686 | "fcuker", 687 | "fcuking", 688 | "fecal", 689 | "feces", 690 | "feck", 691 | "fecker", 692 | "feg", 693 | "feist", 694 | "felch", 695 | "felcher", 696 | "felching", 697 | "fellate", 698 | "fellatio", 699 | "feltch", 700 | "feltcher", 701 | "female squirting", 702 | "femdom", 703 | "fenian", 704 | "fice", 705 | "ficken", 706 | "figging", 707 | "fingerbang", 708 | "fingerfuck", 709 | "fingerfucked", 710 | "fingerfucker", 711 | "fingerfuckers", 712 | "fingerfucking", 713 | "fingerfucks", 714 | "fingering", 715 | "fist fuck", 716 | "fisted", 717 | "fistfuck", 718 | "fistfucked", 719 | "fistfucker", 720 | "fistfuckers", 721 | "fistfucking", 722 | "fistfuckings", 723 | "fistfucks", 724 | "fisting", 725 | "fisty", 726 | "fitt*", 727 | "flamer", 728 | "flange", 729 | "flaps", 730 | "fleshflute", 731 | "Flikker", 732 | "flipping the bird", 733 | "flog the log", 734 | "floozy", 735 | "foad", 736 | "foah", 737 | "fondle", 738 | "foobar", 739 | "fook", 740 | "fooker", 741 | "foot fetish", 742 | "footjob", 743 | "foreskin", 744 | "Fotze", 745 | "freex", 746 | "frenchify", 747 | "frigg", 748 | "frigga", 749 | "frotting", 750 | "fubar", 751 | "fuc", 752 | "fuck", 753 | "f-u-c-k", 754 | "fuck buttons", 755 | "fuck hole", 756 | "Fuck off", 757 | "fuck puppet", 758 | "fuck trophy", 759 | "fuck yo mama", 760 | "fuck you", 761 | "fucka", 762 | "fuckass", 763 | "fuck-ass", 764 | "fuckbag", 765 | "fuck-bitch", 766 | "fuckboy", 767 | "fuckbrain", 768 | "fuckbutt", 769 | "fuckbutter", 770 | "fucked", 771 | "fuckedup", 772 | "fucker", 773 | "fuckers", 774 | "fuckersucker", 775 | "fuckface", 776 | "fuckhead", 777 | "fuckheads", 778 | "fuckhole", 779 | "fuckin", 780 | "fucking", 781 | "fuckings", 782 | "fuckingshitmotherfucker", 783 | "fuckme", 784 | "fuckmeat", 785 | "fucknugget", 786 | "fucknut", 787 | "fucknutt", 788 | "fuckoff", 789 | "fucks", 790 | "fuckstick", 791 | "fucktard", 792 | "fuck-tard", 793 | "fucktards", 794 | "fucktart", 795 | "fucktoy", 796 | "fucktwat", 797 | "fuckup", 798 | "fuckwad", 799 | "fuckwhit", 800 | "fuckwit", 801 | "fuckwitt", 802 | "fudge packer", 803 | "fudgepacker", 804 | "fudge-packer", 805 | "fuk", 806 | "fuk*", 807 | "Fukah", 808 | "Fuken", 809 | "fuker", 810 | "Fukin", 811 | "Fukk", 812 | "Fukkah", 813 | "Fukken", 814 | "Fukker", 815 | "fukkers", 816 | "fukkin", 817 | "fuks", 818 | "fukwhit", 819 | "fukwit", 820 | "fuq", 821 | "futanari", 822 | "futkretzn", 823 | "fux", 824 | "fux0r", 825 | "fvck", 826 | "fxck", 827 | "g00k", 828 | "gae", 829 | "gai", 830 | "gang bang", 831 | "gangbang", 832 | "gang-bang", 833 | "gangbanged", 834 | "gangbangs", 835 | "ganja", 836 | "gash", 837 | "gassy ass", 838 | "gay", 839 | "gay sex", 840 | "gayass", 841 | "gaybob", 842 | "gaydo", 843 | "gayfuck", 844 | "gayfuckist", 845 | "gaylord", 846 | "gays", 847 | "gaysex", 848 | "gaytard", 849 | "gaywad", 850 | "gender bender", 851 | "genitals", 852 | "gey", 853 | "gfy", 854 | "ghay", 855 | "ghey", 856 | "giant cock", 857 | "gigolo", 858 | "ginger", 859 | "gippo", 860 | "girl on", 861 | "girl on top", 862 | "girls gone wild", 863 | "git", 864 | "glans", 865 | "goatcx", 866 | "goatse", 867 | "god", 868 | "god damn", 869 | "godamn", 870 | "godamnit", 871 | "goddam", 872 | "god-dam", 873 | "goddammit", 874 | "goddamn", 875 | "goddamned", 876 | "god-damned", 877 | "goddamnit", 878 | "godsdamn", 879 | "gokkun", 880 | "golden shower", 881 | "goldenshower", 882 | "golliwog", 883 | "gonad", 884 | "gonads", 885 | "goo girl", 886 | "gooch", 887 | "goodpoop", 888 | "gook", 889 | "gooks", 890 | "goregasm", 891 | "gringo", 892 | "grope", 893 | "group sex", 894 | "gspot", 895 | "g-spot", 896 | "gtfo", 897 | "guido", 898 | "guiena", 899 | "guro", 900 | "h00r", 901 | "h0ar", 902 | "h0m0", 903 | "h0mo", 904 | "h0r", 905 | "h0re", 906 | "h4x0r", 907 | "ham flap", 908 | "hand job", 909 | "handjob", 910 | "hard core", 911 | "hard on", 912 | "hardcore", 913 | "hardcoresex", 914 | "he11", 915 | "hebe", 916 | "heeb", 917 | "hell", 918 | "hells", 919 | "helvete", 920 | "hemp", 921 | "hentai", 922 | "heroin", 923 | "herp", 924 | "herpes", 925 | "herpy", 926 | "heshe", 927 | "he-she", 928 | "hircismus", 929 | "hitler", 930 | "hiv", 931 | "ho", 932 | "hoar", 933 | "hoare", 934 | "hobag", 935 | "hoe", 936 | "hoer", 937 | "hoer*", 938 | "holy shit", 939 | "hom0", 940 | "homey", 941 | "homo", 942 | "homodumbshit", 943 | "homoerotic", 944 | "homoey", 945 | "honkey", 946 | "honky", 947 | "hooch", 948 | "hookah", 949 | "hooker", 950 | "hoor", 951 | "hoore", 952 | "hootch", 953 | "hooter", 954 | "hooters", 955 | "hore", 956 | "horniest", 957 | "horny", 958 | "hot carl", 959 | "hot chick", 960 | "hotsex", 961 | "how to kill", 962 | "how to murdep", 963 | "how to murder", 964 | "Huevon", 965 | "huge fat", 966 | "hui", 967 | "hump", 968 | "humped", 969 | "humping", 970 | "hun", 971 | "hussy", 972 | "hymen", 973 | "iap", 974 | "iberian slap", 975 | "inbred", 976 | "incest", 977 | "injun", 978 | "intercourse", 979 | "jack off", 980 | "jackass", 981 | "jackasses", 982 | "jackhole", 983 | "jackoff", 984 | "jack-off", 985 | "jaggi", 986 | "jagoff", 987 | "jail bait", 988 | "jailbait", 989 | "jap", 990 | "japs", 991 | "jelly donut", 992 | "jerk", 993 | "jerk off", 994 | "jerk0ff", 995 | "jerkass", 996 | "jerked", 997 | "jerkoff", 998 | "jerk-off", 999 | "jigaboo", 1000 | "jiggaboo", 1001 | "jiggerboo", 1002 | "jisim", 1003 | "jism", 1004 | "jiss", 1005 | "jiz", 1006 | "jizm", 1007 | "jizz", 1008 | "jizzed", 1009 | "jock", 1010 | "juggs", 1011 | "jungle bunny", 1012 | "junglebunny", 1013 | "junkie", 1014 | "junky", 1015 | "kafir", 1016 | "kanker*", 1017 | "kawk", 1018 | "kike", 1019 | "kikes", 1020 | "kill", 1021 | "kinbaku", 1022 | "kinkster", 1023 | "kinky", 1024 | "klan", 1025 | "klootzak", 1026 | "knob", 1027 | "knob end", 1028 | "knobbing", 1029 | "knobead", 1030 | "knobed", 1031 | "knobend", 1032 | "knobhead", 1033 | "knobjocky", 1034 | "knobjokey", 1035 | "knobs", 1036 | "knobz", 1037 | "knulle", 1038 | "kock", 1039 | "kondum", 1040 | "kondums", 1041 | "kooch", 1042 | "kooches", 1043 | "kootch", 1044 | "kraut", 1045 | "kuk", 1046 | "kuksuger", 1047 | "kum", 1048 | "kummer", 1049 | "kumming", 1050 | "kums", 1051 | "kunilingus", 1052 | "kunja", 1053 | "kunt", 1054 | "kunts", 1055 | "kuntz", 1056 | "Kurac", 1057 | "kurwa", 1058 | "kusi*", 1059 | "kwif", 1060 | "kyke", 1061 | "kyrpa*", 1062 | "l3i+ch", 1063 | "l3itch", 1064 | "labia", 1065 | "lameass", 1066 | "lardass", 1067 | "leather restraint", 1068 | "leather straight jacket", 1069 | "lech", 1070 | "lemon party", 1071 | "LEN", 1072 | "leper", 1073 | "lesbian", 1074 | "lesbians", 1075 | "lesbo", 1076 | "lesbos", 1077 | "lez", 1078 | "lezza/lesbo", 1079 | "Lezzian", 1080 | "lezzie", 1081 | "Lipshits", 1082 | "Lipshitz", 1083 | "lmao", 1084 | "lmfao", 1085 | "loin", 1086 | "loins", 1087 | "lolita", 1088 | "looney", 1089 | "lovemaking", 1090 | "lube", 1091 | "lust", 1092 | "lusting", 1093 | "lusty", 1094 | "m0f0", 1095 | "m0fo", 1096 | "m45terbate", 1097 | "ma5terb8", 1098 | "ma5terbate", 1099 | "mafugly", 1100 | "make me come", 1101 | "male squirting", 1102 | "mamhoon", 1103 | "mams", 1104 | "masochist", 1105 | "masokist", 1106 | "massa", 1107 | "massterbait", 1108 | "masstrbait", 1109 | "masstrbate", 1110 | "masterb8", 1111 | "masterbaiter", 1112 | "masterbat*", 1113 | "masterbat3", 1114 | "masterbate", 1115 | "master-bate", 1116 | "masterbates", 1117 | "masterbating", 1118 | "masterbation", 1119 | "masterbations", 1120 | "masturbat*", 1121 | "masturbate", 1122 | "masturbating", 1123 | "masturbation", 1124 | "maxi", 1125 | "mcfagget", 1126 | "menage a trois", 1127 | "menses", 1128 | "menstruate", 1129 | "menstruation", 1130 | "merd*", 1131 | "meth", 1132 | "m-fucking", 1133 | "mibun", 1134 | "mick", 1135 | "microphallus", 1136 | "middle finger", 1137 | "midget", 1138 | "milf", 1139 | "minge", 1140 | "minger", 1141 | "missionary position", 1142 | "mof0", 1143 | "mofo", 1144 | "mo-fo", 1145 | "molest", 1146 | "mong", 1147 | "monkleigh", 1148 | "moo moo foo foo", 1149 | "moolie", 1150 | "moron", 1151 | "Motha Fucker", 1152 | "Motha Fuker", 1153 | "Motha Fukkah", 1154 | "Motha Fukker", 1155 | "mothafuck", 1156 | "mothafucka", 1157 | "mothafuckas", 1158 | "mothafuckaz", 1159 | "mothafucked", 1160 | "mothafucker", 1161 | "mothafuckers", 1162 | "mothafuckin", 1163 | "mothafucking", 1164 | "mothafuckings", 1165 | "mothafucks", 1166 | "mother fucker", 1167 | "Mother Fukah", 1168 | "Mother Fuker", 1169 | "Mother Fukkah", 1170 | "Mother Fukker", 1171 | "motherfuck", 1172 | "motherfucka", 1173 | "motherfucked", 1174 | "motherfucker", 1175 | "mother-fucker", 1176 | "motherfuckers", 1177 | "motherfuckin", 1178 | "motherfucking", 1179 | "motherfuckings", 1180 | "motherfuckka", 1181 | "motherfucks", 1182 | "mouliewop", 1183 | "mound of venus", 1184 | "mr hands", 1185 | "muff", 1186 | "muff diver", 1187 | "muff puff", 1188 | "muffdiver", 1189 | "muffdiving", 1190 | "muie", 1191 | "mulkku", 1192 | "munging", 1193 | "munter", 1194 | "murder", 1195 | "muschi", 1196 | "mutha", 1197 | "Mutha Fucker", 1198 | "Mutha Fukah", 1199 | "Mutha Fuker", 1200 | "Mutha Fukkah", 1201 | "Mutha Fukker", 1202 | "muthafecker", 1203 | "muthafuckker", 1204 | "muther", 1205 | "mutherfucker", 1206 | "n1gga", 1207 | "n1gger", 1208 | "n1gr", 1209 | "naked", 1210 | "nambla", 1211 | "napalm", 1212 | "nappy", 1213 | "nastt", 1214 | "nawashi", 1215 | "nazi", 1216 | "nazis", 1217 | "nazism", 1218 | "need the dick", 1219 | "negro", 1220 | "neonazi", 1221 | "nepesaurio", 1222 | "nig nog", 1223 | "nigaboo", 1224 | "nigg3r", 1225 | "nigg4h", 1226 | "nigga", 1227 | "niggah", 1228 | "niggas", 1229 | "niggaz", 1230 | "nigger", 1231 | "nigger*", 1232 | "nigger;", 1233 | "niggers", 1234 | "niggle", 1235 | "niglet", 1236 | "nig-nog", 1237 | "nigur;", 1238 | "niiger;", 1239 | "niigr;", 1240 | "nimphomania", 1241 | "nimrod", 1242 | "ninny", 1243 | "ninnyhammer", 1244 | "nipple", 1245 | "nipples", 1246 | "nob", 1247 | "nob jokey", 1248 | "nobhead", 1249 | "nobjocky", 1250 | "nobjokey", 1251 | "nonce", 1252 | "nsfw images", 1253 | "nude", 1254 | "nudity", 1255 | "numbnuts", 1256 | "nut butter", 1257 | "nut sack", 1258 | "nutsack", 1259 | "nutter", 1260 | "nympho", 1261 | "nymphomania", 1262 | "octopussy", 1263 | "old bag", 1264 | "omg", 1265 | "omorashi", 1266 | "one cup two girls", 1267 | "one guy one jar", 1268 | "opiate", 1269 | "opium", 1270 | "orafis", 1271 | "orally", 1272 | "organ", 1273 | "orgasim", 1274 | "orgasim;", 1275 | "orgasims", 1276 | "orgasm", 1277 | "orgasmic", 1278 | "orgasms", 1279 | "orgasum", 1280 | "orgies", 1281 | "orgy", 1282 | "oriface", 1283 | "orifice", 1284 | "orifiss", 1285 | "orospu", 1286 | "ovary", 1287 | "ovum", 1288 | "ovums", 1289 | "p.u.s.s.y.", 1290 | "p0rn", 1291 | "packi", 1292 | "packie", 1293 | "packy", 1294 | "paedophile", 1295 | "paki", 1296 | "pakie", 1297 | "paky", 1298 | "panooch", 1299 | "pansy", 1300 | "pantie", 1301 | "panties", 1302 | "panty", 1303 | "paska*", 1304 | "pawn", 1305 | "pcp", 1306 | "pecker", 1307 | "peckerhead", 1308 | "pedo", 1309 | "pedobear", 1310 | "pedophile", 1311 | "pedophilia", 1312 | "pedophiliac", 1313 | "pee", 1314 | "peeenus", 1315 | "peeenusss", 1316 | "peenus", 1317 | "peepee", 1318 | "pegging", 1319 | "peinus", 1320 | "pen1s", 1321 | "penas", 1322 | "penetrate", 1323 | "penetration", 1324 | "penial", 1325 | "penile", 1326 | "penis", 1327 | "penisbanger", 1328 | "penis-breath", 1329 | "penisfucker", 1330 | "penispuffer", 1331 | "penus", 1332 | "penuus", 1333 | "perse", 1334 | "perversion", 1335 | "phallic", 1336 | "phone sex", 1337 | "phonesex", 1338 | "Phuc", 1339 | "Phuck", 1340 | "phuk", 1341 | "phuked", 1342 | "Phuker", 1343 | "phuking", 1344 | "phukked", 1345 | "Phukker", 1346 | "phukking", 1347 | "phuks", 1348 | "phuq", 1349 | "picka", 1350 | "piece of shit", 1351 | "pierdol*", 1352 | "pigfucker", 1353 | "pikey", 1354 | "pillowbiter", 1355 | "pillu*", 1356 | "pimmel", 1357 | "pimp", 1358 | "pimpis", 1359 | "pinko", 1360 | "piss", 1361 | "piss off", 1362 | "piss pig", 1363 | "piss*", 1364 | "pissed", 1365 | "pissed off", 1366 | "pisser", 1367 | "pissers", 1368 | "pisses", 1369 | "pissflaps", 1370 | "pissin", 1371 | "pissing", 1372 | "pissoff", 1373 | "piss-off", 1374 | "pisspig", 1375 | "pizda", 1376 | "playboy", 1377 | "pleasure chest", 1378 | "pms", 1379 | "polac", 1380 | "polack", 1381 | "polak", 1382 | "pole smoker", 1383 | "polesmoker", 1384 | "pollock", 1385 | "ponyplay", 1386 | "poof", 1387 | "poon", 1388 | "poonani", 1389 | "poonany", 1390 | "poontang", 1391 | "poontsee", 1392 | "poop", 1393 | "poop chute", 1394 | "poopchute", 1395 | "Poopuncher", 1396 | "porch monkey", 1397 | "porchmonkey", 1398 | "porn", 1399 | "porno", 1400 | "pornography", 1401 | "pornos", 1402 | "pot", 1403 | "potty", 1404 | "pr0n", 1405 | "pr1c", 1406 | "pr1ck", 1407 | "pr1k", 1408 | "preteen", 1409 | "prick", 1410 | "pricks", 1411 | "prickteaser", 1412 | "prig", 1413 | "prince albert piercing", 1414 | "prod", 1415 | "pron", 1416 | "prostitute", 1417 | "prude", 1418 | "psycho", 1419 | "pthc", 1420 | "pube", 1421 | "pubes", 1422 | "pubic", 1423 | "pubis", 1424 | "pula", 1425 | "pule", 1426 | "punani", 1427 | "punanny", 1428 | "punany", 1429 | "punkass", 1430 | "punky", 1431 | "punta", 1432 | "puss", 1433 | "pusse", 1434 | "pussee", 1435 | "pussi", 1436 | "pussies", 1437 | "pussy", 1438 | "pussy fart", 1439 | "pussy palace", 1440 | "pussylicking", 1441 | "pussypounder", 1442 | "pussys", 1443 | "pust", 1444 | "puta", 1445 | "puto", 1446 | "puuke", 1447 | "puuker", 1448 | "qahbeh", 1449 | "queaf", 1450 | "queef", 1451 | "queef*", 1452 | "queer", 1453 | "queerbait", 1454 | "queerhole", 1455 | "queero", 1456 | "queers", 1457 | "queerz", 1458 | "quicky", 1459 | "quim", 1460 | "qweers", 1461 | "qweerz", 1462 | "qweir", 1463 | "racy", 1464 | "raghead", 1465 | "raging boner", 1466 | "rape", 1467 | "raped", 1468 | "raper", 1469 | "rapey", 1470 | "raping", 1471 | "rapist", 1472 | "raunch", 1473 | "rautenberg", 1474 | "recktum", 1475 | "rectal", 1476 | "rectum", 1477 | "rectus", 1478 | "reefer", 1479 | "reetard", 1480 | "reich", 1481 | "renob", 1482 | "retard", 1483 | "retarded", 1484 | "reverse cowgirl", 1485 | "revue", 1486 | "rimjaw", 1487 | "rimjob", 1488 | "rimming", 1489 | "ritard", 1490 | "rosy palm", 1491 | "rosy palm and her 5 sisters", 1492 | "rtard", 1493 | "r-tard", 1494 | "rubbish", 1495 | "rum", 1496 | "rump", 1497 | "rumprammer", 1498 | "ruski", 1499 | "rusty trombone", 1500 | "s hit", 1501 | "s&m", 1502 | "s.h.i.t.", 1503 | "s.o.b.", 1504 | "s_h_i_t", 1505 | "s0b", 1506 | "sadism", 1507 | "sadist", 1508 | "sambo", 1509 | "sand nigger", 1510 | "sandbar", 1511 | "Sandler", 1512 | "sandnigger", 1513 | "sanger", 1514 | "santorum", 1515 | "sausage queen", 1516 | "scag", 1517 | "scank", 1518 | "scantily", 1519 | "scat", 1520 | "schaffer", 1521 | "scheiss*", 1522 | "schizo", 1523 | "schlampe", 1524 | "schlong", 1525 | "schmuck", 1526 | "scissoring", 1527 | "screw", 1528 | "screwed", 1529 | "screwing", 1530 | "scroat", 1531 | "scrog", 1532 | "scrot", 1533 | "scrote", 1534 | "scrotum", 1535 | "scrud", 1536 | "scum", 1537 | "seaman", 1538 | "seamen", 1539 | "seduce", 1540 | "seks", 1541 | "semen", 1542 | "sex", 1543 | "sexo", 1544 | "sexual", 1545 | "sexy", 1546 | "sh!+", 1547 | "sh!t", 1548 | "sh!t*", 1549 | "sh1t", 1550 | "s-h-1-t", 1551 | "sh1ter", 1552 | "sh1ts", 1553 | "sh1tter", 1554 | "sh1tz", 1555 | "shag", 1556 | "shagger", 1557 | "shaggin", 1558 | "shagging", 1559 | "shamedame", 1560 | "sharmuta", 1561 | "sharmute", 1562 | "shaved beaver", 1563 | "shaved pussy", 1564 | "shemale", 1565 | "shi+", 1566 | "shibari", 1567 | "shipal", 1568 | "shirt lifter", 1569 | "shit", 1570 | "s-h-i-t", 1571 | "shit ass", 1572 | "shit fucker", 1573 | "shitass", 1574 | "shitbag", 1575 | "shitbagger", 1576 | "shitblimp", 1577 | "shitbrains", 1578 | "shitbreath", 1579 | "shitcanned", 1580 | "shitcunt", 1581 | "shitdick", 1582 | "shite", 1583 | "shiteater", 1584 | "shited", 1585 | "shitey", 1586 | "shitface", 1587 | "shitfaced", 1588 | "shitfuck", 1589 | "shitfull", 1590 | "shithead", 1591 | "shitheads", 1592 | "shithole", 1593 | "shithouse", 1594 | "shiting", 1595 | "shitings", 1596 | "shits", 1597 | "shitspitter", 1598 | "shitstain", 1599 | "shitt", 1600 | "shitted", 1601 | "shitter", 1602 | "shitters", 1603 | "shittier", 1604 | "shittiest", 1605 | "shitting", 1606 | "shittings", 1607 | "shitty", 1608 | "Shity", 1609 | "shitz", 1610 | "shiz", 1611 | "shiznit", 1612 | "shota", 1613 | "shrimping", 1614 | "Shyt", 1615 | "Shyte", 1616 | "Shytty", 1617 | "Shyty", 1618 | "sissy", 1619 | "skag", 1620 | "skanck", 1621 | "skank", 1622 | "skankee", 1623 | "skankey", 1624 | "skanks", 1625 | "Skanky", 1626 | "skeet", 1627 | "skribz", 1628 | "skullfuck", 1629 | "skurwysyn", 1630 | "slag", 1631 | "slanteye", 1632 | "slave", 1633 | "sleaze", 1634 | "sleazy", 1635 | "slope", 1636 | "slut", 1637 | "slut bucket", 1638 | "slutbag", 1639 | "slutdumper", 1640 | "slutkiss", 1641 | "sluts", 1642 | "Slutty", 1643 | "slutz", 1644 | "smartass", 1645 | "smartasses", 1646 | "smeg", 1647 | "smegma", 1648 | "smut", 1649 | "smutty", 1650 | "snatch", 1651 | "sniper", 1652 | "snowballing", 1653 | "snuff", 1654 | "s-o-b", 1655 | "sod off", 1656 | "sodom", 1657 | "sodomize", 1658 | "sodomy", 1659 | "son of a bitch", 1660 | "son of a motherless goat", 1661 | "son of a whore", 1662 | "son-of-a-bitch", 1663 | "souse", 1664 | "soused", 1665 | "spac", 1666 | "spade", 1667 | "sperm", 1668 | "sphencter", 1669 | "spic", 1670 | "spick", 1671 | "spierdalaj", 1672 | "spik", 1673 | "spiks", 1674 | "splooge", 1675 | "splooge moose", 1676 | "spooge", 1677 | "spook", 1678 | "spread legs", 1679 | "spunk", 1680 | "stfu", 1681 | "stiffy", 1682 | "stoned", 1683 | "strap on", 1684 | "strapon", 1685 | "strappado", 1686 | "strip", 1687 | "strip club", 1688 | "stroke", 1689 | "stupid", 1690 | "style doggy", 1691 | "suck", 1692 | "suckass", 1693 | "sucked", 1694 | "sucking", 1695 | "sucks", 1696 | "suicide girls", 1697 | "suka", 1698 | "sultry women", 1699 | "sumofabiatch", 1700 | "swastika", 1701 | "swinger", 1702 | "t1t", 1703 | "t1tt1e5", 1704 | "t1tties", 1705 | "taff", 1706 | "taig", 1707 | "tainted love", 1708 | "taking the piss", 1709 | "tampon", 1710 | "tard", 1711 | "tart", 1712 | "taste my", 1713 | "tawdry", 1714 | "tea bagging", 1715 | "teabagging", 1716 | "teat", 1717 | "teets", 1718 | "teez", 1719 | "teste", 1720 | "testee", 1721 | "testes", 1722 | "testical", 1723 | "testicle", 1724 | "testicle*", 1725 | "testis", 1726 | "threesome", 1727 | "throating", 1728 | "thrust", 1729 | "thug", 1730 | "thundercunt", 1731 | "tied up", 1732 | "tight white", 1733 | "tinkle", 1734 | "tit", 1735 | "tit wank", 1736 | "titfuck", 1737 | "titi", 1738 | "tities", 1739 | "tits", 1740 | "titt", 1741 | "titt*", 1742 | "tittie5", 1743 | "tittiefucker", 1744 | "titties", 1745 | "titty", 1746 | "tittyfuck", 1747 | "tittyfucker", 1748 | "tittywank", 1749 | "titwank", 1750 | "toke", 1751 | "tongue in a", 1752 | "toots", 1753 | "topless", 1754 | "tosser", 1755 | "towelhead", 1756 | "tramp", 1757 | "tranny", 1758 | "transsexual", 1759 | "trashy", 1760 | "tribadism", 1761 | "trumped", 1762 | "tub girl", 1763 | "tubgirl", 1764 | "turd", 1765 | "tush", 1766 | "tushy", 1767 | "tw4t", 1768 | "twat", 1769 | "twathead", 1770 | "twatlips", 1771 | "twats", 1772 | "twatty", 1773 | "twatwaffle", 1774 | "twink", 1775 | "twinkie", 1776 | "two fingers", 1777 | "two fingers with tongue", 1778 | "two girls one cup", 1779 | "twunt", 1780 | "twunter", 1781 | "ugly", 1782 | "unclefucker", 1783 | "undies", 1784 | "undressing", 1785 | "unwed", 1786 | "upskirt", 1787 | "urethra play", 1788 | "urinal", 1789 | "urine", 1790 | "urophilia", 1791 | "uterus", 1792 | "uzi", 1793 | "v14gra", 1794 | "v1gra", 1795 | "va1jina", 1796 | "vag", 1797 | "vag1na", 1798 | "vagiina", 1799 | "vagina", 1800 | "vaj1na", 1801 | "vajayjay", 1802 | "vajina", 1803 | "va-j-j", 1804 | "valium", 1805 | "venus mound", 1806 | "veqtable", 1807 | "viagra", 1808 | "vibrator", 1809 | "violet wand", 1810 | "virgin", 1811 | "vittu", 1812 | "vixen", 1813 | "vjayjay", 1814 | "vodka", 1815 | "vomit", 1816 | "vorarephilia", 1817 | "voyeur", 1818 | "vulgar", 1819 | "vullva", 1820 | "vulva", 1821 | "w00se", 1822 | "w0p", 1823 | "wad", 1824 | "wang", 1825 | "wank", 1826 | "wank*", 1827 | "wanker", 1828 | "wankjob", 1829 | "wanky", 1830 | "wazoo", 1831 | "wedgie", 1832 | "weed", 1833 | "weenie", 1834 | "weewee", 1835 | "weiner", 1836 | "weirdo", 1837 | "wench", 1838 | "wet dream", 1839 | "wetback", 1840 | "wetback*", 1841 | "wh00r", 1842 | "wh0re", 1843 | "wh0reface", 1844 | "white power", 1845 | "whiz", 1846 | "whoar", 1847 | "whoralicious", 1848 | "whore", 1849 | "whorealicious", 1850 | "whorebag", 1851 | "whored", 1852 | "whoreface", 1853 | "whorehopper", 1854 | "whorehouse", 1855 | "whores", 1856 | "whoring", 1857 | "wichser", 1858 | "wigger", 1859 | "willies", 1860 | "willy", 1861 | "window licker", 1862 | "wiseass", 1863 | "wiseasses", 1864 | "wog", 1865 | "womb", 1866 | "wop", 1867 | "wop*", 1868 | "wrapping men", 1869 | "wrinkled starfish", 1870 | "wtf", 1871 | "xrated", 1872 | "x-rated", 1873 | "xx", 1874 | "xxx", 1875 | "yaoi", 1876 | "yeasty", 1877 | "yed", 1878 | "yellow showers", 1879 | "yid", 1880 | "yiffy", 1881 | "yobbo", 1882 | "zabourah", 1883 | "zibbi", 1884 | "zoophilia", 1885 | "zubb" 1886 | }; 1887 | 1888 | internal static Lazy> Expressions = new Lazy>(() => EvaluateExpressions()); 1889 | 1890 | static ISet EvaluateExpressions() 1891 | { 1892 | var set = new HashSet(); 1893 | foreach (var wordOrPhrase in 1894 | WordsAndPhrases.Select(term => Regex.Replace(term, @"/(\W)/g", "$1"))) 1895 | { 1896 | set.Add(new Regex($"\\b{wordOrPhrase}\\b", RegexOptions.IgnoreCase)); 1897 | } 1898 | 1899 | return set; 1900 | } 1901 | 1902 | #endregion 1903 | } 1904 | } --------------------------------------------------------------------------------