Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

local admins in chats won't trigger ML deletion #45

Merged
merged 1 commit into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ ML_TRAINING_SET_FRACTION=0.2
ML_SPAM_THRESHOLD=0.5
ML_WARNING_THRESHOLD=0.0
ML_STOP_WORDS_IN_CHATS={"-123":["word1","word2"]}
UPDATE_CHAT_ADMINS_INTERVAL_SEC=86400
UPDATE_CHAT_ADMINS=true
14 changes: 7 additions & 7 deletions src/VahterBanBot.Tests/BanTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type BanTests(fixture: VahterTestContainers) =

// send the ban message
let! banResp =
Tg.replyMsg(msgUpdate.Message, "/ban", fixture.AdminUsers[0])
Tg.replyMsg(msgUpdate.Message, "/ban", fixture.Vahters[0])
|> fixture.SendMessage
Assert.Equal(HttpStatusCode.OK, banResp.StatusCode)

Expand Down Expand Up @@ -50,7 +50,7 @@ type BanTests(fixture: VahterTestContainers) =

// send the ban message
let! banResp =
Tg.replyMsg(msgUpdate.Message, "/ban", fixture.AdminUsers[0])
Tg.replyMsg(msgUpdate.Message, "/ban", fixture.Vahters[0])
|> fixture.SendMessage
Assert.Equal(HttpStatusCode.OK, banResp.StatusCode)

Expand All @@ -62,12 +62,12 @@ type BanTests(fixture: VahterTestContainers) =
[<Fact>]
let ``Vahter can't ban another vahter`` () = task {
// record a message in a random chat
let msgUpdate = Tg.quickMsg(chat = fixture.ChatsToMonitor[0], from = fixture.AdminUsers[0])
let msgUpdate = Tg.quickMsg(chat = fixture.ChatsToMonitor[0], from = fixture.Vahters[0])
let! _ = fixture.SendMessage msgUpdate

// send the ban message
let! banResp =
Tg.replyMsg(msgUpdate.Message, "/ban", fixture.AdminUsers[1])
Tg.replyMsg(msgUpdate.Message, "/ban", fixture.Vahters[1])
|> fixture.SendMessage
Assert.Equal(HttpStatusCode.OK, banResp.StatusCode)

Expand All @@ -84,7 +84,7 @@ type BanTests(fixture: VahterTestContainers) =

// send the ban message
let! banResp =
Tg.replyMsg(msgUpdate.Message, "/ban", fixture.AdminUsers[0])
Tg.replyMsg(msgUpdate.Message, "/ban", fixture.Vahters[0])
|> fixture.SendMessage
Assert.Equal(HttpStatusCode.OK, banResp.StatusCode)

Expand All @@ -94,7 +94,7 @@ type BanTests(fixture: VahterTestContainers) =

// send the unban message from another vahter
let! banResp =
Tg.quickMsg($"/unban {msgUpdate.Message.From.Id}", chat = fixture.ChatsToMonitor[0], from = fixture.AdminUsers[1])
Tg.quickMsg($"/unban {msgUpdate.Message.From.Id}", chat = fixture.ChatsToMonitor[0], from = fixture.Vahters[1])
|> fixture.SendMessage
Assert.Equal(HttpStatusCode.OK, banResp.StatusCode)

Expand All @@ -111,7 +111,7 @@ type BanTests(fixture: VahterTestContainers) =

// send the ban message
let! banResp =
Tg.replyMsg(msgUpdate.Message, "/ban", fixture.AdminUsers[0])
Tg.replyMsg(msgUpdate.Message, "/ban", fixture.Vahters[0])
|> fixture.SendMessage
Assert.Equal(HttpStatusCode.OK, banResp.StatusCode)

Expand Down
8 changes: 7 additions & 1 deletion src/VahterBanBot.Tests/ContainerTestBase.fs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ type VahterTestContainers() =
// https://learn.microsoft.com/en-us/dotnet/core/compatibility/containers/8.0/aspnet-port
// Azure default port for containers is 80, se we need explicitly set it
.WithEnvironment("ASPNETCORE_HTTP_PORTS", "80")
.WithEnvironment("UPDATE_CHAT_ADMINS", "true")
.WithEnvironment("UPDATE_CHAT_ADMINS_INTERVAL_SEC", "86400")
.DependsOn(flywayContainer)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(80))
.Build()
Expand Down Expand Up @@ -172,10 +174,14 @@ type VahterTestContainers() =
return resp
}

