diff --git a/Jetsparrow.Aasb.Tests/BleepTestsBase.cs b/Jetsparrow.Aasb.Tests/BleepTestsBase.cs new file mode 100644 index 0000000..81585e4 --- /dev/null +++ b/Jetsparrow.Aasb.Tests/BleepTestsBase.cs @@ -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 dictCfg { get; } + protected IOptions ublCfg { get; } + protected IHostApplicationLifetime lifetime { get; } +} \ No newline at end of file diff --git a/Jetsparrow.Aasb.Tests/DetectTests.cs b/Jetsparrow.Aasb.Tests/DetectTests.cs index 163018b..823e8f8 100644 --- a/Jetsparrow.Aasb.Tests/DetectTests.cs +++ b/Jetsparrow.Aasb.Tests/DetectTests.cs @@ -1,29 +1,13 @@ -using System; +namespace Jetsparrow.Aasb.Tests; -using Microsoft.Extensions.Options; - -using Xunit; - -namespace Jetsparrow.Aasb.Tests; - -public class DetectTests +public class DetectTests : BleepTestsBase { - Unbleeper ubl { get; } - SearchDictionary dict { get; } - - public DetectTests() - { - - dict = new SearchDictionary(MockOptionsMonitor.Create(DefaultSettings.SearchDictionary)); - ubl = new Unbleeper(dict, Options.Create(DefaultSettings.Unbleeper)); - } - [Theory] [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); } @@ -34,9 +18,9 @@ public class DetectTests [InlineData("еб*ть—колотить", "*ебать")] [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); } } diff --git a/Jetsparrow.Aasb.Tests/FilterTests.cs b/Jetsparrow.Aasb.Tests/FilterTests.cs index b3290e7..380b78a 100644 --- a/Jetsparrow.Aasb.Tests/FilterTests.cs +++ b/Jetsparrow.Aasb.Tests/FilterTests.cs @@ -1,20 +1,6 @@ -using Microsoft.Extensions.Options; - -using Xunit; - -namespace Jetsparrow.Aasb.Tests; - -public class FilterTests +namespace Jetsparrow.Aasb.Tests; +public class FilterTests : BleepTestsBase { - Unbleeper ubl { get; } - SearchDictionary dict { get; } - - public FilterTests() - { - dict = new SearchDictionary(MockOptionsMonitor.Create(DefaultSettings.SearchDictionary)); - ubl = new Unbleeper(dict, Options.Create(DefaultSettings.Unbleeper)); - } - [Theory] [InlineData("*")] [InlineData("**#")] diff --git a/Jetsparrow.Aasb.Tests/GlobalUsings.cs b/Jetsparrow.Aasb.Tests/GlobalUsings.cs new file mode 100644 index 0000000..819d559 --- /dev/null +++ b/Jetsparrow.Aasb.Tests/GlobalUsings.cs @@ -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; \ No newline at end of file diff --git a/Jetsparrow.Aasb.Tests/Jetsparrow.Aasb.Tests.csproj b/Jetsparrow.Aasb.Tests/Jetsparrow.Aasb.Tests.csproj index adf9ac3..9ecd36d 100644 --- a/Jetsparrow.Aasb.Tests/Jetsparrow.Aasb.Tests.csproj +++ b/Jetsparrow.Aasb.Tests/Jetsparrow.Aasb.Tests.csproj @@ -16,4 +16,10 @@ + + + Always + + + diff --git a/Jetsparrow.Aasb.Tests/Utils/FakeLifetime.cs b/Jetsparrow.Aasb.Tests/Utils/FakeLifetime.cs new file mode 100644 index 0000000..3e6eed9 --- /dev/null +++ b/Jetsparrow.Aasb.Tests/Utils/FakeLifetime.cs @@ -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(); + } +} diff --git a/Jetsparrow.Aasb.Tests/MockOptionsMonitor.cs b/Jetsparrow.Aasb.Tests/Utils/MockOptionsMonitor.cs similarity index 100% rename from Jetsparrow.Aasb.Tests/MockOptionsMonitor.cs rename to Jetsparrow.Aasb.Tests/Utils/MockOptionsMonitor.cs diff --git a/Jetsparrow.Aasb.Tests/dict/ObsceneDictionaryRu.txt b/Jetsparrow.Aasb.Tests/dict/ObsceneDictionaryRu.txt new file mode 100644 index 0000000..3687723 --- /dev/null +++ b/Jetsparrow.Aasb.Tests/dict/ObsceneDictionaryRu.txt @@ -0,0 +1,1315 @@ +бля +хуй +блядь +пиздец +пидр +пидор +охуеть +бздун +беспезды +блудилище +блядво +блядеха +блядина +блядистка +блядище +блядки +блядование +блядовать +блядовитый +блядовозка +блядолиз +блядоход +блядский +блядство +блядствовать +блядун +бляди +бляд +блядюга +блядюра +блядюшка +блядюшник +бордель +вагина +вафлист +вжопить +вжопиться +вздрачивание +вздрачивать +вздрачиваться +вздрочить +вздрочиться +вздрючить +вздрючивание +вздрючивать +взъебка +взъебщик +взъебнуть +влагалище +вхуйнуть +вхуйнуться +вхуякать +вхуякаться +вхуя +вхуякивать +вхуякиваться +вхуякнуть +вхуякнуться +вхуяривание +вхуяривать +вхуяриваться +вхуярить +вхуяриться +вхуячивание +вхуячивать +вхуячиваться +вхуячить +вхуячиться +вхуяшивать +вхуяшиваться +вхуяшить +вхуяшиться +въебать +въебаться +въебашивать +въебашиваться +въебашить +въебашиться +въебенивать +въебениваться +въебенить +въебениться +выблядок +выебанный +выебат +выебаться +выебнулся +высрать +высраться +выссать +выссаться +высераться +выссереть +говнецо +говнистый +говниться +говно +говновоз +говнодав +говноеб +говноед +говномес +говномер +говносерка +говнюк +голожопая +гомик +гомосек +гондон +гонорея +давалка +двужопник +дерьмо +дерьмоед +дерьмовый +дилдо +додрочить +додрочиться +доебать +доебаться +доебенивать +доебениваться +доебенить +доебениться +долбоеб +допиздить +допиздиться +допиздовать +допиздоваться +допиздовывать +допиздовываться +допиздохать +допиздохаться +допиздохивать +допиздохиваться +допиздошить +допиздошиться +допиздошивать +допиздошиваться +допиздюлить +допиздюлиться +допиздюливать +допиздюливаться +допиздюрить +допиздюриться +допиздюривать +допиздюриваться +допиздюхать +допиздюхаться +допиздюхивать +допиздюхиваться +допиздякать +допиздякаться +допиздякивать +допиздякиваться +допиздярить +допиздяриться +допиздяривать +допиздяриваться +допиздяхать +допиздяхаться +допиздяхивать +допиздяхиваться +допиздячить +допиздячиться +допиздячивать +допиздячиваться +допиздяшить +допиздяшиться +допиздяшивать +допиздяшиваться +допиздоболивать +допиздоболиваться +допиздоболиться +допиздюкать +допиздюкаться +допиздюкивать +допиздюкиваться +допизживать +дотрахать +дотрахаться +дохуйнуть +дохуякать +дохуякаться +дохуякивать +дохуякиваться +дохуяривать +дохуяриваться +дохуярить +дохуяриться +дохуячить +дохуячиться +дохуячивать +дохуячиваться +дрисня +дристать +дристун +дроченье +дрочилыцик +дрочить +дрочиться +дрочка +дрючить +дрючиться +дурак +дуроеб +выебать +ебало +ебальник +ебальные +ебальный +ебанатик +ебанашка +ебанутый +ебануть +ебануться +ебать +ебат +ебаться +ебатьс +ебашил +ебитесь +ебло +еблом +еблысь +ебля +ебнуть +ебнуться +ебня +ебучий +заебла +надроченный +объебешь +поебать +жирнозадый +жопа +жопой +жопастая +жопоеб +жопенци +жопища +жопка +жопник +жополиз +жополизание +жопоногий +жопочка +жопочник +жопство +жопу +забздеть +заблядовать +заблядоваться +задница +задрачивать +задрачиваться +задроченный +задрочить +задрочиться +задрючить +задрючиться +заебанный +заебать +заебаться +заебательская +заебашивать +заебашиваться +заебашить +заебашиться +заебенивать +заебениваться +заебенить +заебениться +залупа +залупу +залупаться +залупенить +залупень +залупить +залупляться +залупистый +запиздарить +запизденная +запизденелый +запиздить +запиздиться +запиздоболивать +запиздоболиваться +запиздоболить +запиздоболиться +запиздовать +запиздоваться +запиздовывать +запиздовываться +запиздохать +запиздошить +запиздошиться +запиздошивать +запиздошиваться +запиздюкать +запиздюкаться +запиздюкивать +запиздюкиваться +запиздюлить +запиздюлиться +запиздюливать +запиздюливаться +запиздюрить +запиздюриться +запиздюривать +запиздюриваться +запиздюхать +запиздюхаться +запиздюхивать +запиздюхиваться +запиздючить +запиздючиться +запиздючивать +запиздючиваться +засранец +засранка +засранный +засратый +засрать +засраться +зассать +затраханный +затрахать +затрахаться +затрахивать +затрахиваться +захуить +захуйнуть +захуйнуться +захуякать +захуякаться +захуякивать +захуякиваться +захуярить +захуяриться +захуяривать +захуяриваться +захуячить +захуячиться +захуячивать +захуячиваться +захуяшить +захуяшиться +захуяшивать +захуяшиваться +злоебучий +издрочиться +измандить +измандиться +измандовать +измандоваться +измандовывать +измандовываться +изъебать +изъебаться +изъебашить +изъебашиться +изъебашивать +изъебашиваться +изъебенить +изъебениться +изъебенивать +изъебениваться +изъеб +испиздеться +испиздить +испражнение +испражняться +исхуякать +исхуякаться +исхуякивать +исхуякиваться +исхуярить +исхуяриться +исхуяривать +какать +какашка +кастрат +кастрировать +клитор +клоака +кнахт +кончить +косоебить +косоебиться +кривохуй +курва +курвиный +лахудра +лох +лохудра +лохматка +манда +мандавошка +мандавоха +мандить +мандиться +мандоватая +мандовать +мандохать +мандохаться +мандохивать +мандохиваться +мандошить +мастурбатор +минет +минетить +минетка +минетчик +минетчица +мозгоеб +мозгоебатель +мозгоебать +мозгоебка +мокрожопый +мокропиздая +моча +мочиться +мудак +мудашвили +мудило +мудильщик +мудистый +мудить +мудоеб +наебанный +наебка +наебщик +наебывать +наебываться +наебыш +набздеть +наблядоваться +надрочивать +надрочить +надрочиться +надристать +наебать +наебаться +наебнуть +наебнуться +накакать +накакаться +накакивать +напиздить +напиздошить +напиздюрить +напиздюриться +насрать +насраться +нассать +нассаться +натрахать +натрахаться +натрахивать +натрахиваться +нахуй +нахуякать +нахуякаться +нахуякивать +нахуякиваться +нахуярить +нахуяриться +нахуяривать +нахуяриваться +нахуячить +нахуячиться +нахуячивать +нахуячиваться +нахуяшить +недоебанный +недоносок +неебущий +нищеебство +оебыват +обдристанный +обдристать +обдрочиться +обосранец +обосранная +обосраный +обосрать +обосраться +обоссанец +обоссаный +обоссать +обоссаться +обоссывать +обоссываться +пизду +обпиздить +обпиздиться +обпиздовать +обпиздоваться +обпиздовывать +обпиздовываться +обпиздохать +обпиздохаться +обпиздохивать +обпиздохиваться +обпиздошить +обтрахать +обтрахаться +обтрахивать +обтрахиваться +обхуярить +обхуяриться +обхуячить +объебать +объебаться +объебенить +объебнуть +объебон +одинхуй +однапизда +однохуйственно +оебать +оебашивать +оебашить +оебенивать +оебенить +опедерастить +опизденеть +опизденный +опизденно +опиздеть +опиздить +остоебеть +остоебенить +остоебенило +остопиздеть +остопиздело +остохуело +остохуеть +отдрачивать +отдрачиваться +отдрочить +отдрочиться +отпиздить +отпиздошить +отпиздяшить +отпиздяшиться +отпиздяшивание +отпиздяшивать +отпиздяшиваться +отсасывать +отсасываться +отсосать +отсосаться +оттраханная +оттрахать +оттрахаться +оттрахивать +оттрахиваться +отхерачить +отхуякать +отхуякаться +отхуякивать +отхуякиваться +отхуярить +отхуяриться +отхуяривать +отхуяриваться +отхуячить +отхуячиться +отхуячивать +отхуячиваться +отхуяшить +отхуяшиться +отхуяшивать +отхуяшиваться +отъебать +отъебывание +отъебывать +отъебываться +отъебашить +отъебашивание +отъебашивать +отъебашиваться +отъебенить +отъебениться +отъебенивать +отъебениваться +отъебнуть +отьебаться +отьебашиться +отьебенивание +отьебнуться +охуевать +охуевающий +охуевший +охуение +охуенно +охуенные +охуительно +охуительный +охуякать +охуякаться +охуякивать +охуякиваться +охуякнуть +охуякнуться +охуярить +охуяриться +охуяривать +охуяриваться +охуячить +охуячиться +охуячивать +охуячиваться +охуяшить +охуяшиться +охуяшивать +охуяшиваться +очко +перднуть +падла +падлюка +педераст +педерастина +педерастический +педерастия +педик +педрило +пежить +пенис +пердеж +пердеть +пердун +перебздеть +передрачивать +передрочить +передрочиться +переебаться +переебашить +перетрахать +перетрахаться +перетрахивать +перетрахиваться +перехуйнуть +перехуйнуться +перехуякнуть +перехуякнуться +перехуякать +перехуякаться +перехуякивать +перехуякиваться +перехуярить +перехуяриться +перехуяривать +перехуяриваться +перехуячить +юнити +перехуячиться +наебениться +перехуячивать +пидорас +пизда +пизданутая +пиздануть +пиздануться +пиздато +пизденка +пизденочка +пиздень +пизденыш +пиздеть +пиздища +пиздобол +пиздовать +пиздолиз +пиздомол +пиздосос +пиздоход +пиздуй +пиздун +пиздюга +пиздюлей +пиздюли +пиздюлина +пиздюк +пиздюкать +пиздюкаться +пиздюшка +пиздякать +пиздятина +пиздятиной +пиздячий +писька +писюлек +плоскозадая +поебочка +поебывать +поебываться +поблудить +поблядовать +поблядушка +подосрать +подосраться +подоссать +подпиздить +подпиздовать +подпиздоваться +подпиздовывать +подпиздовываться +подпиздохать +подпиздохаться +подпиздохивать +подпиздохиваться +подпиздошить +подпиздошиться +подпиздошивать +подпиздякать +подпиздякаться +подпиздякивать +подпиздякиваться +подпиздярить +подпиздяриться +подпиздяривать +подпиздяриваться +подпиздяхать +подпиздяхаться +подпиздяхивать +подпиздяхиваться +подпиздячить +подпиздячиться +подпиздячивать +подпиздячиваться +подпиздяшить +подпиздяшиться +подпиздяшивать +подпиздяшиваться +подристывать +подрочить +подсирать +подхуякнуть +подхуякнуться +подхуякать +подхуякаться +подхуякивать +подхуякиваться +подхуярить +подхуяриться +подхуяривать +подхуяриваться +подхуячивать +подхуячиться +подхуячиваться +подхуяшить +подхуяшиться +подхуяшивать +подхуяшиваться +подъеб +подъебать +подъебаться +подъебашить +подъебнуть +подъебка +подъебывать +подъябывать +поебанный +поебаться +поебень +поебистика +поебон +поебончик +попердеть +попердеться +попердывать +попизденная +попиздеть +попиздистее +попиздить +попиздиться +попиздоватей +попиздоболивать +попиздоболиваться +попиздоболить +попиздоболиться +попиздовать +попиздоваться +попиздовывать +попиздовываться +попиздохать +попиздохаться +попиздохивать +попиздохиваться +попиздошить +попиздошиться +попиздошивать +попиздошиваться +попиздюкать +попиздюкаться +попиздюкивать +попиздюкиваться +попиздюлить +попиздюлиться +попиздюливать +попиздюливаться +попиздюрить +попиздюриться +попиздюривать +попиздюриваться +попиздюхать +попиздюхаться +попиздюхивать +попиздюхиваться +попиздякать +попиздякаться +попиздякивать +попиздякиваться +попиздярить +попиздяриться +попиздяривать +попиздяриваться +попиздяхать +попиздяхаться +попиздяхивать +попиздяхиваться +попиздячить +попиздячиться +попиздячивать +попиздячиваться +попиздяшить +попиздяшиться +попиздяшивать +попиздяшиваться +попизживать +попизживаться +потаскун +потаскуха +потраханная +потрахать +потрахаться +потрахивать +потрахиваться +похер +похуист +похуякать +похуякаться +похуякивать +похуякиваться +похуярить +похуяриться +похуяривать +похуяриваться +похуячить +похуячиться +похуячивать +похуячиваться +похуяшить +похуяшиться +похуяшивать +похуяшиваться +поц +пошмариться +поябывать +приебать +приебаться +приебывать +приебываться +приебашить +приебашиться +приебашивать +приебашиваться +приебенить +приебениться +приебенивать +приебениваться +приебехать +приебехаться +приебехивать +приебехиваться +приебистый +приебурить +приебуриться +приебуривать +приебуриваться +прижопить +прижопывать +прикинуть +примандовать +примандоваться +примавдовывать +примандовываться +примандохать +примандохаться +примандохивать +примандохиваться +примандошить +примандошиться +примандошивать +примандошиваться +примандюкать +примандюкаться +примандюкивать +примандюкиваться +примандехать +примандехаться +примандехивать +примандехиваться +примандюлить +примандюлиться +примандюливать +примандюливаться +примандюрить +примандюриться +примандюривать +примандюриваться +примандякать +примандякаться +примандякивать +примандякиваться +примандярить +примандяриться +примандяривать +примандяриваться +примандяхать +примандяхаться +примандяхивать +примандяхиваться +примандячить +примандячиться +примандячивать +примандячиваться +примандяшить +примандяшиться +примандяшивать +примандяшиваться +примудохать +примудохаться +примудохивать +примудохиваться +примандить +примандиться +припизденный +припиздень +припиздить +припиздиться +припиздывать +припиздываться +припиздовать +припиздоваться +припиздовывать +припиздовываться +припиздохать +припиздохаться +припиздохивать +припиздохиваться +припиздошить +припиздошиться +припиздошивать +припиздошиваться +припиздюкать +припиздюкаться +припиздюкивать +припиздюкиваться +припиздюлить +припиздюлиться +припиздюливать +припиздюливаться +припиздюрить +припиздюриться +припиздюривать +припиздюхать +припиздюриваться +припиздюхаться +припиздюхивать +припиздюхиваться +припиздякать +припиздякаться +припиздякивать +припиздякиваться +припиздярить +припиздяриться +припиздяривать +припиздяриваться +припиздяхать +припиздяхаться +припиздяхивать +припиздяхиваться +припиздячить +припиздячиться +припиздячивать +припиздячиваться +припиздяшить +припиздяшиться +припиздяшивать +припиздяшиваться +припиздронить +припиздрониться +припиздронивать +припиздрониваться +припизживать +припизживаться +прихуеть +прихуякать +прихуякаться +прихуякивать +прихуякиваться +прихуярить +прихуяриться +прихуяривать +прихуяриваться +прихуячить +прихуячиться +прихуячивать +прихуячиваться +прихуяшить +прихуяшиться +прихуяшивать +прихуяшиваться +притрахаться +проблядовать +проблядь +проблядушка +продрачивать +продрачиваться +продрочить +продрочиться +проебать +проебаться +проебашить +проебашиться +проебашивать +проебашиваться +проебенить +проебениться +проебывать +проебываться +пропиздить +пропиздиться +пропиздоболивать +пропиздоболиваться +пропиздоболить +пропиздоболиться +пропиздовать +пропиздоваться +пропиздовывать +пропиздовываться +пропиздохать +пропиздохаться +пропиздохивать +пропиздохиваться +пропиздошить +пропиздошиться +пропиздошивать +пропиздошиваться +пропиздюкать +пропиздюкаться +пропиздюкивать +пропиздюкиваться +пропиздюлить +пропиздюлиться +пропиздюливать +пропиздюливаться +пропиздюрить +пропиздюриться +пропиздюривать +пропиздюриваться +пропиздюхать +пропиздюхаться +пропиздюхивать +пропиздюхиваться +пропиздякать +пропиздякаться +пропиздякивать +пропиздякиваться +пропиздярить +пропиздяриться +пропиздяривать +пропиздяриваться +пропиздяхать +пропиздяхивать +пропиздяхиваться +пропиздячить +пропиздячиться +пропиздячивать +пропиздячиваться +пропиздяшить +пропиздяшиться +пропиздяшивать +пропиздяшиваться +пропизживать +пропизживаться +пропиздон +прохуякать +прохуякаться +прохуякивать +прохуякиваться +прохуярить +прохуяриться +прохуяривать +прохуяриваться +прохуячить +прохуячиться +прохуячивать +прохуячиваться +прохуяшить +прохуяшиться +прохуяшивать +прохуяшиваться +разблядоваться +раздрочить +раздрочиться +раззалупаться +разнохуйственно +разъебать +разъебаться +разъебашить +разъебашиться +разъебашивать +разъебашиваться +разъебенить +разъебениться +разъебенивать +разъебениваться +распиздить +распиздиться +распиздовать +распиздоваться +распиздовывать +распиздовываться +распиздохать +распиздохаться +распиздохивать +распиздохиваться +распиздошить +распиздошиться +распиздошивать +распиздошиваться +распиздон +распиздяй +расхуярить +расхуяриться +расхуяривать +расхуяриваться +расхуячить +расхуячиться +расхуячивать +расхуячиваться +сдрочить +сестроеб +сифилитик +сифилюга +скурвиться +смандить +смандиться +сперматозавр +спиздеть +стерва +стервоза +сука +суки +сукин +сукины +суходрочка +суходрочкой +сучара +сучий +сучка +сучье +схуякать +схуякаться +схуякивать +схуякиваться +схуярить +схуяриться +схуяривать +схуяриваться +схуячить +схуячиться +схуячивать +съебывать +съебываться +съебать +съебаться +съебашить +съебашиться +съебашивать +съебашиваться +съебенить +съебениться +съебенивать +спиздить +тварь +толстожопый +толстозадая +торчило +траханье +трахать +трахаться +трахнуть +трахнуться +трепак +триппер +уебывать +уебываться +уебыш +ублюдок +уебать +уебашить +уебашивать +уебенить +уебище +усраться +усрачка +уссать +уссаться +ухуякать +ухуякаться +ухуякивать +ухуякиваться +ухуярить +ухуяриться +ухуяривать +ухуяриваться +ухуячить +ухуячиться +ухуячивать +ухуячиваться +ухуяшить +ухуяшиться +ухуяшивать +ухуяшиваться +фаллос +фекал +фекалий +фекалии +хер +херами +херня +херовина +херов +хрен +хреново +хреновое +хреновый +хуевина +хуев +хуево +хуевый +хуек +хуечек +худоебина +хуебень +хуева +хуевато +хуеватый +хуеглот +хуегрыз +хуедрыга +хуемудрие +хуемыслие +хуеньки +хуеплет +хуесос +хуета +хуетень +хуец +хуила +хуиный +хуистый +хуишко +хуище +хуи +хуило +хуйло +хуйство +хуйнуть +хуйня +хуйню +хули +хуюжить +хуюжиться +хуюживать +хуюживаться +хуюшки +хуя +хуяк +хуякать +хуями +хуярить +хуяриться +хуястый +хуячий +хуячить +хуячиться +хуяшить +целка +целку +целочка +черножопые +чернозадый +член +шалава +шлюха +шмара +шмарить +шмариться +отъебись +отьебись +спам +spam +мудила +пидарасы +пиздоблядская +мудопроебина +пиздострадание +наебенюсь diff --git a/Jetsparrow.Aasb/.config/dotnet-tools.json b/Jetsparrow.Aasb/.config/dotnet-tools.json new file mode 100644 index 0000000..b0e38ab --- /dev/null +++ b/Jetsparrow.Aasb/.config/dotnet-tools.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "isRoot": true, + "tools": {} +} \ No newline at end of file diff --git a/Jetsparrow.Aasb/Aasb.cs b/Jetsparrow.Aasb/Aasb.cs deleted file mode 100644 index f3e0c04..0000000 --- a/Jetsparrow.Aasb/Aasb.cs +++ /dev/null @@ -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 log, IOptions 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); - } - } -} diff --git a/Jetsparrow.Aasb/CommandRouter.cs b/Jetsparrow.Aasb/CommandRouter.cs deleted file mode 100644 index 9bb2c2c..0000000 --- a/Jetsparrow.Aasb/CommandRouter.cs +++ /dev/null @@ -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 Commands { get; } - - public ChatCommandRouter(string username) - { - Username = username; - Commands = new Dictionary(); - } - - 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; - } - } -} diff --git a/Jetsparrow.Aasb/Commands/CommandRouter.cs b/Jetsparrow.Aasb/Commands/CommandRouter.cs new file mode 100644 index 0000000..19c7d33 --- /dev/null +++ b/Jetsparrow.Aasb/Commands/CommandRouter.cs @@ -0,0 +1,43 @@ +namespace Jetsparrow.Aasb.Commands; +public class ChatCommandRouter +{ + string BotUsername { get; } + Dictionary Commands { get; } + IOptionsMonitor Access { get; } + + public ChatCommandRouter(string username, IOptionsMonitor accessCfg) + { + BotUsername = username; + Access = accessCfg; + Commands = new Dictionary(); + } + + 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); + } +} diff --git a/Jetsparrow.Aasb/Commands/CommandString.cs b/Jetsparrow.Aasb/Commands/CommandString.cs index 0a7eb72..b62b18b 100644 --- a/Jetsparrow.Aasb/Commands/CommandString.cs +++ b/Jetsparrow.Aasb/Commands/CommandString.cs @@ -1,40 +1,9 @@ -using System.Text.RegularExpressions; - -namespace Jetsparrow.Aasb.Commands; - -public class CommandString +namespace Jetsparrow.Aasb.Commands; +public class CommandContext { - public CommandString(string command, string username, params string[] parameters) - { - Command = command; - Username = username; - Parameters = parameters; - } - - 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(@"/(?\w+)(@(?\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; - } + public string Command { get; set; } + public string Recipient { get; set; } + public string ChatId { get; set; } + public string SenderId { get; set; } + public string[] Parameters { get; set; } } diff --git a/Jetsparrow.Aasb/Commands/IChatCommand.cs b/Jetsparrow.Aasb/Commands/IChatCommand.cs new file mode 100644 index 0000000..7815e54 --- /dev/null +++ b/Jetsparrow.Aasb/Commands/IChatCommand.cs @@ -0,0 +1,6 @@ +namespace Jetsparrow.Aasb.Commands; +public interface IChatCommand +{ + bool Authorize { get; } + string Execute(CommandContext cmd); +} diff --git a/Jetsparrow.Aasb/Commands/IdCommand.cs b/Jetsparrow.Aasb/Commands/IdCommand.cs new file mode 100644 index 0000000..3f6f48b --- /dev/null +++ b/Jetsparrow.Aasb/Commands/IdCommand.cs @@ -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}`"; + } +} diff --git a/Jetsparrow.Aasb/Commands/LearnCommand.cs b/Jetsparrow.Aasb/Commands/LearnCommand.cs index 3afc5d8..6a7c830 100644 --- a/Jetsparrow.Aasb/Commands/LearnCommand.cs +++ b/Jetsparrow.Aasb/Commands/LearnCommand.cs @@ -1,26 +1,28 @@ -using System.Text.RegularExpressions; -using Telegram.Bot.Types; +using Jetsparrow.Aasb.Services; namespace Jetsparrow.Aasb.Commands; public class LearnCommand : IChatCommand { + public bool Authorize => true; SearchDictionary Dict { get; } - public LearnCommand(SearchDictionary dict) { Dict = dict; } - public string Execute(CommandString cmd, Update args) + public string Execute(CommandContext cmd) { var word = cmd.Parameters.FirstOrDefault(); if (string.IsNullOrWhiteSpace(word)) return null; - if (!Regex.IsMatch(word, @"[а-яА-Я]+")) - return null; - - bool newWord = Dict.Learn(word); - return newWord ? $"Принято слово \"{word}\"" : $"Поднял рейтинг слову \"{word}\""; + var learnRes = Dict.Learn(word); + return learnRes switch + { + SearchDictionary.LearnResult.Known => $"Я знаю что такое \"{word}\"", + SearchDictionary.LearnResult.Added => $"Понял принял, \"{word}\"", + SearchDictionary.LearnResult.Illegal => "Я такое запоминать не буду", + _ => "ась?" + }; } } diff --git a/Jetsparrow.Aasb/Commands/UnlearnCommand.cs b/Jetsparrow.Aasb/Commands/UnlearnCommand.cs index 1aeb188..3e4ccb3 100644 --- a/Jetsparrow.Aasb/Commands/UnlearnCommand.cs +++ b/Jetsparrow.Aasb/Commands/UnlearnCommand.cs @@ -1,9 +1,9 @@ -using System.Text.RegularExpressions; -using Telegram.Bot.Types; +using Jetsparrow.Aasb.Services; namespace Jetsparrow.Aasb.Commands; public class UnlearnCommand : IChatCommand { + public bool Authorize => true; SearchDictionary Dict { get; } public UnlearnCommand(SearchDictionary dict) @@ -11,17 +11,15 @@ public class UnlearnCommand : IChatCommand Dict = dict; } - public string Execute(CommandString cmd, Update args) + public string Execute(CommandContext cmd) { var word = cmd.Parameters.FirstOrDefault(); if (string.IsNullOrWhiteSpace(word)) return null; - if (!Regex.IsMatch(word, @"[а-яА-Я]+")) - return null; if (Dict.Unlearn(word)) - return $"Удалил слово \"{word}\""; + return $"Больше не буду"; else - return $"Не нашел слово \"{word}\""; + return $"А я и не знаю что такое \"{word}\""; } } diff --git a/Jetsparrow.Aasb/Config.cs b/Jetsparrow.Aasb/Config.cs index 8cd63b0..b6f8f1c 100644 --- a/Jetsparrow.Aasb/Config.cs +++ b/Jetsparrow.Aasb/Config.cs @@ -1,12 +1,10 @@ -namespace Jetsparrow.Aasb; +using System.ComponentModel.DataAnnotations; -public class ServiceSettings -{ - public string Urls { get; set; } -} +namespace Jetsparrow.Aasb; public class UnbleeperSettings { + public string LegalWordsRegex { get; set; } public string BleepedSwearsRegex { get; set; } public int MinAmbiguousWordLength { get; set; } public int MinWordLength { get; set; } @@ -15,14 +13,17 @@ public class UnbleeperSettings public class SearchDictionarySettings { public string DictionaryPath { get; set; } + + [Range(typeof(TimeSpan), "00:00:10", "01:00:00")] + public TimeSpan AutosavePeriod { get; set; } } public class TelegramSettings { 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; } +} \ No newline at end of file diff --git a/Jetsparrow.Aasb/GlobalUsings.cs b/Jetsparrow.Aasb/GlobalUsings.cs index e570036..1842ce9 100644 --- a/Jetsparrow.Aasb/GlobalUsings.cs +++ b/Jetsparrow.Aasb/GlobalUsings.cs @@ -2,5 +2,9 @@ global using System.Text; global using System.Linq; global using System.Collections.Generic; +global using System.Threading; +global using System.Threading.Tasks; -global using Microsoft.Extensions.Options; \ No newline at end of file +global using Microsoft.Extensions.Options; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Hosting; diff --git a/Jetsparrow.Aasb/Health/HealthUtils.cs b/Jetsparrow.Aasb/Health/HealthUtils.cs new file mode 100644 index 0000000..7372f48 --- /dev/null +++ b/Jetsparrow.Aasb/Health/HealthUtils.cs @@ -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())); + } +} diff --git a/Jetsparrow.Aasb/StartupHealthCheck.cs b/Jetsparrow.Aasb/Health/StartupHealthCheck.cs similarity index 68% rename from Jetsparrow.Aasb/StartupHealthCheck.cs rename to Jetsparrow.Aasb/Health/StartupHealthCheck.cs index 62c9958..d0e222b 100644 --- a/Jetsparrow.Aasb/StartupHealthCheck.cs +++ b/Jetsparrow.Aasb/Health/StartupHealthCheck.cs @@ -1,14 +1,13 @@ -using System.Threading; -using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Diagnostics.HealthChecks; +using Jetsparrow.Aasb.Services; -namespace Jetsparrow.Aasb; +namespace Jetsparrow.Aasb.Health; public class StartupHealthCheck : IHealthCheck { - Aasb Bot { get; } + AntiAntiSwearingBot Bot { get; } - public StartupHealthCheck(Aasb bot) + public StartupHealthCheck(AntiAntiSwearingBot bot) { Bot = bot; } diff --git a/Jetsparrow.Aasb/Jetsparrow.Aasb.csproj b/Jetsparrow.Aasb/Jetsparrow.Aasb.csproj index dec9685..9700170 100644 --- a/Jetsparrow.Aasb/Jetsparrow.Aasb.csproj +++ b/Jetsparrow.Aasb/Jetsparrow.Aasb.csproj @@ -3,6 +3,7 @@ net6.0 en + False @@ -10,7 +11,7 @@ - + @@ -34,4 +35,8 @@ + + + + diff --git a/Jetsparrow.Aasb/Language.cs b/Jetsparrow.Aasb/Language.cs index ead61d0..68291de 100644 --- a/Jetsparrow.Aasb/Language.cs +++ b/Jetsparrow.Aasb/Language.cs @@ -1,7 +1,7 @@ using System.Text.RegularExpressions; 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); } diff --git a/Jetsparrow.Aasb/Program.cs b/Jetsparrow.Aasb/Program.cs index 0e079c0..daa936a 100644 --- a/Jetsparrow.Aasb/Program.cs +++ b/Jetsparrow.Aasb/Program.cs @@ -1,34 +1,37 @@ using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Jetsparrow.Aasb; +using Jetsparrow.Aasb.Services; +using Jetsparrow.Aasb.Health; var builder = WebApplication.CreateBuilder(); - -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); -}); +Console.WriteLine("Configuring..."); var cfg = builder.Configuration; var svc = builder.Services; -svc.Configure(cfg.GetSection("SearchDictionary")); -svc.Configure(cfg.GetSection("Telegram")); -svc.Configure(cfg.GetSection("Unbleeper")); +svc.AddOptions().BindConfiguration("SearchDictionary").ValidateDataAnnotations(); +svc.AddOptions().BindConfiguration("Telegram"); +svc.AddOptions().BindConfiguration("Unbleeper"); + svc.AddHealthChecks().AddCheck("Startup"); -svc.AddHostedSingleton(); +svc.AddSingleton(); svc.AddSingleton(); -svc.AddHostedSingleton(); +svc.AddHostedSingleton(); +Console.WriteLine("Building..."); var app = builder.Build(); app.UseDeveloperExceptionPage(); app.UseRouting(); app.UseEndpoints(cfg => { cfg.MapHealthChecks("/health"); + cfg.MapHealthChecks("/health/verbose", new() + { + ResponseWriter = HealthUtils.WriteHealthCheckResponse + }); }); + +Console.WriteLine("Running..."); app.Run(); diff --git a/Jetsparrow.Aasb/Properties/PublishProfiles/FolderProfile.pubxml b/Jetsparrow.Aasb/Properties/PublishProfiles/FolderProfile.pubxml index ec705a2..ddddd62 100644 --- a/Jetsparrow.Aasb/Properties/PublishProfiles/FolderProfile.pubxml +++ b/Jetsparrow.Aasb/Properties/PublishProfiles/FolderProfile.pubxml @@ -1,15 +1,16 @@  - + - FileSystem - Release - Any CPU - net6.0 - C:\Prog\releases\aasb - false - <_IsPortable>true + True + False + True + Release + Any CPU + FileSystem + C:\Prog\releases\aasb + FileSystem \ No newline at end of file diff --git a/Jetsparrow.Aasb/SearchDictionary.cs b/Jetsparrow.Aasb/SearchDictionary.cs deleted file mode 100644 index 68d80f0..0000000 --- a/Jetsparrow.Aasb/SearchDictionary.cs +++ /dev/null @@ -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 cfg) - { - Cfg = cfg; - var path = cfg.CurrentValue.DictionaryPath; - words = File.ReadAllLines(path).ToList(); - } - - IOptionsMonitor Cfg { get; } - ReaderWriterLockSlim DictLock = new(); - List 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 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); - } - -} diff --git a/Jetsparrow.Aasb/Services/Aasb.cs b/Jetsparrow.Aasb/Services/Aasb.cs new file mode 100644 index 0000000..197d79c --- /dev/null +++ b/Jetsparrow.Aasb/Services/Aasb.cs @@ -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 AccessCfg { get; } + ILogger Log { get; } + public bool Started { get; private set; } = false; + + public AntiAntiSwearingBot( + ILogger log, + IOptions tgCfg, + Unbleeper unbp, + IOptionsMonitor 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(@"/(?\w+)(@(?\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; + } +} diff --git a/Jetsparrow.Aasb/Services/SearchDictionary.cs b/Jetsparrow.Aasb/Services/SearchDictionary.cs new file mode 100644 index 0000000..9048fd3 --- /dev/null +++ b/Jetsparrow.Aasb/Services/SearchDictionary.cs @@ -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 searchDictionaryCfg, + IOptions 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 Match(string pattern) + { + await Loaded; + var matches = TopMatches(pattern); + return matches[Random.Shared.Next(0, matches.Count)]; + } + + IList TopMatches(string pattern) + { + pattern = pattern.ToLowerInvariant(); + List 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 words; + + #region load/save + Task Loaded, Autosave; + bool Changed; + async Task Load() + { + words = new List(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 +} diff --git a/Jetsparrow.Aasb/Unbleeper.cs b/Jetsparrow.Aasb/Services/Unbleeper.cs similarity index 59% rename from Jetsparrow.Aasb/Unbleeper.cs rename to Jetsparrow.Aasb/Services/Unbleeper.cs index ab8e02c..5fbccdf 100644 --- a/Jetsparrow.Aasb/Unbleeper.cs +++ b/Jetsparrow.Aasb/Services/Unbleeper.cs @@ -1,7 +1,6 @@ using System.Text.RegularExpressions; -namespace Jetsparrow.Aasb; - +namespace Jetsparrow.Aasb.Services; public class Unbleeper { SearchDictionary Dict { get; } @@ -11,43 +10,45 @@ public class Unbleeper { Dict = dict; 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; } static readonly char[] WORD_SEPARATORS = { ' ', '\t', '\r', '\n', '.', ',', '!', '?', ';', ':', '-', '—' }; - public string UnbleepSwears(string text) + public async Task UnbleepSwears(string text) { if (string.IsNullOrWhiteSpace(text)) return null; text = text.Trim(); - if (text.StartsWith('/')) // is chat command - return null; var words = text.Split(WORD_SEPARATORS, StringSplitOptions.RemoveEmptyEntries); var candidates = words .Where(w => - !Language.IsTelegramMention(w) - && !Language.IsEmailPart(w) - && Language.HasNonWordChars(w) - && !Language.IsHashTag(w) - && (Language.HasWordChars(w) || w.Length >= Cfg.MinAmbiguousWordLength) + StringEx.HasNonWordChars(w) + && (StringEx.HasWordChars(w) || w.Length >= Cfg.MinAmbiguousWordLength) && w.Length >= Cfg.MinWordLength + && !StringEx.IsTelegramMention(w) + && !StringEx.IsEmailPart(w) + && !StringEx.IsHashTag(w) && BleepedSwearsRegex.IsMatch(w) - ) - .ToArray(); + ) + .ToList(); if (!candidates.Any()) return null; 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)); } return response.ToString(); diff --git a/Jetsparrow.Aasb/appsettings.json b/Jetsparrow.Aasb/appsettings.json index b4a95dc..ca64e06 100644 --- a/Jetsparrow.Aasb/appsettings.json +++ b/Jetsparrow.Aasb/appsettings.json @@ -1,4 +1,10 @@ { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, "Kestrel": { "Endpoints": { "Http": { @@ -7,12 +13,14 @@ } }, "Unbleeper": { + "LegalWordsRegex": "[а-яА-ЯёЁ]+", "BleepedSwearsRegex": "[а-яА-ЯёЁ@\\*#]+", "MinWordLength": 3, "MinAmbiguousWordLength": 5 }, "SearchDictionary": { - "DictionaryPath": "dict/ObsceneDictionaryRu.txt" + "DictionaryPath": "dict/ObsceneDictionaryRu.txt", + "AutosavePeriod": "00:01:00" }, "Telegram": { "UseProxy": false