Switch to aspnet WebApplication

This commit is contained in:
jetsparrow 2022-02-27 20:40:08 +03:00
parent 354876985d
commit 4090adef78
16 changed files with 255 additions and 211 deletions

5
.gitignore vendored
View File

@ -41,6 +41,5 @@ Thumbs.db
*.dotCover
#secret config
karma.cfg.json
*secrets.ini
*.secret.json
secrets.json
secrets.*.json

View File

@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.Options;
using Xunit;
namespace AntiAntiSwearingBot.Tests
@ -8,13 +11,12 @@ namespace AntiAntiSwearingBot.Tests
public class DetectTests
{
Unbleeper ubl { get; }
Config cfg { get; }
SearchDictionary dict { get; }
public DetectTests()
{
cfg = Config.Load<Config>("aasb.cfg.json");
dict = new SearchDictionary(cfg);
dict = new SearchDictionary(OptionsMonitor );
ubl = new Unbleeper(dict, cfg.Unbleeper);
}

View File

@ -1,42 +1,43 @@
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
using Telegram.Bot;
using Telegram.Bot.Exceptions;
using Telegram.Bot.Extensions.Polling;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using AntiAntiSwearingBot.Commands;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace AntiAntiSwearingBot;
public class AntiAntiSwearingBot
public class Aasb : IHostedService
{
Config Config { get; }
SearchDictionary Dict { get; }
Unbleeper Unbleeper { get; }
ILogger Log { get; }
public bool Started { get; private set; } = false;
public AntiAntiSwearingBot(Config cfg, SearchDictionary dict)
public Aasb(ILogger<Aasb> log, IOptions<TelegramSettings> tgCfg, Unbleeper unbp)
{
Config = cfg;
Dict = dict;
Unbleeper = new Unbleeper(dict, cfg.Unbleeper);
Log = log;
TelegramSettings = tgCfg.Value;
Unbleeper = unbp;
}
TelegramSettings TelegramSettings { get; }
TelegramBotClient TelegramBot { get; set; }
ChatCommandRouter Router { get; set; }
public User Me { get; private set; }
public async Task Init()
public async Task StartAsync(CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(Config.ApiKey))
if (string.IsNullOrWhiteSpace(TelegramSettings.ApiKey))
return;
TelegramBot = new TelegramBotClient(Config.ApiKey);
TelegramBot = new TelegramBotClient(TelegramSettings.ApiKey);
Me = await TelegramBot.GetMeAsync();
Log.LogInformation("Connected to Telegram as @{Username}", Me.Username);
Router = new ChatCommandRouter(Me.Username);
Router.Add(new LearnCommand(Dict), "learn");
Router.Add(new UnlearnCommand(Dict), "unlearn");
@ -46,16 +47,19 @@ public class AntiAntiSwearingBot
HandleUpdateAsync,
HandleErrorAsync,
receiverOptions);
Log.LogInformation("AntiAntiSwearBot started!");
Started = true;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await TelegramBot.CloseAsync();
}
Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken cancellationToken)
{
var ErrorMessage = exception switch
{
ApiRequestException apiRequestException => $"Telegram API Error:\n[{apiRequestException.ErrorCode}]\n{apiRequestException.Message}",
_ => exception.ToString()
};
Console.WriteLine(ErrorMessage);
Log.LogError(exception, "Exception while handling API message");
return Task.CompletedTask;
}
@ -76,7 +80,8 @@ public class AntiAntiSwearingBot
await TelegramBot.SendTextMessageAsync(
msg.Chat.Id,
commandResponse,
replyToMessageId: msg.MessageId);
replyToMessageId: msg.MessageId,
disableNotification: true);
}
else
{
@ -85,13 +90,13 @@ public class AntiAntiSwearingBot
await TelegramBot.SendTextMessageAsync(
msg.Chat.Id,
unbleepResponse,
replyToMessageId: msg.MessageId);
replyToMessageId: msg.MessageId,
disableNotification: true);
}
}
catch (Exception e)
{
Console.WriteLine(e);
Log.LogError(e, "Exception while handling message {0}", msg);
}
}
}

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
@ -10,10 +10,18 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Telegram.Bot.Extensions.Polling" Version="2.0.0-alpha.1" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="secrets.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="aasb.cfg.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>

