Skip to content

Commit

Permalink
updated to the latest version of Telegram.Bot library (#57)
Browse files Browse the repository at this point in the history
  • Loading branch information
Szer authored Oct 18, 2024
1 parent 088533c commit d35a8d8
Show file tree
Hide file tree
Showing 13 changed files with 124 additions and 88 deletions.
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -7,12 +7,13 @@ LABEL "org.testcontainers.resource-reaper-session"=$RESOURCE_REAPER_SESSION_ID

WORKDIR /src/VahterBanBot
COPY src/VahterBanBot/VahterBanBot.fsproj .
COPY NuGet.Config .
RUN dotnet restore
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"]
7 changes: 7 additions & 0 deletions NuGet.Config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="Telegram.Bot" value="https://pkgs.dev.azure.com/tgbots/Telegram.Bot/_packaging/release/nuget/v3/index.json" />
</packageSources>
</configuration>
98 changes: 52 additions & 46 deletions src/VahterBanBot.Tests/ContainerTestBase.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ open System
open System.IO
open System.Net.Http
open System.Text
open System.Text.Json
open System.Threading.Tasks
open DotNet.Testcontainers.Builders
open DotNet.Testcontainers.Configurations
open DotNet.Testcontainers.Containers
open Newtonsoft.Json
open Npgsql
open Telegram.Bot.Types
open Testcontainers.PostgreSql
open VahterBanBot.Tests.TgMessageUtils
open VahterBanBot.Types
open VahterBanBot.Utils
open Xunit
open Dapper

Expand Down Expand Up @@ -83,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")
Expand Down Expand Up @@ -115,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
Expand All @@ -168,10 +174,10 @@ type VahterTestContainers() =
member _.Uri = uri

member this.SendMessage(update: Update) = task {
let json = JsonConvert.SerializeObject(update)
let json = JsonSerializer.Serialize(update, options = jsonOptions)
return! this.SendMessage(json)
}

member _.SendMessage(json: string) = task {
let content = new StringContent(json, Encoding.UTF8, "application/json")
let! resp = httpClient.PostAsync("/bot", content)
Expand Down
6 changes: 3 additions & 3 deletions src/VahterBanBot.Tests/MessageTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ type MessageTests(fixture: VahterTestContainers) =
let date = DateTimeOffset(msg.Date).ToUnixTimeSeconds()

Assert.Equal(
dbMsg.Value,
{ chat_id = msgUpdate.Message.Chat.Id
message_id = msgUpdate.Message.MessageId
user_id = msgUpdate.Message.From.Id
text = msgUpdate.Message.Text
raw_message = $"""{{"chat": {{"id": -666, "type": "unknown", "username": "pro.hell"}}, "date": {date}, "from": {{"id": {msg.From.Id}, "is_bot": false, "first_name": "{msg.From.FirstName}"}}, "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"}}, "entities": [{{"type": "code", "length": 6, "offset": 0}}], "message_id": {msg.MessageId}}}"""
created_at = dbMsg.Value.created_at }
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
)
}

Expand Down
4 changes: 3 additions & 1 deletion src/VahterBanBot.Tests/TgMessageUtils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module VahterBanBot.Tests.TgMessageUtils
open System
open System.Threading
open Telegram.Bot.Types
open Telegram.Bot.Types.Enums

type Tg() =
static let mutable i = 1L // higher than the data in the test_seed.sql
Expand All @@ -17,7 +18,8 @@ type Tg() =
static member chat (?id: int64, ?username: string) =
Chat(
Id = (id |> Option.defaultValue (nextInt64())),
Username = (username |> Option.defaultValue null)
Username = (username |> Option.defaultValue null),
Type = ChatType.Supergroup
)

static member callback(data: string, ?from: User) =
Expand Down
14 changes: 6 additions & 8 deletions src/VahterBanBot.Tests/VahterBanBot.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
</PropertyGroup>

<ItemGroup>
<Content Include="test_seed.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="test_seed.sql" CopyToOutputDirectory="PreserveNewest" />
<Compile Include="TgMessageUtils.fs" />
<Compile Include="ContainerTestBase.fs" />
<Compile Include="BaseTests.fs" />
Expand All @@ -23,12 +21,12 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Testcontainers" Version="3.8.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="3.8.0" />
<PackageReference Include="xunit" Version="2.8.1"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Testcontainers" Version="3.10.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="3.10.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="Xunit.Extensions.AssemblyFixture" Version="2.6.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
2 changes: 1 addition & 1 deletion src/VahterBanBot/Bot.fs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ let softBanInChat (botClient: ITelegramBotClient) (chatId: ChatId) targetUserId
)
let untilDate = DateTime.UtcNow.AddHours duration
try
do! botClient.RestrictChatMemberAsync(chatId, targetUserId, permissions, Nullable(), untilDate)
do! botClient.RestrictChatMemberAsync(chatId, targetUserId, permissions, untilDate = untilDate)
return Ok(chatId, targetUserId)
with e ->
return Error(chatId, targetUserId, e)
Expand Down
9 changes: 5 additions & 4 deletions src/VahterBanBot/FakeTgApi.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ open System
open System.Net
open System.Net.Http
open System.Text
open System.Text.Json
open System.Threading.Tasks
open Newtonsoft.Json
open Telegram.Bot.Types
open Telegram.Bot.Types.Enums
open VahterBanBot.Types
open VahterBanBot.Utils

