diff --git a/.env.example b/.env.example index 42a664b..2bbb84c 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # envs -BOT_TELEGRAM_TOKEN=SECRET_FROM_TELEGRAM +BOT_TELEGRAM_TOKEN=123:456 BOT_AUTH_TOKEN=JUST_YOUR_SECRET BOT_HOOK_ROUTE=/bot BOT_USER_ID=123456789 @@ -9,7 +9,7 @@ DEBUG=true LOGS_CHANNEL_ID=-1000000000000 ALLOWED_USERS={"you":"123467890"} CHATS_TO_MONITOR={"your_channel":"-100123467890"} -DATABASE_URL=Server=localhost;Database=vahter_bot_ban;Port=5432;User Id=vahter_bot_ban_service;Password=password; +DATABASE_URL='Server=localhost;Database=vahter_bot_ban;Port=5432;User Id=vahter_bot_ban_service;Password=password;' OTEL_EXPORTER_ZIPKIN_ENDPOINT=http://zipkin:9411/api/v2/spans OTEL_EXPORTER_CONSOLE=false IGNORE_SIDE_EFFECTS=true diff --git a/Dockerfile b/Dockerfile index c834616..d2cfa13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0.302-jammy as build-env +FROM mcr.microsoft.com/dotnet/sdk:8.0.302-jammy AS build-env ### workaround for testcontainers resource reaper issue ARG RESOURCE_REAPER_SESSION_ID="00000000-0000-0000-0000-000000000000" @@ -13,7 +13,7 @@ COPY src/VahterBanBot . COPY global.json . RUN dotnet publish -c Release -o /publish -FROM mcr.microsoft.com/dotnet/aspnet:8.0 as runtime +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime WORKDIR /publish COPY --from=build-env /publish . ENTRYPOINT ["dotnet", "VahterBanBot.dll"] diff --git a/src/VahterBanBot.Tests/ContainerTestBase.fs b/src/VahterBanBot.Tests/ContainerTestBase.fs index 88813fb..6307e09 100644 --- a/src/VahterBanBot.Tests/ContainerTestBase.fs +++ b/src/VahterBanBot.Tests/ContainerTestBase.fs @@ -84,11 +84,11 @@ type VahterTestContainers() = .WithPortBinding(80, true) .WithEnvironment("BOT_USER_ID", "1337") .WithEnvironment("BOT_USER_NAME", "test_bot") - .WithEnvironment("BOT_TELEGRAM_TOKEN", "TELEGRAM_SECRET") + .WithEnvironment("BOT_TELEGRAM_TOKEN", "123:456") .WithEnvironment("BOT_AUTH_TOKEN", "OUR_SECRET") .WithEnvironment("LOGS_CHANNEL_ID", "-123") - .WithEnvironment("CHATS_TO_MONITOR", """{"pro.hell": -666, "dotnetru": -42}""") - .WithEnvironment("ALLOWED_USERS", """{"vahter_1": 34, "vahter_2": 69}""") + .WithEnvironment("CHATS_TO_MONITOR", """{"pro.hell":-666,"dotnetru":-42}""") + .WithEnvironment("ALLOWED_USERS", """{"vahter_1":34,"vahter_2":69}""") .WithEnvironment("SHOULD_DELETE_CHANNEL_MESSAGES", "true") .WithEnvironment("IGNORE_SIDE_EFFECTS", "false") .WithEnvironment("USE_FAKE_TG_API", "true") @@ -116,46 +116,51 @@ type VahterTestContainers() = interface IAsyncLifetime with member this.InitializeAsync() = task { - // start building the image and spin up db at the same time - let imageTask = image.CreateAsync() - let dbTask = dbContainer.StartAsync() - - // wait for both to finish - do! imageTask - do! dbTask - publicConnectionString <- $"Server=127.0.0.1;Database=vahter_bot_ban;Port={dbContainer.GetMappedPublicPort(5432)};User Id=vahter_bot_ban_service;Password=vahter_bot_ban_service;Include Error Detail=true;Minimum Pool Size=1;Maximum Pool Size=20;Max Auto Prepare=100;Auto Prepare Min Usages=1;Trust Server Certificate=true;" - - // initialize DB with the schema, database and a DB user - let script = File.ReadAllText(CommonDirectoryPath.GetSolutionDirectory().DirectoryPath + "/init.sql") - let! initResult = dbContainer.ExecScriptAsync(script) - if initResult.Stderr <> "" then - failwith initResult.Stderr - - // run migrations - do! flywayContainer.StartAsync() - let! out, err = flywayContainer.GetLogsAsync() - if err <> "" then - failwith err - if not (out.Contains "Successfully applied") then - failwith out - - // seed some test data - let script = File.ReadAllText(CommonDirectoryPath.GetCallerFileDirectory().DirectoryPath + "/test_seed.sql") - let scriptFilePath = String.Join("/", String.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName()) - do! dbContainer.CopyAsync(Encoding.Default.GetBytes script, scriptFilePath, Unix.FileMode644) - let! scriptResult = dbContainer.ExecAsync [|"psql"; "--username"; "vahter_bot_ban_service"; "--dbname"; "vahter_bot_ban"; "--file"; scriptFilePath |] - - if scriptResult.Stderr <> "" then - failwith scriptResult.Stderr - - // start the app container - do! appContainer.StartAsync() - - // initialize the http client with correct hostname and port - httpClient <- new HttpClient() - uri <- Uri($"http://{appContainer.Hostname}:{appContainer.GetMappedPublicPort(80)}") - httpClient.BaseAddress <- uri - httpClient.DefaultRequestHeaders.Add("X-Telegram-Bot-Api-Secret-Token", "OUR_SECRET") + try + // start building the image and spin up db at the same time + let imageTask = image.CreateAsync() + let dbTask = dbContainer.StartAsync() + + // wait for both to finish + do! imageTask + do! dbTask + publicConnectionString <- $"Server=127.0.0.1;Database=vahter_bot_ban;Port={dbContainer.GetMappedPublicPort(5432)};User Id=vahter_bot_ban_service;Password=vahter_bot_ban_service;Include Error Detail=true;Minimum Pool Size=1;Maximum Pool Size=20;Max Auto Prepare=100;Auto Prepare Min Usages=1;Trust Server Certificate=true;" + + // initialize DB with the schema, database and a DB user + let script = File.ReadAllText(CommonDirectoryPath.GetSolutionDirectory().DirectoryPath + "/init.sql") + let! initResult = dbContainer.ExecScriptAsync(script) + if initResult.Stderr <> "" then + failwith initResult.Stderr + + // run migrations + do! flywayContainer.StartAsync() + let! out, err = flywayContainer.GetLogsAsync() + if err <> "" then + failwith err + if not (out.Contains "Successfully applied") then + failwith out + + // seed some test data + let script = File.ReadAllText(CommonDirectoryPath.GetCallerFileDirectory().DirectoryPath + "/test_seed.sql") + let scriptFilePath = String.Join("/", String.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName()) + do! dbContainer.CopyAsync(Encoding.Default.GetBytes script, scriptFilePath, Unix.FileMode644) + let! scriptResult = dbContainer.ExecAsync [|"psql"; "--username"; "vahter_bot_ban_service"; "--dbname"; "vahter_bot_ban"; "--file"; scriptFilePath |] + + if scriptResult.Stderr <> "" then + failwith scriptResult.Stderr + + // start the app container + do! appContainer.StartAsync() + + // initialize the http client with correct hostname and port + httpClient <- new HttpClient() + uri <- Uri($"http://{appContainer.Hostname}:{appContainer.GetMappedPublicPort(80)}") + httpClient.BaseAddress <- uri + httpClient.DefaultRequestHeaders.Add("X-Telegram-Bot-Api-Secret-Token", "OUR_SECRET") + finally + let struct (_, err) = appContainer.GetLogsAsync().Result + if err <> "" then + failwith err } member this.DisposeAsync() = task { // stop all the containers, flyway might be dead already diff --git a/src/VahterBanBot.Tests/MessageTests.fs b/src/VahterBanBot.Tests/MessageTests.fs index 2574641..b85fc08 100644 --- a/src/VahterBanBot.Tests/MessageTests.fs +++ b/src/VahterBanBot.Tests/MessageTests.fs @@ -32,7 +32,7 @@ type MessageTests(fixture: VahterTestContainers) = message_id = msgUpdate.Message.MessageId user_id = msgUpdate.Message.From.Id text = msgUpdate.Message.Text - raw_message = $"""{{"chat": {{"id": -666, "type": "supergroup", "is_forum": false, "username": "pro.hell"}}, "date": {date}, "from": {{"id": {msg.From.Id}, "is_bot": false, "first_name": "{msg.From.FirstName}", "is_premium": false, "can_join_groups": false, "supports_inline_queries": false, "added_to_attachment_menu": false, "can_read_all_group_messages": false}}, "text": "{msg.Text}", "sticker": {{"type": "mask", "width": 512, "height": 512, "file_id": "sticker-id", "is_video": false, "is_animated": false, "file_unique_id": "sticker-uid", "needs_repainting": false}}, "entities": [{{"type": "code", "length": 6, "offset": 0}}], "message_id": {msg.MessageId}, "is_topic_message": false, "has_media_spoiler": false, "is_automatic_forward": false, "has_protected_content": false}}""" + raw_message = $"""{{"chat": {{"id": -666, "type": "supergroup", "is_forum": false, "username": "pro.hell"}}, "date": {date}, "from": {{"id": {msg.From.Id}, "is_bot": false, "first_name": "{msg.From.FirstName}", "is_premium": false, "can_join_groups": false, "has_main_web_app": false, "can_connect_to_business": false, "supports_inline_queries": false, "added_to_attachment_menu": false, "can_read_all_group_messages": false}}, "text": "{msg.Text}", "sticker": {{"type": "mask", "width": 512, "height": 512, "file_id": "sticker-id", "is_video": false, "is_animated": false, "file_unique_id": "sticker-uid", "needs_repainting": false}}, "entities": [{{"type": "code", "length": 6, "offset": 0}}], "message_id": {msg.MessageId}, "is_from_offline": false, "is_topic_message": false, "has_media_spoiler": false, "is_automatic_forward": false, "has_protected_content": false, "show_caption_above_media": false}}""" created_at = dbMsg.Value.created_at }, dbMsg.Value ) diff --git a/src/VahterBanBot.Tests/VahterBanBot.Tests.fsproj b/src/VahterBanBot.Tests/VahterBanBot.Tests.fsproj index 42b4202..ffa6474 100644 --- a/src/VahterBanBot.Tests/VahterBanBot.Tests.fsproj +++ b/src/VahterBanBot.Tests/VahterBanBot.Tests.fsproj @@ -9,9 +9,7 @@ - - PreserveNewest - + diff --git a/src/VahterBanBot/FakeTgApi.fs b/src/VahterBanBot/FakeTgApi.fs index f508f93..08e92d8 100644 --- a/src/VahterBanBot/FakeTgApi.fs +++ b/src/VahterBanBot/FakeTgApi.fs @@ -16,7 +16,7 @@ let fakeTgApi (botConf: BotConfiguration) = member x.SendAsync(request, cancellationToken) = let apiResult text = let resp = new HttpResponseMessage(HttpStatusCode.OK) - resp.Content <- new StringContent($"""{{"Ok":true,"Result":{text}}}""", Encoding.UTF8, "application/json") + resp.Content <- new StringContent($"""{{"ok":true,"result":{text}}}""", Encoding.UTF8, "application/json") resp let url = request.RequestUri.ToString() diff --git a/src/VahterBanBot/Program.fs b/src/VahterBanBot/Program.fs index 0bdb6ef..9b55a65 100644 --- a/src/VahterBanBot/Program.fs +++ b/src/VahterBanBot/Program.fs @@ -81,6 +81,8 @@ let builder = WebApplication.CreateBuilder() %builder.Services .AddSingleton(botConf) .AddGiraffe() + // we need to customize Giraffe STJ settings to conform to the Telegram.Bot API + .AddSingleton(Json.Serializer(jsonOptions)) .ConfigureTelegramBot(fun x -> x.SerializerOptions) .AddHostedService() .AddHostedService() diff --git a/src/VahterBanBot/Types.fs b/src/VahterBanBot/Types.fs index 001ba72..cc4c441 100644 --- a/src/VahterBanBot/Types.fs +++ b/src/VahterBanBot/Types.fs @@ -4,6 +4,7 @@ open System open System.Collections.Generic open System.Text open System.Text.Json +open System.Text.Json.Serialization open Dapper open Telegram.Bot.Types open Utils @@ -146,11 +147,15 @@ type DbCallback = type CallbackMessageTypeHandler() = inherit SqlMapper.TypeHandler() + let callBackOptions = + let opts = JsonFSharpOptions.Default().ToJsonSerializerOptions() + Telegram.Bot.JsonBotAPI.Configure(opts) + opts override this.SetValue(parameter, value) = - parameter.Value <- JsonSerializer.Serialize(value, options = jsonOptions) + parameter.Value <- JsonSerializer.Serialize(value, options = callBackOptions) override this.Parse(value) = - JsonSerializer.Deserialize(value.ToString(), options = jsonOptions) + JsonSerializer.Deserialize(value.ToString(), options = callBackOptions) [] type UserStats = diff --git a/src/VahterBanBot/Utils.fs b/src/VahterBanBot/Utils.fs index c1ed60b..3af8c91 100644 --- a/src/VahterBanBot/Utils.fs +++ b/src/VahterBanBot/Utils.fs @@ -80,6 +80,6 @@ let jsonOptions = // there is a contradiction in Telegram.Bot library where User.IsBot is not nullable and required during deserialization, // but it is omitted when default on deserialization via settings setup in JsonBotAPI.Configure // so we'll override this setting explicitly - baseOpts.SerializerOptions.DefaultIgnoreCondition <- System.Text.Json.Serialization.JsonIgnoreCondition.Never + baseOpts.SerializerOptions.DefaultIgnoreCondition <- System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull baseOpts.SerializerOptions diff --git a/src/VahterBanBot/VahterBanBot.fsproj b/src/VahterBanBot/VahterBanBot.fsproj index 90588fd..fff6a1e 100644 --- a/src/VahterBanBot/VahterBanBot.fsproj +++ b/src/VahterBanBot/VahterBanBot.fsproj @@ -23,6 +23,7 @@ +