View File

@ -1,29 +1,28 @@
namespace AntiAntiSwearingBot;
public class Config : ConfigBase
public class ServiceSettings
{
public string ApiKey { get; private set; }
public ProxySettings Proxy { get; private set; }
public SearchDictionarySettings SearchDictionary { get; private set; }
public UnbleeperSettings Unbleeper { get; private set; }
public string Urls { get; set; }
}
public struct UnbleeperSettings
public class UnbleeperSettings
{
public string BleepedSwearsRegex { get; private set; }
public int MinAmbiguousWordLength { get; private set; }
public int MinWordLength { get; private set; }
public string BleepedSwearsRegex { get; set; }
public int MinAmbiguousWordLength { get; set; }
public int MinWordLength { get; set; }
}
public struct SearchDictionarySettings
public class SearchDictionarySettings
{
public string DictionaryPath { get; private set; }
public string DictionaryPath { get; set; }
}
public struct ProxySettings
public class TelegramSettings
{
public string Url { get; private set; }
public int Port { get; private set; }
public string Login { get; private set; }
public string Password { get; private set; }
public string ApiKey { get; set; }
public bool UseProxy { get; set; }
public string Url { get; set; }
public int Port { get; set; }
public string Login { get; set; }
public string Password { get; set; }
}

View File

@ -1,75 +0,0 @@
using System.IO;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace AntiAntiSwearingBot;
public abstract class ConfigBase
{
public static T Load<T>(params string[] paths) where T : ConfigBase, new()
{
var result = new T();
var configJson = new JObject();
var mergeSettings = new JsonMergeSettings
{
MergeArrayHandling = MergeArrayHandling.Union
};
foreach (var path in paths)
{
try { configJson.Merge(JObject.Parse(File.ReadAllText(path)), mergeSettings);}
catch { }
}
using (var sr = configJson.CreateReader())
{
var settings = new JsonSerializerSettings
{
ContractResolver = new PrivateSetterContractResolver()
};
JsonSerializer.CreateDefault(settings).Populate(sr, result);
}
return result;
}
}
public class PrivateSetterContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var jProperty = base.CreateProperty(member, memberSerialization);
if (jProperty.Writable)
return jProperty;
jProperty.Writable = member.IsPropertyWithSetter();
return jProperty;
}
}
public class PrivateSetterCamelCasePropertyNamesContractResolver : CamelCasePropertyNamesContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var jProperty = base.CreateProperty(member, memberSerialization);
if (jProperty.Writable)
return jProperty;
jProperty.Writable = member.IsPropertyWithSetter();
return jProperty;
}
}
internal static class MemberInfoExtensions
{
internal static bool IsPropertyWithSetter(this MemberInfo member)
{
var property = member as PropertyInfo;
return property?.GetSetMethod(true) != null;
}
}

View File

@ -0,0 +1,11 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace AntiAntiSwearingBot;
public static class IServiceCollectionExtensions
{
public static IServiceCollection AddHostedSingleton<TService>(this IServiceCollection isc) where TService : class, IHostedService
{
return isc.AddSingleton<TService>().AddHostedService(svc => svc.GetRequiredService<TService>());
}
}

View File