let fakeTgApi (botConf: BotConfiguration) =
{ new DelegatingHandler() with
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()
Expand All @@ -37,7 +38,7 @@ let fakeTgApi (botConf: BotConfiguration) =
Type = ChatType.Private
)
)
|> JsonConvert.SerializeObject
|> fun x -> JsonSerializer.Serialize(x, options = jsonOptions)
apiResult message
elif url.EndsWith "/getChatAdministrators" then
// respond with the request body as a string
Expand All @@ -59,7 +60,7 @@ let fakeTgApi (botConf: BotConfiguration) =
)
)
|]
|> JsonConvert.SerializeObject
|> fun x -> JsonSerializer.Serialize(x, options = jsonOptions)
apiResult message
else
// return 500 for any other request
Expand Down
15 changes: 9 additions & 6 deletions src/VahterBanBot/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

open System
open System.Collections.Generic
open System.Text.Json
open System.Threading
open System.Threading.Tasks
open Dapper
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Logging
open Microsoft.FSharp.Core
open Newtonsoft.Json
open Telegram.Bot
open Telegram.Bot.Polling
open Telegram.Bot.Types
Expand Down Expand Up @@ -43,8 +43,8 @@ let botConf =
BotUserId = getEnv "BOT_USER_ID" |> int64
BotUserName = getEnv "BOT_USER_NAME"
LogsChannelId = getEnv "LOGS_CHANNEL_ID" |> int64
ChatsToMonitor = getEnv "CHATS_TO_MONITOR" |> JsonConvert.DeserializeObject<_>
AllowedUsers = getEnv "ALLOWED_USERS" |> JsonConvert.DeserializeObject<_>
ChatsToMonitor = getEnv "CHATS_TO_MONITOR" |> JsonSerializer.Deserialize<_>
AllowedUsers = getEnv "ALLOWED_USERS" |> JsonSerializer.Deserialize<_>
ShouldDeleteChannelMessages = getEnvOr "SHOULD_DELETE_CHANNEL_MESSAGES" "true" |> bool.Parse
IgnoreSideEffects = getEnvOr "IGNORE_SIDE_EFFECTS" "false" |> bool.Parse
UsePolling = getEnvOr "USE_POLLING" "false" |> bool.Parse
Expand All @@ -67,7 +67,7 @@ let botConf =
MlTrainingSetFraction = getEnvOr "ML_TRAINING_SET_FRACTION" "0.2" |> float
MlSpamThreshold = getEnvOr "ML_SPAM_THRESHOLD" "0.5" |> single
MlWarningThreshold = getEnvOr "ML_WARNING_THRESHOLD" "0.0" |> single
MlStopWordsInChats = getEnvOr "ML_STOP_WORDS_IN_CHATS" "{}" |> JsonConvert.DeserializeObject<_> }
MlStopWordsInChats = getEnvOr "ML_STOP_WORDS_IN_CHATS" "{}" |> JsonSerializer.Deserialize<_> }

let validateApiKey (ctx : HttpContext) =
match ctx.TryGetRequestHeader "X-Telegram-Bot-Api-Secret-Token" with
Expand All @@ -81,6 +81,9 @@ 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.ISerializer>(Json.Serializer(jsonOptions))
.ConfigureTelegramBot<Microsoft.AspNetCore.Http.Json.JsonOptions>(fun x -> x.SerializerOptions)
.AddHostedService<CleanupService>()
.AddHostedService<StartupMessage>()
.AddHostedService<UpdateChatAdmins>()
Expand Down Expand Up @@ -153,7 +156,7 @@ let webApp = choose [

POST >=> route botConf.Route >=> requiresApiKey >=> bindJson<Update> (fun update next ctx -> task {
let updateBodyJson =
try JsonConvert.SerializeObject update
try JsonSerializer.Serialize(update, options = jsonOptions)
with e -> e.Message
use topActivity =
botActivity
Expand Down Expand Up @@ -195,7 +198,7 @@ if botConf.UsePolling then
let ml = ctx.ServiceProvider.GetRequiredService<MachineLearning>()
do! onUpdate botUser client botConf logger ml update
}
member x.HandlePollingErrorAsync (botClient: ITelegramBotClient, ex: Exception, cancellationToken: CancellationToken) =
member this.HandleErrorAsync(botClient, ``exception``, source, cancellationToken) =
Task.CompletedTask
}
telegramClient.StartReceiving(pollingHandler, null, CancellationToken.None)
Expand Down
13 changes: 9 additions & 4 deletions src/VahterBanBot/Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
open System
open System.Collections.Generic
open System.Text
open System.Text.Json
open System.Text.Json.Serialization
open Dapper
open Newtonsoft.Json
open Telegram.Bot.Types
open Utils

Expand Down Expand Up @@ -93,7 +94,7 @@ type DbMessage =
user_id = message.From.Id
created_at = DateTime.UtcNow
text = message.TextOrCaption
raw_message = JsonConvert.SerializeObject message }
raw_message = JsonSerializer.Serialize(message, options = jsonOptions) }

[<CLIMutable>]
type VahterStat =
Expand Down Expand Up @@ -146,11 +147,15 @@ type DbCallback =

type CallbackMessageTypeHandler() =
inherit SqlMapper.TypeHandler<CallbackMessage>()
let callBackOptions =
let opts = JsonFSharpOptions.Default().ToJsonSerializerOptions()
Telegram.Bot.JsonBotAPI.Configure(opts)
opts

override this.SetValue(parameter, value) =
parameter.Value <- JsonConvert.SerializeObject value
parameter.Value <- JsonSerializer.Serialize(value, options = callBackOptions)
override this.Parse(value) =
JsonConvert.DeserializeObject<CallbackMessage>(value.ToString())
JsonSerializer.Deserialize<CallbackMessage>(value.ToString(), options = callBackOptions)

[<CLIMutable>]
type UserStats =
Expand Down
Loading

0 comments on commit d35a8d8

Please sign in to comment.