member _.AdminUsers = [
member _.Vahters = [
Tg.user(id = 34, username = "vahter_1")
Tg.user(id = 69, username = "vahter_2")
]

member _.Admins = [
Tg.user(id = 42, username = "just_admin")
]

member _.LogChat = Tg.chat(id = -123, username = "logs")
member _.ChatsToMonitor = [
Expand Down
17 changes: 15 additions & 2 deletions src/VahterBanBot.Tests/MLBanTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,20 @@ type MLBanTests(fixture: VahterTestContainers, _unused: MlAwaitFixture) =
// record a message, where 2 is in a training set as spam word
// ChatsToMonitor[0] doesn't have stopwords
// but it was sent by vahter
let msgUpdate = Tg.quickMsg(chat = fixture.ChatsToMonitor[0], text = "2222222", from = fixture.AdminUsers[0])
let msgUpdate = Tg.quickMsg(chat = fixture.ChatsToMonitor[0], text = "2222222", from = fixture.Vahters[0])
let! _ = fixture.SendMessage msgUpdate

// assert that the message got auto banned
let! msgBanned = fixture.MessageIsAutoBanned msgUpdate.Message
Assert.False msgBanned
}

[<Fact>]
let ``Message is NOT autobanned if it looks like a spam BUT local admin sent it`` () = task {
// record a message, where 2 is in a training set as spam word
// ChatsToMonitor[0] doesn't have stopwords
// but it was sent by local admin
let msgUpdate = Tg.quickMsg(chat = fixture.ChatsToMonitor[0], text = "2222222", from = fixture.Admins[0])
let! _ = fixture.SendMessage msgUpdate

// assert that the message got auto banned
Expand Down Expand Up @@ -83,7 +96,7 @@ type MLBanTests(fixture: VahterTestContainers, _unused: MlAwaitFixture) =

// send a callback to mark it as false-positive
let! callbackId = fixture.GetCallbackId msgUpdate.Message "NotASpam"
let msgCallback = Tg.callback(string callbackId, from = fixture.AdminUsers[0])
let msgCallback = Tg.callback(string callbackId, from = fixture.Vahters[0])
let! _ = fixture.SendMessage msgCallback

// assert it is false-positive
Expand Down
6 changes: 4 additions & 2 deletions src/VahterBanBot/Bot.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ open Telegram.Bot.Types.ReplyMarkups
open VahterBanBot.ML
open VahterBanBot.Types
open VahterBanBot.Utils
open VahterBanBot.UpdateChatAdmins

let botActivity = new ActivitySource("VahterBanBot")

Expand Down Expand Up @@ -463,8 +464,9 @@ let justMessage
use mlActivity = botActivity.StartActivity("mlPrediction")

let shouldBeSkipped =
// skip prediction for vahters
if botConfig.AllowedUsers.ContainsValue message.From.Id then
// skip prediction for vahters or local admins
if botConfig.AllowedUsers.ContainsValue message.From.Id
|| UpdateChatAdmins.Admins.Contains message.From.Id then
true
else

Expand Down
74 changes: 50 additions & 24 deletions src/VahterBanBot/FakeTgApi.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,67 @@ open System
open System.Net
open System.Net.Http
open System.Text
open System.Threading.Tasks
open Newtonsoft.Json
open Telegram.Bot.Types
open Telegram.Bot.Types.Enums
open VahterBanBot.Types

let fakeTgApi (botConf: BotConfiguration) =
{ new DelegatingHandler() with
member x.SendAsync(request, cancellationToken) = task {
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

let url = request.RequestUri.ToString()
if not(url.StartsWith("https://api.telegram.org/bot" + botConf.BotToken)) then
// return 404 for any other request
return new HttpResponseMessage(HttpStatusCode.NotFound)
elif url.EndsWith "/deleteMessage" || url.EndsWith "/banChatMember" then
// respond with "true"
return apiResult "true"
elif url.EndsWith "/sendMessage" then
// respond with the request body as a string
let message =
Message(
MessageId = 1,
Date = DateTime.UtcNow,
Chat = Chat(
Id = 1L,
Type = ChatType.Private
let resp =
if not(url.StartsWith("https://api.telegram.org/bot" + botConf.BotToken)) then
// return 404 for any other request
new HttpResponseMessage(HttpStatusCode.NotFound)
elif url.EndsWith "/deleteMessage" || url.EndsWith "/banChatMember" then
// respond with "true"
apiResult "true"
elif url.EndsWith "/sendMessage" then
// respond with the request body as a string
let message =
Message(
MessageId = 1,
Date = DateTime.UtcNow,
Chat = Chat(
Id = 1L,
Type = ChatType.Private
)
)
)
|> JsonConvert.SerializeObject
return apiResult message
else
// return 500 for any other request
return new HttpResponseMessage(HttpStatusCode.InternalServerError)
}
|> JsonConvert.SerializeObject
apiResult message
elif url.EndsWith "/getChatAdministrators" then
// respond with the request body as a string
let message =
[|
ChatMemberAdministrator(
CanBeEdited = false,
IsAnonymous = false,
CanDeleteMessages = false,
CanManageVideoChats = false,
CanRestrictMembers = false,
CanPromoteMembers = false,
CanChangeInfo = false,
CanInviteUsers = false,
User = User(
Id = 42L,
FirstName = "just_admin",
Username = "just_admin"
)
)
|]
|> JsonConvert.SerializeObject
apiResult message
else
// return 500 for any other request
// TODO pass fucking ILogger here somehow -_-
Console.WriteLine $"Unhandled request: {url}"
new HttpResponseMessage(HttpStatusCode.InternalServerError)
Task.FromResult resp
}
4 changes: 4 additions & 0 deletions src/VahterBanBot/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ open VahterBanBot.Utils
open VahterBanBot.Bot
open VahterBanBot.Types
open VahterBanBot.StartupMessage
open VahterBanBot.UpdateChatAdmins
open VahterBanBot.FakeTgApi
open OpenTelemetry.Trace
open OpenTelemetry.Metrics
Expand All @@ -48,6 +49,8 @@ let botConf =
CleanupOldMessages = getEnvOr "CLEANUP_OLD_MESSAGES" "true" |> bool.Parse
CleanupInterval = getEnvOr "CLEANUP_INTERVAL_SEC" "86400" |> int |> TimeSpan.FromSeconds
CleanupOldLimit = getEnvOr "CLEANUP_OLD_LIMIT_SEC" "259200" |> int |> TimeSpan.FromSeconds
UpdateChatAdminsInterval = getEnvOrWith "UPDATE_CHAT_ADMINS_INTERVAL_SEC" None (int >> TimeSpan.FromSeconds >> Some)
UpdateChatAdmins = getEnvOr "UPDATE_CHAT_ADMINS" "false" |> bool.Parse
MlEnabled = getEnvOr "ML_ENABLED" "false" |> bool.Parse
MlSeed = getEnvOrWith "ML_SEED" (Nullable<int>()) (int >> Nullable)
MlSpamDeletionEnabled = getEnvOr "ML_SPAM_DELETION_ENABLED" "false" |> bool.Parse
Expand All @@ -71,6 +74,7 @@ let builder = WebApplication.CreateBuilder()
.AddGiraffe()
.AddHostedService<CleanupService>()
.AddHostedService<StartupMessage>()
.AddHostedService<UpdateChatAdmins>()
.AddSingleton<MachineLearning>()
.AddHostedService<MachineLearning>(fun sp -> sp.GetRequiredService<MachineLearning>())
.AddHttpClient("telegram_bot_client")
Expand Down
2 changes: 2 additions & 0 deletions src/VahterBanBot/Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type BotConfiguration =
CleanupOldMessages: bool
CleanupInterval: TimeSpan
CleanupOldLimit: TimeSpan
UpdateChatAdminsInterval: TimeSpan option
UpdateChatAdmins: bool
MlEnabled: bool
MlSeed: Nullable<int>
MlSpamDeletionEnabled: bool
Expand Down
58 changes: 58 additions & 0 deletions src/VahterBanBot/UpdateChatAdmins.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module VahterBanBot.UpdateChatAdmins

open System.Collections.Generic
open System.Text
open System.Threading.Tasks
open Microsoft.Extensions.Logging
open Telegram.Bot
open Telegram.Bot.Types
open VahterBanBot.Types
open VahterBanBot.Utils
open System
open System.Threading
open Microsoft.Extensions.Hosting

type UpdateChatAdmins(
logger: ILogger<UpdateChatAdmins>,
telegramClient: ITelegramBotClient,
botConf: BotConfiguration
) =
let mutable timer: Timer = null
static let mutable localAdmins: ISet<int64> = HashSet<int64>()

let updateChatAdmins _ = task {
let sb = StringBuilder()
%sb.AppendLine("New chat admins:")
let result = HashSet<int64>()
for chatId in botConf.ChatsToMonitor.Values do
let! admins = telegramClient.GetChatAdministratorsAsync(ChatId chatId)

// wait a bit so we don't get rate limited
do! Task.Delay 100

for admin in admins do
%result.Add admin.User.Id
%sb.AppendJoin(",", $"{prependUsername admin.User.Username} ({admin.User.Id})")
localAdmins <- result
logger.LogInformation (sb.ToString())
}

static member Admins = localAdmins

interface IHostedService with
member this.StartAsync _ =
if not botConf.IgnoreSideEffects && botConf.UpdateChatAdmins then
if botConf.UpdateChatAdminsInterval.IsSome then
// recurring
timer <- new Timer(TimerCallback(updateChatAdmins >> ignore), null, TimeSpan.Zero, botConf.UpdateChatAdminsInterval.Value)
Task.CompletedTask
else
// once
updateChatAdmins()
else
Task.CompletedTask

member this.StopAsync _ =
match timer with
| null -> Task.CompletedTask
| timer -> timer.DisposeAsync().AsTask()
1 change: 1 addition & 0 deletions src/VahterBanBot/VahterBanBot.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<Compile Include="Types.fs" />
<Compile Include="DB.fs" />
<Compile Include="ML.fs" />
<Compile Include="UpdateChatAdmins.fs" />
<Compile Include="Bot.fs" />
<Compile Include="Cleanup.fs" />
<Compile Include="StartupMessage.fs" />
Expand Down