@ -0,0 +1,23 @@
using System.Threading;
namespace AntiAntiSwearingBot;
public readonly ref struct ReadLockToken
{
ReaderWriterLockSlim Lock { get; }
public ReadLockToken(ReaderWriterLockSlim l) => (Lock = l).EnterReadLock();
public void Dispose() => Lock.ExitReadLock();
}
public readonly ref struct WriteLockToken
{
ReaderWriterLockSlim Lock { get; }
public WriteLockToken(ReaderWriterLockSlim l) => (Lock = l).EnterWriteLock();
public void Dispose() => Lock.ExitWriteLock();
}
public static class ReaderWriterLockSlimExtensions
{
public static ReadLockToken GetReadLockToken(this ReaderWriterLockSlim l) => new ReadLockToken(l);
public static WriteLockToken GetWriteLockToken(this ReaderWriterLockSlim l) => new WriteLockToken(l);
}

View File

@ -3,3 +3,4 @@ global using System.Text;
global using System.Linq;
global using System.Collections.Generic;
global using Microsoft.Extensions.Options;

View File

@ -1,31 +1,34 @@
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using AntiAntiSwearingBot;
static void Log(string m) => Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}|{m}");
var builder = WebApplication.CreateBuilder();
Log("AntiAntiSwearBot starting....");
var cfg = Config.Load<Config>("aasb.cfg.json", "aasb.cfg.secret.json");
var dict = new SearchDictionary(cfg);
Log($"{dict.Count} words loaded.");
var bot = new AntiAntiSwearingBot.AntiAntiSwearingBot(cfg, dict);
bot.Init().Wait();
Log($"Connected to Telegram as @{bot.Me.Username}");
Log("AntiAntiSwearBot started! Press Ctrl-C to exit.");
ManualResetEvent quitEvent = new ManualResetEvent(false);
try
builder.WebHost.ConfigureAppConfiguration((hostingContext, config) =>
{
Console.CancelKeyPress += (sender, eArgs) => // ctrl-c
var env = hostingContext.HostingEnvironment.EnvironmentName;
config.AddJsonFile("secrets.json", optional: true, reloadOnChange: true);
config.AddJsonFile($"secrets.{env}.json", optional: true, reloadOnChange: true);
});
var cfg = builder.Configuration;
var svc = builder.Services;
svc.Configure<SearchDictionarySettings>(cfg.GetSection("SearchDictionary"));
svc.Configure<TelegramSettings>(cfg.GetSection("Telegram"));
svc.Configure<UnbleeperSettings>(cfg.GetSection("Unbleeper"));
svc.AddHealthChecks().AddCheck<StartupHealthCheck>("Startup");
svc.AddHostedSingleton<SearchDictionary>();
svc.AddSingleton<Unbleeper>();
svc.AddHostedSingleton<Aasb>();
var app = builder.Build();
app.UseDeveloperExceptionPage();
app.UseRouting();
app.UseEndpoints(cfg =>
{
eArgs.Cancel = true;
quitEvent.Set();
};
}
catch { }
quitEvent.WaitOne(Timeout.Infinite);
Console.WriteLine("Waiting for exit...");
dict.Save();
cfg.MapHealthChecks("/health");
});
app.Run();

View File

@ -7,9 +7,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PublishProtocol>FileSystem</PublishProtocol>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<TargetFramework>netcoreapp2.1</TargetFramework>
<PublishDir>i:\aasb</PublishDir>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<TargetFramework>net6.0</TargetFramework>
<PublishDir>C:\Prog\releases\aasb</PublishDir>
<SelfContained>false</SelfContained>
<_IsPortable>true</_IsPortable>
</PropertyGroup>

View File

