mirror of
https://github.com/Jetsparrow/antiantiswearingbot.git
synced 2026-01-20 23:16:08 +03:00
Massive rewrite
This commit is contained in:
parent
16c3544526
commit
bc4a16ff71
35
Jetsparrow.Aasb.Tests/BleepTestsBase.cs
Normal file
35
Jetsparrow.Aasb.Tests/BleepTestsBase.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using Jetsparrow.Aasb.Services;
|
||||||
|
using Jetsparrow.Aasb.Tests.Utils;
|
||||||
|
|
||||||
|
namespace Jetsparrow.Aasb.Tests;
|
||||||
|
|
||||||
|
public class BleepTestsBase
|
||||||
|
{
|
||||||
|
public BleepTestsBase()
|
||||||
|
{
|
||||||
|
dictCfg = Options.Create(new SearchDictionarySettings
|
||||||
|
{
|
||||||
|
AutosavePeriod = TimeSpan.MaxValue,
|
||||||
|
DictionaryPath = "dict/ObsceneDictionaryRu.txt"
|
||||||
|
});
|
||||||
|
|
||||||
|
ublCfg = Options.Create(new UnbleeperSettings
|
||||||
|
{
|
||||||
|
LegalWordsRegex = "[а-яА-ЯёЁ]+",
|
||||||
|
BleepedSwearsRegex = "[а-яА-ЯёЁ@\\*#]+",
|
||||||
|
MinWordLength = 3,
|
||||||
|
MinAmbiguousWordLength = 5
|
||||||
|
});
|
||||||
|
|
||||||
|
lifetime = new FakeLifetime();
|
||||||
|
|
||||||
|
dict = new SearchDictionary(dictCfg, ublCfg, lifetime);
|
||||||
|
ubl = new Unbleeper(dict, ublCfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Unbleeper ubl { get; }
|
||||||
|
protected SearchDictionary dict { get; }
|
||||||
|
protected IOptions<SearchDictionarySettings> dictCfg { get; }
|
||||||
|
protected IOptions<UnbleeperSettings> ublCfg { get; }
|
||||||
|
protected IHostApplicationLifetime lifetime { get; }
|
||||||
|
}
|
||||||
@ -1,29 +1,13 @@
|
|||||||
using System;
|
namespace Jetsparrow.Aasb.Tests;
|
||||||
|
|
||||||
using Microsoft.Extensions.Options;
|
public class DetectTests : BleepTestsBase
|
||||||
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Jetsparrow.Aasb.Tests;
|
|
||||||
|
|
||||||
public class DetectTests
|
|
||||||
{
|
{
|
||||||
Unbleeper ubl { get; }
|
|
||||||
SearchDictionary dict { get; }
|
|
||||||
|
|
||||||
public DetectTests()
|
|
||||||
{
|
|
||||||
|
|
||||||
dict = new SearchDictionary(MockOptionsMonitor.Create(DefaultSettings.SearchDictionary));
|
|
||||||
ubl = new Unbleeper(dict, Options.Create(DefaultSettings.Unbleeper));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("бл**ь", "*блядь")]
|
[InlineData("бл**ь", "*блядь")]
|
||||||
[InlineData("ж**а", "*жопа")]
|
[InlineData("ж**а", "*жопа")]
|
||||||
public void UnbleepSimpleSwears(string word, string expected)
|
public async Task UnbleepSimpleSwears(string word, string expected)
|
||||||
{
|
{
|
||||||
var unbleep = ubl.UnbleepSwears(word).TrimEnd(Environment.NewLine.ToCharArray());
|
var unbleep = (await ubl.UnbleepSwears(word)).TrimEnd(Environment.NewLine.ToCharArray());
|
||||||
Assert.Equal(expected, unbleep);
|
Assert.Equal(expected, unbleep);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,9 +18,9 @@ public class DetectTests
|
|||||||
[InlineData("еб*ть—колотить", "*ебать")]
|
[InlineData("еб*ть—колотить", "*ебать")]
|
||||||
[InlineData("Получилась полная х**ня: даже не знаю, что и сказать, б**.", "*херня\n**бля")]
|
[InlineData("Получилась полная х**ня: даже не знаю, что и сказать, б**.", "*херня\n**бля")]
|
||||||
[InlineData("Сергей опять вы**нулся своим знанием тонкостей русского языка; в окно еб*шил стылый ноябрьский ветер. ", "*выебнулся\n**ебашил")]
|
[InlineData("Сергей опять вы**нулся своим знанием тонкостей русского языка; в окно еб*шил стылый ноябрьский ветер. ", "*выебнулся\n**ебашил")]
|
||||||
public void DetectWordsWithPunctuation(string text, string expected)
|
public async void DetectWordsWithPunctuation(string text, string expected)
|
||||||
{
|
{
|
||||||
var unbleep = ubl.UnbleepSwears(text).Replace("\r\n", "\n").Trim();
|
var unbleep = (await ubl.UnbleepSwears(text)).Replace("\r\n", "\n").Trim();
|
||||||
Assert.Equal(expected, unbleep);
|
Assert.Equal(expected, unbleep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,6 @@
|
|||||||
using Microsoft.Extensions.Options;
|
namespace Jetsparrow.Aasb.Tests;
|
||||||
|
public class FilterTests : BleepTestsBase
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Jetsparrow.Aasb.Tests;
|
|
||||||
|
|
||||||
public class FilterTests
|
|
||||||
{
|
{
|
||||||
Unbleeper ubl { get; }
|
|
||||||
SearchDictionary dict { get; }
|
|
||||||
|
|
||||||
public FilterTests()
|
|
||||||
{
|
|
||||||
dict = new SearchDictionary(MockOptionsMonitor.Create(DefaultSettings.SearchDictionary));
|
|
||||||
ubl = new Unbleeper(dict, Options.Create(DefaultSettings.Unbleeper));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("*")]
|
[InlineData("*")]
|
||||||
[InlineData("**#")]
|
[InlineData("**#")]
|
||||||
|
|||||||
11
Jetsparrow.Aasb.Tests/GlobalUsings.cs
Normal file
11
Jetsparrow.Aasb.Tests/GlobalUsings.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
global using System;
|
||||||
|
global using System.Collections.Generic;
|
||||||
|
global using System.Linq;
|
||||||
|
global using System.Text;
|
||||||
|
global using System.Threading.Tasks;
|
||||||
|
|
||||||
|
|
||||||
|
global using Microsoft.Extensions.Hosting;
|
||||||
|
global using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
global using Xunit;
|
||||||
@ -16,4 +16,10 @@
|
|||||||
<ProjectReference Include="..\Jetsparrow.Aasb\Jetsparrow.Aasb.csproj" />
|
<ProjectReference Include="..\Jetsparrow.Aasb\Jetsparrow.Aasb.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="dict\ObsceneDictionaryRu.txt">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
18
Jetsparrow.Aasb.Tests/Utils/FakeLifetime.cs
Normal file
18
Jetsparrow.Aasb.Tests/Utils/FakeLifetime.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Jetsparrow.Aasb.Tests.Utils;
|
||||||
|
public class FakeLifetime : IHostApplicationLifetime
|
||||||
|
{
|
||||||
|
CancellationTokenSource Started = new(), Stopping = new(), Stopped = new();
|
||||||
|
|
||||||
|
public CancellationToken ApplicationStarted => Started.Token;
|
||||||
|
|
||||||
|
public CancellationToken ApplicationStopping => Stopping.Token;
|
||||||
|
|
||||||
|
public CancellationToken ApplicationStopped => Stopped.Token;
|
||||||
|
|
||||||
|
public void StopApplication()
|
||||||
|
{
|
||||||
|
Stopping.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
1315
Jetsparrow.Aasb.Tests/dict/ObsceneDictionaryRu.txt
Normal file
1315
Jetsparrow.Aasb.Tests/dict/ObsceneDictionaryRu.txt
Normal file
File diff suppressed because it is too large
Load Diff
5
Jetsparrow.Aasb/.config/dotnet-tools.json
Normal file
5
Jetsparrow.Aasb/.config/dotnet-tools.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {}
|
||||||
|
}
|
||||||
@ -1,102 +0,0 @@
|
|||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Telegram.Bot;
|
|
||||||
using Telegram.Bot.Extensions.Polling;
|
|
||||||
using Telegram.Bot.Types;
|
|
||||||
using Telegram.Bot.Types.Enums;
|
|
||||||
using Jetsparrow.Aasb.Commands;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Jetsparrow.Aasb;
|
|
||||||
|
|
||||||
public class Aasb : IHostedService
|
|
||||||
{
|
|
||||||
SearchDictionary Dict { get; }
|
|
||||||
Unbleeper Unbleeper { get; }
|
|
||||||
ILogger Log { get; }
|
|
||||||
public bool Started { get; private set; } = false;
|
|
||||||
|
|
||||||
public Aasb(ILogger<Aasb> log, IOptions<TelegramSettings> tgCfg, Unbleeper unbp)
|
|
||||||
{
|
|
||||||
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 StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(TelegramSettings.ApiKey))
|
|
||||||
return;
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
var receiverOptions = new ReceiverOptions { AllowedUpdates = new[] { UpdateType.Message } };
|
|
||||||
TelegramBot.StartReceiving(
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
Log.LogError(exception, "Exception while handling API message");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task HandleUpdateAsync(ITelegramBotClient sender, Update update, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (update.Type != UpdateType.Message || update?.Message?.Type != MessageType.Text)
|
|
||||||
return;
|
|
||||||
var msg = update.Message!;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string commandResponse = null;
|
|
||||||
|
|
||||||
try { commandResponse = Router.Execute(sender, update); }
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
if (commandResponse != null)
|
|
||||||
{
|
|
||||||
await TelegramBot.SendTextMessageAsync(
|
|
||||||
msg.Chat.Id,
|
|
||||||
commandResponse,
|
|
||||||
replyToMessageId: msg.MessageId,
|
|
||||||
disableNotification: true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var unbleepResponse = Unbleeper.UnbleepSwears(msg.Text);
|
|
||||||
if (unbleepResponse != null)
|
|
||||||
await TelegramBot.SendTextMessageAsync(
|
|
||||||
msg.Chat.Id,
|
|
||||||
unbleepResponse,
|
|
||||||
replyToMessageId: msg.MessageId,
|
|
||||||
disableNotification: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.LogError(e, "Exception while handling message {0}", msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
using Telegram.Bot.Types;
|
|
||||||
using Jetsparrow.Aasb.Commands;
|
|
||||||
|
|
||||||
namespace Jetsparrow.Aasb;
|
|
||||||
|
|
||||||
public interface IChatCommand
|
|
||||||
{
|
|
||||||
string Execute(CommandString cmd, Update messageEventArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ChatCommandRouter
|
|
||||||
{
|
|
||||||
string Username { get; }
|
|
||||||
Dictionary<string, IChatCommand> Commands { get; }
|
|
||||||
|
|
||||||
public ChatCommandRouter(string username)
|
|
||||||
{
|
|
||||||
Username = username;
|
|
||||||
Commands = new Dictionary<string, IChatCommand>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Execute(object sender, Update args)
|
|
||||||
{
|
|
||||||
var text = args.Message.Text;
|
|
||||||
if (CommandString.TryParse(text, out var cmd))
|
|
||||||
{
|
|
||||||
if (cmd.Username != null && cmd.Username != Username)
|
|
||||||
return null;
|
|
||||||
if (Commands.ContainsKey(cmd.Command))
|
|
||||||
return Commands[cmd.Command].Execute(cmd, args);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(IChatCommand c, params string[] cmds)
|
|
||||||
{
|
|
||||||
foreach (var cmd in cmds)
|
|
||||||
{
|
|
||||||
if (Commands.ContainsKey(cmd))
|
|
||||||
throw new ArgumentException($"collision for {cmd}, commands {Commands[cmd].GetType()} and {c.GetType()}");
|
|
||||||
Commands[cmd] = c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
43
Jetsparrow.Aasb/Commands/CommandRouter.cs
Normal file
43
Jetsparrow.Aasb/Commands/CommandRouter.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
namespace Jetsparrow.Aasb.Commands;
|
||||||
|
public class ChatCommandRouter
|
||||||
|
{
|
||||||
|
string BotUsername { get; }
|
||||||
|
Dictionary<string, IChatCommand> Commands { get; }
|
||||||
|
IOptionsMonitor<AccessSettings> Access { get; }
|
||||||
|
|
||||||
|
public ChatCommandRouter(string username, IOptionsMonitor<AccessSettings> accessCfg)
|
||||||
|
{
|
||||||
|
BotUsername = username;
|
||||||
|
Access = accessCfg;
|
||||||
|
Commands = new Dictionary<string, IChatCommand>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register(IChatCommand c, params string[] cmds)
|
||||||
|
{
|
||||||
|
foreach (var cmd in cmds)
|
||||||
|
{
|
||||||
|
if (Commands.ContainsKey(cmd))
|
||||||
|
throw new ArgumentException($"collision for {cmd}, commands {Commands[cmd].GetType()} and {c.GetType()}");
|
||||||
|
Commands[cmd] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Execute(CommandContext cmd)
|
||||||
|
{
|
||||||
|
var allowed = Access.CurrentValue.AllowedChats;
|
||||||
|
|
||||||
|
if (cmd.Recipient != null && cmd.Recipient != BotUsername)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!Commands.TryGetValue(cmd.Command, out var handler))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (handler.Authorize)
|
||||||
|
{
|
||||||
|
if (!allowed.Contains(cmd.ChatId) && !allowed.Contains(cmd.SenderId))
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler.Execute(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,40 +1,9 @@
|
|||||||
using System.Text.RegularExpressions;
|
namespace Jetsparrow.Aasb.Commands;
|
||||||
|
public class CommandContext
|
||||||
namespace Jetsparrow.Aasb.Commands;
|
|
||||||
|
|
||||||
public class CommandString
|
|
||||||
{
|
{
|
||||||
public CommandString(string command, string username, params string[] parameters)
|
public string Command { get; set; }
|
||||||
{
|
public string Recipient { get; set; }
|
||||||
Command = command;
|
public string ChatId { get; set; }
|
||||||
Username = username;
|
public string SenderId { get; set; }
|
||||||
Parameters = parameters;
|
public string[] Parameters { get; set; }
|
||||||
}
|
|
||||||
|
|
||||||
public string Command { get; }
|
|
||||||
public string Username { get; }
|
|
||||||
public string[] Parameters { get; }
|
|
||||||
|
|
||||||
static readonly char[] WS_CHARS = new[] { ' ', '\r', '\n', '\n' };
|
|
||||||
|
|
||||||
public static bool TryParse(string s, out CommandString result)
|
|
||||||
{
|
|
||||||
result = null;
|
|
||||||
if (string.IsNullOrWhiteSpace(s) || s[0] != '/')
|
|
||||||
return false;
|
|
||||||
|
|
||||||
string[] words = s.Split(WS_CHARS, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
var cmdRegex = new Regex(@"/(?<cmd>\w+)(@(?<name>\w+))?");
|
|
||||||
var match = cmdRegex.Match(words.First());
|
|
||||||
if (!match.Success)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
string cmd = match.Groups["cmd"].Captures[0].Value;
|
|
||||||
string username = match.Groups["name"].Captures.Count > 0 ? match.Groups["name"].Captures[0].Value : null;
|
|
||||||
string[] parameters = words.Skip(1).ToArray();
|
|
||||||
|
|
||||||
result = new CommandString(cmd, username, parameters);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
6
Jetsparrow.Aasb/Commands/IChatCommand.cs
Normal file
6
Jetsparrow.Aasb/Commands/IChatCommand.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace Jetsparrow.Aasb.Commands;
|
||||||
|
public interface IChatCommand
|
||||||
|
{
|
||||||
|
bool Authorize { get; }
|
||||||
|
string Execute(CommandContext cmd);
|
||||||
|
}
|
||||||
11
Jetsparrow.Aasb/Commands/IdCommand.cs
Normal file
11
Jetsparrow.Aasb/Commands/IdCommand.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace Jetsparrow.Aasb.Commands;
|
||||||
|
public class IdCommand : IChatCommand
|
||||||
|
{
|
||||||
|
public bool Authorize => true;
|
||||||
|
public string Execute(CommandContext cmd)
|
||||||
|
{
|
||||||
|
if (cmd.ChatId == cmd.SenderId)
|
||||||
|
return $"userid: `{cmd.SenderId}`";
|
||||||
|
return $"chatid: `{cmd.ChatId}`";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,26 +1,28 @@
|
|||||||
using System.Text.RegularExpressions;
|
using Jetsparrow.Aasb.Services;
|
||||||
using Telegram.Bot.Types;
|
|
||||||
|
|
||||||
namespace Jetsparrow.Aasb.Commands;
|
namespace Jetsparrow.Aasb.Commands;
|
||||||
public class LearnCommand : IChatCommand
|
public class LearnCommand : IChatCommand
|
||||||
{
|
{
|
||||||
|
public bool Authorize => true;
|
||||||
SearchDictionary Dict { get; }
|
SearchDictionary Dict { get; }
|
||||||
|
|
||||||
public LearnCommand(SearchDictionary dict)
|
public LearnCommand(SearchDictionary dict)
|
||||||
{
|
{
|
||||||
Dict = dict;
|
Dict = dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Execute(CommandString cmd, Update args)
|
public string Execute(CommandContext cmd)
|
||||||
{
|
{
|
||||||
var word = cmd.Parameters.FirstOrDefault();
|
var word = cmd.Parameters.FirstOrDefault();
|
||||||
if (string.IsNullOrWhiteSpace(word))
|
if (string.IsNullOrWhiteSpace(word))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (!Regex.IsMatch(word, @"[а-яА-Я]+"))
|
var learnRes = Dict.Learn(word);
|
||||||
return null;
|
return learnRes switch
|
||||||
|
{
|
||||||
bool newWord = Dict.Learn(word);
|
SearchDictionary.LearnResult.Known => $"Я знаю что такое \"{word}\"",
|
||||||
return newWord ? $"Принято слово \"{word}\"" : $"Поднял рейтинг слову \"{word}\"";
|
SearchDictionary.LearnResult.Added => $"Понял принял, \"{word}\"",
|
||||||
|
SearchDictionary.LearnResult.Illegal => "Я такое запоминать не буду",
|
||||||
|
_ => "ась?"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
using System.Text.RegularExpressions;
|
using Jetsparrow.Aasb.Services;
|
||||||
using Telegram.Bot.Types;
|
|
||||||
|
|
||||||
namespace Jetsparrow.Aasb.Commands;
|
namespace Jetsparrow.Aasb.Commands;
|
||||||
public class UnlearnCommand : IChatCommand
|
public class UnlearnCommand : IChatCommand
|
||||||
{
|
{
|
||||||
|
public bool Authorize => true;
|
||||||
SearchDictionary Dict { get; }
|
SearchDictionary Dict { get; }
|
||||||
|
|
||||||
public UnlearnCommand(SearchDictionary dict)
|
public UnlearnCommand(SearchDictionary dict)
|
||||||
@ -11,17 +11,15 @@ public class UnlearnCommand : IChatCommand
|
|||||||
Dict = dict;
|
Dict = dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Execute(CommandString cmd, Update args)
|
public string Execute(CommandContext cmd)
|
||||||
{
|
{
|
||||||
var word = cmd.Parameters.FirstOrDefault();
|
var word = cmd.Parameters.FirstOrDefault();
|
||||||
if (string.IsNullOrWhiteSpace(word))
|
if (string.IsNullOrWhiteSpace(word))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (!Regex.IsMatch(word, @"[а-яА-Я]+"))
|
|
||||||
return null;
|
|
||||||
if (Dict.Unlearn(word))
|
if (Dict.Unlearn(word))
|
||||||
return $"Удалил слово \"{word}\"";
|
return $"Больше не буду";
|
||||||
else
|
else
|
||||||
return $"Не нашел слово \"{word}\"";
|
return $"А я и не знаю что такое \"{word}\"";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
namespace Jetsparrow.Aasb;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
public class ServiceSettings
|
namespace Jetsparrow.Aasb;
|
||||||
{
|
|
||||||
public string Urls { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UnbleeperSettings
|
public class UnbleeperSettings
|
||||||
{
|
{
|
||||||
|
public string LegalWordsRegex { get; set; }
|
||||||
public string BleepedSwearsRegex { get; set; }
|
public string BleepedSwearsRegex { get; set; }
|
||||||
public int MinAmbiguousWordLength { get; set; }
|
public int MinAmbiguousWordLength { get; set; }
|
||||||
public int MinWordLength { get; set; }
|
public int MinWordLength { get; set; }
|
||||||
@ -15,14 +13,17 @@ public class UnbleeperSettings
|
|||||||
public class SearchDictionarySettings
|
public class SearchDictionarySettings
|
||||||
{
|
{
|
||||||
public string DictionaryPath { get; set; }
|
public string DictionaryPath { get; set; }
|
||||||
|
|
||||||
|
[Range(typeof(TimeSpan), "00:00:10", "01:00:00")]
|
||||||
|
public TimeSpan AutosavePeriod { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TelegramSettings
|
public class TelegramSettings
|
||||||
{
|
{
|
||||||
public string ApiKey { get; 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; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class AccessSettings
|
||||||
|
{
|
||||||
|
public string[] AllowedChats { get; set; }
|
||||||
|
}
|
||||||
@ -2,5 +2,9 @@
|
|||||||
global using System.Text;
|
global using System.Text;
|
||||||
global using System.Linq;
|
global using System.Linq;
|
||||||
global using System.Collections.Generic;
|
global using System.Collections.Generic;
|
||||||
|
global using System.Threading;
|
||||||
|
global using System.Threading.Tasks;
|
||||||
|
|
||||||
global using Microsoft.Extensions.Options;
|
global using Microsoft.Extensions.Options;
|
||||||
|
global using Microsoft.Extensions.Logging;
|
||||||
|
global using Microsoft.Extensions.Hosting;
|
||||||
|
|||||||
52
Jetsparrow.Aasb/Health/HealthUtils.cs
Normal file
52
Jetsparrow.Aasb/Health/HealthUtils.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
|
||||||
|
namespace Jetsparrow.Aasb.Health;
|
||||||
|
|
||||||
|
public static class HealthUtils
|
||||||
|
{
|
||||||
|
|
||||||
|
public static Task WriteHealthCheckResponse(HttpContext context, HealthReport healthReport)
|
||||||
|
{
|
||||||
|
context.Response.ContentType = "application/json; charset=utf-8";
|
||||||
|
|
||||||
|
var options = new JsonWriterOptions { Indented = true };
|
||||||
|
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
using (var jsonWriter = new Utf8JsonWriter(memoryStream, options))
|
||||||
|
{
|
||||||
|
jsonWriter.WriteStartObject();
|
||||||
|
jsonWriter.WriteString("status", healthReport.Status.ToString());
|
||||||
|
jsonWriter.WriteStartObject("results");
|
||||||
|
|
||||||
|
foreach (var healthReportEntry in healthReport.Entries)
|
||||||
|
{
|
||||||
|
jsonWriter.WriteStartObject(healthReportEntry.Key);
|
||||||
|
jsonWriter.WriteString("status",
|
||||||
|
healthReportEntry.Value.Status.ToString());
|
||||||
|
jsonWriter.WriteString("description",
|
||||||
|
healthReportEntry.Value.Description);
|
||||||
|
jsonWriter.WriteStartObject("data");
|
||||||
|
|
||||||
|
foreach (var item in healthReportEntry.Value.Data)
|
||||||
|
{
|
||||||
|
jsonWriter.WritePropertyName(item.Key);
|
||||||
|
|
||||||
|
JsonSerializer.Serialize(jsonWriter, item.Value,
|
||||||
|
item.Value?.GetType() ?? typeof(object));
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonWriter.WriteEndObject();
|
||||||
|
jsonWriter.WriteEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonWriter.WriteEndObject();
|
||||||
|
jsonWriter.WriteEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.Response.WriteAsync(
|
||||||
|
Encoding.UTF8.GetString(memoryStream.ToArray()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +1,13 @@
|
|||||||
using System.Threading;
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
using Jetsparrow.Aasb.Services;
|
||||||
|
|
||||||
namespace Jetsparrow.Aasb;
|
namespace Jetsparrow.Aasb.Health;
|
||||||
public class StartupHealthCheck : IHealthCheck
|
public class StartupHealthCheck : IHealthCheck
|
||||||
{
|
{
|
||||||
Aasb Bot { get; }
|
AntiAntiSwearingBot Bot { get; }
|
||||||
|
|
||||||
public StartupHealthCheck(Aasb bot)
|
public StartupHealthCheck(AntiAntiSwearingBot bot)
|
||||||
{
|
{
|
||||||
Bot = bot;
|
Bot = bot;
|
||||||
}
|
}
|
||||||
@ -3,6 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
|
<UseAppHost>False</UseAppHost>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -10,7 +11,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Telegram.Bot.Extensions.Polling" Version="2.0.0-alpha.1" />
|
<PackageReference Include="Telegram.Bot" Version="18.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -34,4 +35,8 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Properties\PublishProfiles\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Jetsparrow.Aasb;
|
namespace Jetsparrow.Aasb;
|
||||||
public static class Language
|
public static class StringEx
|
||||||
{
|
{
|
||||||
static int min(int a, int b, int c) { return Math.Min(Math.Min(a, b), c); }
|
static int min(int a, int b, int c) { return Math.Min(Math.Min(a, b), c); }
|
||||||
|
|
||||||
|
|||||||
@ -1,34 +1,37 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Jetsparrow.Aasb;
|
using Jetsparrow.Aasb;
|
||||||
|
using Jetsparrow.Aasb.Services;
|
||||||
|
using Jetsparrow.Aasb.Health;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder();
|
var builder = WebApplication.CreateBuilder();
|
||||||
|
Console.WriteLine("Configuring...");
|
||||||
builder.WebHost.ConfigureAppConfiguration((hostingContext, config) =>
|
|
||||||
{
|
|
||||||
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 cfg = builder.Configuration;
|
||||||
var svc = builder.Services;
|
var svc = builder.Services;
|
||||||
|
|
||||||
svc.Configure<SearchDictionarySettings>(cfg.GetSection("SearchDictionary"));
|
svc.AddOptions<SearchDictionarySettings>().BindConfiguration("SearchDictionary").ValidateDataAnnotations();
|
||||||
svc.Configure<TelegramSettings>(cfg.GetSection("Telegram"));
|
svc.AddOptions<TelegramSettings>().BindConfiguration("Telegram");
|
||||||
svc.Configure<UnbleeperSettings>(cfg.GetSection("Unbleeper"));
|
svc.AddOptions<UnbleeperSettings>().BindConfiguration("Unbleeper");
|
||||||
|
|
||||||
|
|
||||||
svc.AddHealthChecks().AddCheck<StartupHealthCheck>("Startup");
|
svc.AddHealthChecks().AddCheck<StartupHealthCheck>("Startup");
|
||||||
svc.AddHostedSingleton<SearchDictionary>();
|
svc.AddSingleton<SearchDictionary>();
|
||||||
svc.AddSingleton<Unbleeper>();
|
svc.AddSingleton<Unbleeper>();
|
||||||
svc.AddHostedSingleton<Aasb>();
|
svc.AddHostedSingleton<AntiAntiSwearingBot>();
|
||||||
|
|
||||||
|
Console.WriteLine("Building...");
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseEndpoints(cfg =>
|
app.UseEndpoints(cfg =>
|
||||||
{
|
{
|
||||||
cfg.MapHealthChecks("/health");
|
cfg.MapHealthChecks("/health");
|
||||||
|
cfg.MapHealthChecks("/health/verbose", new()
|
||||||
|
{
|
||||||
|
ResponseWriter = HealthUtils.WriteHealthCheckResponse
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Console.WriteLine("Running...");
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||||
-->
|
-->
|
||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PublishProtocol>FileSystem</PublishProtocol>
|
<DeleteExistingFiles>True</DeleteExistingFiles>
|
||||||
<Configuration>Release</Configuration>
|
<ExcludeApp_Data>False</ExcludeApp_Data>
|
||||||
<Platform>Any CPU</Platform>
|
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||||
<PublishDir>C:\Prog\releases\aasb</PublishDir>
|
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||||
<SelfContained>false</SelfContained>
|
<PublishProvider>FileSystem</PublishProvider>
|
||||||
<_IsPortable>true</_IsPortable>
|
<PublishUrl>C:\Prog\releases\aasb</PublishUrl>
|
||||||
|
<WebPublishMethod>FileSystem</WebPublishMethod>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@ -1,91 +0,0 @@
|
|||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
|
|
||||||
namespace Jetsparrow.Aasb;
|
|
||||||
public class SearchDictionary : BackgroundService
|
|
||||||
{
|
|
||||||
public SearchDictionary(IOptionsMonitor<SearchDictionarySettings> cfg)
|
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
using var guard = DictLock.GetWriteLockToken();
|
|
||||||
Changed = false;
|
|
||||||
|
|
||||||
var path = Cfg.CurrentValue.DictionaryPath;
|
|
||||||
var tmppath = path + ".tmp";
|
|
||||||
|
|
||||||
File.WriteAllLines(tmppath, words);
|
|
||||||
File.Move(tmppath, path, overwrite: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public record struct WordMatch (string Word, int Distance, int Rating);
|
|
||||||
|
|
||||||
public WordMatch Match(string pattern)
|
|
||||||
=> AllMatches(pattern).First();
|
|
||||||
|
|
||||||
public IEnumerable<WordMatch> AllMatches(string pattern)
|
|
||||||
{
|
|
||||||
using var guard = DictLock.GetReadLockToken();
|
|
||||||
return words
|
|
||||||
.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)
|
|
||||||
{
|
|
||||||
using var guard = DictLock.GetWriteLockToken();
|
|
||||||
Changed = true;
|
|
||||||
|
|
||||||
int index = words.IndexOf(word);
|
|
||||||
if (index > 0)
|
|
||||||
{
|
|
||||||
words.Move(index, 0);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
words.Insert(0, word);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Unlearn(string word)
|
|
||||||
{
|
|
||||||
using var guard = DictLock.GetWriteLockToken();
|
|
||||||
Changed = true;
|
|
||||||
return words.Remove(word);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
146
Jetsparrow.Aasb/Services/Aasb.cs
Normal file
146
Jetsparrow.Aasb/Services/Aasb.cs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
using Telegram.Bot;
|
||||||
|
using Telegram.Bot.Polling;
|
||||||
|
using Telegram.Bot.Types;
|
||||||
|
using Telegram.Bot.Types.Enums;
|
||||||
|
using Jetsparrow.Aasb.Commands;
|
||||||
|
|
||||||
|
namespace Jetsparrow.Aasb.Services;
|
||||||
|
|
||||||
|
public class AntiAntiSwearingBot : IHostedService
|
||||||
|
{
|
||||||
|
SearchDictionary Dict { get; }
|
||||||
|
Unbleeper Unbleeper { get; }
|
||||||
|
IOptionsMonitor<AccessSettings> AccessCfg { get; }
|
||||||
|
ILogger Log { get; }
|
||||||
|
public bool Started { get; private set; } = false;
|
||||||
|
|
||||||
|
public AntiAntiSwearingBot(
|
||||||
|
ILogger<AntiAntiSwearingBot> log,
|
||||||
|
IOptions<TelegramSettings> tgCfg,
|
||||||
|
Unbleeper unbp,
|
||||||
|
IOptionsMonitor<AccessSettings> accessCfg)
|
||||||
|
{
|
||||||
|
Log = log;
|
||||||
|
TelegramSettings = tgCfg.Value;
|
||||||
|
Unbleeper = unbp;
|
||||||
|
AccessCfg = accessCfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
TelegramSettings TelegramSettings { get; }
|
||||||
|
TelegramBotClient TelegramBot { get; set; }
|
||||||
|
ChatCommandRouter Router { get; set; }
|
||||||
|
public User Me { get; private set; }
|
||||||
|
|
||||||
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(TelegramSettings.ApiKey))
|
||||||
|
{
|
||||||
|
Log.LogWarning("No ApiKey found in config!");
|
||||||
|
throw new Exception("No ApiKey found in config!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TelegramBot = new TelegramBotClient(TelegramSettings.ApiKey);
|
||||||
|
|
||||||
|
Log.LogInformation("Connecting to Telegram...");
|
||||||
|
Me = await TelegramBot.GetMeAsync();
|
||||||
|
Log.LogInformation("Connected to Telegram as @{Username}", Me.Username);
|
||||||
|
Router = new ChatCommandRouter(Me.Username, AccessCfg);
|
||||||
|
Router.Register(new LearnCommand(Dict), "learn");
|
||||||
|
Router.Register(new UnlearnCommand(Dict), "unlearn");
|
||||||
|
Router.Register(new IdCommand(), "chatid");
|
||||||
|
|
||||||
|
var receiverOptions = new ReceiverOptions { AllowedUpdates = new[] { UpdateType.Message } };
|
||||||
|
TelegramBot.StartReceiving(
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Log.LogError(exception, "Exception while handling API message");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task HandleUpdateAsync(ITelegramBotClient sender, Update update, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (update.Type != UpdateType.Message || update?.Message?.Type != MessageType.Text)
|
||||||
|
return;
|
||||||
|
var msg = update.Message!;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (TryParseCommand(update, out var cmd))
|
||||||
|
{
|
||||||
|
var cmdResponse = Router.Execute(cmd);
|
||||||
|
|
||||||
|
if (cmdResponse != null)
|
||||||
|
{
|
||||||
|
await TelegramBot.SendTextMessageAsync(
|
||||||
|
msg.Chat.Id,
|
||||||
|
cmdResponse,
|
||||||
|
replyToMessageId: msg.MessageId,
|
||||||
|
parseMode: ParseMode.MarkdownV2,
|
||||||
|
disableNotification: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var unbleepResponse = await Unbleeper.UnbleepSwears(msg.Text);
|
||||||
|
if (unbleepResponse != null)
|
||||||
|
await TelegramBot.SendTextMessageAsync(
|
||||||
|
msg.Chat.Id,
|
||||||
|
unbleepResponse,
|
||||||
|
replyToMessageId: msg.MessageId,
|
||||||
|
disableNotification: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.LogError(e, "Exception while handling message {0}", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const string NS_TELEGRAM = "telegram://";
|
||||||
|
static readonly char[] WS_CHARS = new[] { ' ', '\r', '\n', '\n' };
|
||||||
|
|
||||||
|
bool TryParseCommand(Update update, out CommandContext result)
|
||||||
|
{
|
||||||
|
var s = update.Message.Text;
|
||||||
|
result = null;
|
||||||
|
if (string.IsNullOrWhiteSpace(s) || s[0] != '/')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
string[] words = s.Split(WS_CHARS, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
var cmdRegex = new Regex(@"/(?<cmd>\w+)(@(?<name>\w+))?");
|
||||||
|
var match = cmdRegex.Match(words.First());
|
||||||
|
if (!match.Success)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
string cmd = match.Groups["cmd"].Captures[0].Value;
|
||||||
|
string username = match.Groups["name"].Captures.Count > 0 ? match.Groups["name"].Captures[0].Value : null;
|
||||||
|
string[] parameters = words.Skip(1).ToArray();
|
||||||
|
|
||||||
|
result = new CommandContext
|
||||||
|
{
|
||||||
|
Command = cmd,
|
||||||
|
ChatId = NS_TELEGRAM + update.Message.Chat.Id,
|
||||||
|
SenderId = NS_TELEGRAM + update.Message.From.Id,
|
||||||
|
Recipient = username,
|
||||||
|
Parameters = parameters
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
Jetsparrow.Aasb/Services/SearchDictionary.cs
Normal file
109
Jetsparrow.Aasb/Services/SearchDictionary.cs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Jetsparrow.Aasb.Services;
|
||||||
|
|
||||||
|
public record struct WordMatch(string Word, int Distance);
|
||||||
|
|
||||||
|
public class SearchDictionary
|
||||||
|
{
|
||||||
|
SearchDictionarySettings Cfg { get; }
|
||||||
|
UnbleeperSettings UnbleeperCfg { get; }
|
||||||
|
Regex LegalWordsRegex { get; }
|
||||||
|
|
||||||
|
public SearchDictionary(
|
||||||
|
IOptions<SearchDictionarySettings> searchDictionaryCfg,
|
||||||
|
IOptions<UnbleeperSettings> unbleeperCfg,
|
||||||
|
IHostApplicationLifetime lifetime)
|
||||||
|
{
|
||||||
|
Cfg = searchDictionaryCfg.Value;
|
||||||
|
UnbleeperCfg = unbleeperCfg.Value;
|
||||||
|
Loaded = Load();
|
||||||
|
Autosave = AutosaveLoop(lifetime.ApplicationStopping);
|
||||||
|
LegalWordsRegex = new Regex(UnbleeperCfg.LegalWordsRegex, RegexOptions.Compiled);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<WordMatch> Match(string pattern)
|
||||||
|
{
|
||||||
|
await Loaded;
|
||||||
|
var matches = TopMatches(pattern);
|
||||||
|
return matches[Random.Shared.Next(0, matches.Count)];
|
||||||
|
}
|
||||||
|
|
||||||
|
IList<WordMatch> TopMatches(string pattern)
|
||||||
|
{
|
||||||
|
pattern = pattern.ToLowerInvariant();
|
||||||
|
List<WordMatch> matches;
|
||||||
|
using (var guard = DictLock.GetReadLockToken())
|
||||||
|
{
|
||||||
|
matches = words
|
||||||
|
.Select((w, i) => new WordMatch(w, StringEx.LevenshteinDistance(pattern, w)))
|
||||||
|
.ToList();
|
||||||
|
};
|
||||||
|
var minDist = matches.Min(w => w.Distance);
|
||||||
|
return matches.Where(w => w.Distance == minDist).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LearnResult { Illegal, Added, Known }
|
||||||
|
public LearnResult Learn(string word)
|
||||||
|
{
|
||||||
|
using var guard = DictLock.GetWriteLockToken();
|
||||||
|
if (words.Contains(word))
|
||||||
|
return LearnResult.Known;
|
||||||
|
|
||||||
|
if (!LegalWordsRegex.IsMatch(word))
|
||||||
|
return LearnResult.Illegal;
|
||||||
|
|
||||||
|
words.Add(word);
|
||||||
|
Changed = true;
|
||||||
|
return LearnResult.Added;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Unlearn(string word)
|
||||||
|
{
|
||||||
|
using var guard = DictLock.GetWriteLockToken();
|
||||||
|
var res = words.Remove(word);
|
||||||
|
Changed |= res;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReaderWriterLockSlim DictLock = new();
|
||||||
|
List<string> words;
|
||||||
|
|
||||||
|
#region load/save
|
||||||
|
Task Loaded, Autosave;
|
||||||
|
bool Changed;
|
||||||
|
async Task Load()
|
||||||
|
{
|
||||||
|
words = new List<string>(await File.ReadAllLinesAsync(Cfg.DictionaryPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task AutosaveLoop(CancellationToken stopToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!stopToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(Cfg.AutosavePeriod, stopToken);
|
||||||
|
DoSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException) { }
|
||||||
|
DoSave();
|
||||||
|
}
|
||||||
|
void DoSave()
|
||||||
|
{
|
||||||
|
if (!Changed) return;
|
||||||
|
using (var guard = DictLock.GetWriteLockToken())
|
||||||
|
{
|
||||||
|
if (!Changed) return;
|
||||||
|
Changed = false;
|
||||||
|
var tmppath = Cfg.DictionaryPath + ".tmp";
|
||||||
|
File.WriteAllLines(tmppath, words);
|
||||||
|
File.Move(tmppath, Cfg.DictionaryPath, overwrite: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Jetsparrow.Aasb;
|
namespace Jetsparrow.Aasb.Services;
|
||||||
|
|
||||||
public class Unbleeper
|
public class Unbleeper
|
||||||
{
|
{
|
||||||
SearchDictionary Dict { get; }
|
SearchDictionary Dict { get; }
|
||||||
@ -11,43 +10,45 @@ public class Unbleeper
|
|||||||
{
|
{
|
||||||
Dict = dict;
|
Dict = dict;
|
||||||
Cfg = cfg.Value;
|
Cfg = cfg.Value;
|
||||||
BleepedSwearsRegex = new Regex("^" + Cfg.BleepedSwearsRegex + "$", RegexOptions.Compiled);
|
var toBleep = Cfg.BleepedSwearsRegex;
|
||||||
|
|
||||||
|
if (!toBleep.StartsWith('^')) toBleep = "^" + toBleep;
|
||||||
|
if (!toBleep.EndsWith('$')) toBleep = toBleep + "$";
|
||||||
|
BleepedSwearsRegex = new Regex(toBleep, RegexOptions.Compiled);
|
||||||
}
|
}
|
||||||
|
|
||||||
Regex BleepedSwearsRegex { get; }
|
Regex BleepedSwearsRegex { get; }
|
||||||
|
|
||||||
static readonly char[] WORD_SEPARATORS = { ' ', '\t', '\r', '\n', '.', ',', '!', '?', ';', ':', '-', '—' };
|
static readonly char[] WORD_SEPARATORS = { ' ', '\t', '\r', '\n', '.', ',', '!', '?', ';', ':', '-', '—' };
|
||||||
|
|
||||||
public string UnbleepSwears(string text)
|
public async Task<string> UnbleepSwears(string text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
text = text.Trim();
|
text = text.Trim();
|
||||||
|
|
||||||
if (text.StartsWith('/')) // is chat command
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var words = text.Split(WORD_SEPARATORS, StringSplitOptions.RemoveEmptyEntries);
|
var words = text.Split(WORD_SEPARATORS, StringSplitOptions.RemoveEmptyEntries);
|
||||||
var candidates = words
|
var candidates = words
|
||||||
.Where(w =>
|
.Where(w =>
|
||||||
!Language.IsTelegramMention(w)
|
StringEx.HasNonWordChars(w)
|
||||||
&& !Language.IsEmailPart(w)
|
&& (StringEx.HasWordChars(w) || w.Length >= Cfg.MinAmbiguousWordLength)
|
||||||
&& Language.HasNonWordChars(w)
|
|
||||||
&& !Language.IsHashTag(w)
|
|
||||||
&& (Language.HasWordChars(w) || w.Length >= Cfg.MinAmbiguousWordLength)
|
|
||||||
&& w.Length >= Cfg.MinWordLength
|
&& w.Length >= Cfg.MinWordLength
|
||||||
|
&& !StringEx.IsTelegramMention(w)
|
||||||
|
&& !StringEx.IsEmailPart(w)
|
||||||
|
&& !StringEx.IsHashTag(w)
|
||||||
&& BleepedSwearsRegex.IsMatch(w)
|
&& BleepedSwearsRegex.IsMatch(w)
|
||||||
)
|
)
|
||||||
.ToArray();
|
.ToList();
|
||||||
|
|
||||||
if (!candidates.Any())
|
if (!candidates.Any())
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var response = new StringBuilder();
|
var response = new StringBuilder();
|
||||||
for (int i = 0; i < candidates.Length; ++i)
|
for (int i = 0; i < candidates.Count; ++i)
|
||||||
{
|
{
|
||||||
var m = Dict.Match(candidates[i]);
|
var m = await Dict.Match(candidates[i]);
|
||||||
response.AppendLine(new string('*', i + 1) + m.Word + new string('?', m.Distance));
|
response.AppendLine(new string('*', i + 1) + m.Word + new string('?', m.Distance));
|
||||||
}
|
}
|
||||||
return response.ToString();
|
return response.ToString();
|
||||||
@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
"Endpoints": {
|
"Endpoints": {
|
||||||
"Http": {
|
"Http": {
|
||||||
@ -7,12 +13,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Unbleeper": {
|
"Unbleeper": {
|
||||||
|
"LegalWordsRegex": "[а-яА-ЯёЁ]+",
|
||||||
"BleepedSwearsRegex": "[а-яА-ЯёЁ@\\*#]+",
|
"BleepedSwearsRegex": "[а-яА-ЯёЁ@\\*#]+",
|
||||||
"MinWordLength": 3,
|
"MinWordLength": 3,
|
||||||
"MinAmbiguousWordLength": 5
|
"MinAmbiguousWordLength": 5
|
||||||
},
|
},
|
||||||
"SearchDictionary": {
|
"SearchDictionary": {
|
||||||
"DictionaryPath": "dict/ObsceneDictionaryRu.txt"
|
"DictionaryPath": "dict/ObsceneDictionaryRu.txt",
|
||||||
|
"AutosavePeriod": "00:01:00"
|
||||||
},
|
},
|
||||||
"Telegram": {
|
"Telegram": {
|
||||||
"UseProxy": false
|
"UseProxy": false
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user