@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50671/",
"sslPort": 44398
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"AntiAntiSwearingBot": {
"commandName": "Project",
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

View File

@ -1,55 +1,73 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
namespace AntiAntiSwearingBot;
public class SearchDictionary
public class SearchDictionary : BackgroundService
{
public SearchDictionary(Config cfg)
public SearchDictionary(IOptionsMonitor<SearchDictionarySettings> cfg)
{
var s = cfg.SearchDictionary;
path = s.DictionaryPath;
tmppath = path + ".tmp";
Cfg = cfg;
var path = cfg.CurrentValue.DictionaryPath;
words = File.ReadAllLines(path).ToList();
}
IOptionsMonitor<SearchDictionarySettings> Cfg { get; }
ReaderWriterLockSlim DictLock = new();
List<string> words;
bool Changed = false;
public int Count => words.Count;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
if (Changed) Save();
}
}
finally
{
Save();
}
}
public void Save()
{
if (File.Exists(tmppath))
File.Delete(tmppath);
using var guard = DictLock.GetWriteLockToken();
Changed = false;
var path = Cfg.CurrentValue.DictionaryPath;
var tmppath = path + ".tmp";
File.WriteAllLines(tmppath, words);
if (File.Exists(path))
File.Delete(path);
File.Move(tmppath, path);
File.Move(tmppath, path, overwrite: true);
}
public struct WordMatch
{
public string Word;
public int Distance;
public int Rating;
}
public record struct WordMatch (string Word, int Distance, int Rating);
public WordMatch Match(string pattern)
=> AllMatches(pattern).First();
public IEnumerable<WordMatch> AllMatches(string pattern)
{
lock (SyncRoot)
{
pattern = pattern.ToLowerInvariant();
using var guard = DictLock.GetReadLockToken();
return words
.Select((w, i) => new WordMatch { Word = w, Distance = Language.LevenshteinDistance(pattern, w), Rating = i })
.Select((w, i) => new WordMatch(w, Language.LevenshteinDistance(pattern.ToLowerInvariant(), w), i))
.OrderBy(m => m.Distance)
.ThenBy(m => m.Rating);
}
}
public bool Learn(string word)
{
lock (SyncRoot)
{
using var guard = DictLock.GetWriteLockToken();
Changed = true;
int index = words.IndexOf(word);
if (index > 0)
{
@ -62,20 +80,12 @@ public class SearchDictionary
return true;
}
}
}
public bool Unlearn(string word)
{
lock (SyncRoot)
using var guard = DictLock.GetWriteLockToken();
Changed = true;
return words.Remove(word);
}
#region service
readonly string path, tmppath;
object SyncRoot = new object();
List<string> words;
#endregion
}

View File

@ -0,0 +1,24 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace AntiAntiSwearingBot;
public class StartupHealthCheck : IHealthCheck
{
Aasb Bot { get; }
public StartupHealthCheck(Aasb bot)
{
Bot = bot;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
if (Bot.Started)
return Task.FromResult(HealthCheckResult.Healthy("The startup task has completed."));
else
return Task.FromResult(HealthCheckResult.Unhealthy("That startup task is still running."));
}
}

View File

@ -7,10 +7,10 @@ public class Unbleeper
SearchDictionary Dict { get; }
UnbleeperSettings Cfg { get; }
public Unbleeper(SearchDictionary dict, UnbleeperSettings cfg)
public Unbleeper(SearchDictionary dict, IOptions<UnbleeperSettings> cfg)
{
Dict = dict;
Cfg = cfg;
Cfg = cfg.Value;
BleepedSwearsRegex = new Regex("^" + Cfg.BleepedSwearsRegex + "$", RegexOptions.Compiled);
}
@ -41,8 +41,9 @@ public class Unbleeper
)
.ToArray();
if (candidates.Any())
{
if (!candidates.Any())
return null;
var response = new StringBuilder();
for (int i = 0; i < candidates.Length; ++i)
{
@ -51,7 +52,4 @@ public class Unbleeper
}
return response.ToString();
}
else
return null;
}
}

View File

@ -1,4 +1,11 @@
{
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://localhost:9999"
}
}
},
"Unbleeper": {
"BleepedSwearsRegex": "[а-яА-ЯёЁ@\\*#]+",
"MinWordLength": 3,
@ -6,5 +13,8 @@
},
"SearchDictionary": {
"DictionaryPath": "dict/ObsceneDictionaryRu.txt"
},
"Telegram": {
"UseProxy": false
}
}