diff --git a/README.md b/README.md index 6dedf47..a7a3cdb 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Atlas via long polling and an in-process cron scheduler. | Module | What it does | |---|---| | `util` | `/help`, `/info`, `/stickerid` | -| `misc` | `/ping`, `/ping_stats`, `/random`, `/wheelofnames`, `/wheelofnamesbeta`, `/the_answer`, `/trongtruonghop` + `/tth` disclaimer | +| `misc` | `/ping`, `/ping_stats`, `/random`, `/wheelofnames`, `/wheelofnamesbeta`, `/the_answer`, `/trongtruonghop` + `/tth`, `/trongtruonghopvng` + `/tthvng` disclaimers | | `wordle` | Daily Wordle game | | `loldle` | League-of-Legends "guess the champion" | | `lol` | Pro-match schedule (`/lol`, `/lol_tomorrow`, `/lol_this_week`, `/lol_next_week`) + daily push | diff --git a/internal/modules/misc/handlers_test.go b/internal/modules/misc/handlers_test.go index 74d6b99..869980f 100644 --- a/internal/modules/misc/handlers_test.go +++ b/internal/modules/misc/handlers_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "image" "image/gif" "math" @@ -638,17 +639,15 @@ func trongTruongHopUpdate(t *testing.T, text string, from *models.User) *models. return u } -func TestTrongTruongHop_DefaultArgUsesVNG(t *testing.T) { +func TestTrongTruongHop_DefaultText(t *testing.T) { rb, _ := installMisc(t, 999) rb.Bot.ProcessUpdate(context.Background(), trongTruongHopUpdate(t, "/trongtruonghop", &models.User{ID: 7, Username: "boss", FirstName: "Boss"})) got := rb.LastSent().Text() - if !strings.Contains(got, "VNG") { - t.Errorf("reply missing default target VNG: %q", got) - } - if n := strings.Count(got, "@boss"); n != 2 { - t.Errorf("reply mentions @boss %d times, want 2: %q", n, got) + want := fmt.Sprintf(trongTruongHopTemplate, defaultTarget, "@boss", "@boss") + if got != want { + t.Errorf("reply = %q, want %q", got, want) } } @@ -661,8 +660,11 @@ func TestTrongTruongHop_CustomArg(t *testing.T) { if !strings.Contains(got, "Acme Corp") { t.Errorf("reply missing custom arg Acme Corp: %q", got) } - if strings.Contains(got, "VNG") { - t.Errorf("reply unexpectedly contains default VNG: %q", got) + if strings.Contains(got, defaultTarget) { + t.Errorf("reply unexpectedly contains default target: %q", got) + } + if n := strings.Count(got, "@boss"); n != 2 { + t.Errorf("reply mentions @boss %d times, want 2: %q", n, got) } } @@ -694,27 +696,51 @@ func TestTrongTruongHop_HTMLEscapesArg(t *testing.T) { } } -func TestTrongTruongHop_NoUsernameFallsBackToLink(t *testing.T) { +func TestTrongTruongHopVNG_DefaultText(t *testing.T) { rb, _ := installMisc(t, 999) - rb.Bot.ProcessUpdate(context.Background(), trongTruongHopUpdate(t, "/trongtruonghop", - &models.User{ID: 42, FirstName: "Anh"})) // no Username + rb.Bot.ProcessUpdate(context.Background(), trongTruongHopUpdate(t, "/trongtruonghopvng ignored arg", + &models.User{ID: 7, Username: "boss", FirstName: "Boss"})) got := rb.LastSent().Text() - wantLink := `Anh` - if n := strings.Count(got, wantLink); n != 2 { - t.Errorf("reply contains link %q %d times, want 2: %q", wantLink, n, got) + want := fmt.Sprintf(trongTruongHopVNGTemplate, "@boss", "@boss") + if got != want { + t.Errorf("reply = %q, want %q", got, want) } } -func TestTrongTruongHop_EmptyDisplayNameFallsBackToThanhVien(t *testing.T) { +func TestTTHVNGAlias_DefaultText(t *testing.T) { rb, _ := installMisc(t, 999) - rb.Bot.ProcessUpdate(context.Background(), trongTruongHopUpdate(t, "/trongtruonghop", - &models.User{ID: 42})) // no Username, no FirstName/LastName + rb.Bot.ProcessUpdate(context.Background(), trongTruongHopUpdate(t, "/tthvng ignored arg", + &models.User{ID: 7, Username: "boss", FirstName: "Boss"})) got := rb.LastSent().Text() - wantLink := `thành viên` + want := fmt.Sprintf(trongTruongHopVNGTemplate, "@boss", "@boss") + if got != want { + t.Errorf("reply = %q, want %q", got, want) + } +} + +func TestTrongTruongHop_NoUsernameFallsBackToDisplayNameMention(t *testing.T) { + rb, _ := installMisc(t, 999) + rb.Bot.ProcessUpdate(context.Background(), trongTruongHopUpdate(t, "/trongtruonghop", + &models.User{ID: 42, FirstName: "Anh", LastName: "Le"})) + + got := rb.LastSent().Text() + wantLink := `Anh Le` if n := strings.Count(got, wantLink); n != 2 { - t.Errorf("reply contains fallback link %d times, want 2: %q", n, got) + t.Errorf("reply contains display-name mention %q %d times, want 2: %q", wantLink, n, got) + } +} + +func TestTrongTruongHopVNG_NoUsernameFallsBackToDisplayNameMention(t *testing.T) { + rb, _ := installMisc(t, 999) + rb.Bot.ProcessUpdate(context.Background(), trongTruongHopUpdate(t, "/trongtruonghopvng ignored arg", + &models.User{ID: 42, FirstName: "Anh", LastName: "Le"})) + + got := rb.LastSent().Text() + wantLink := `Anh Le` + if n := strings.Count(got, wantLink); n != 2 { + t.Errorf("reply contains display-name mention %q %d times, want 2: %q", wantLink, n, got) } } diff --git a/internal/modules/misc/misc.go b/internal/modules/misc/misc.go index 569fedb..d8d053f 100644 --- a/internal/modules/misc/misc.go +++ b/internal/modules/misc/misc.go @@ -2,7 +2,7 @@ // /ping (public, exercises KV write), /ping_stats (protected, exercises KV // read), /random (public random picker), /wheelofnames (public streaming // random picker), /wheelofnamesbeta (public GIF wheel picker), /the_answer -// (private easter egg). +// (private easter egg), and small public disclaimer commands. package misc import ( @@ -26,13 +26,17 @@ import ( const lastPingKey = "last_ping" // defaultTarget is the substituted "investigator" name when /trongtruonghop is -// invoked without an argument. -const defaultTarget = "VNG" +// invoked without an argument. The command keeps a custom-arg override. +const defaultTarget = "các cơ quan trực thuộc Bộ CA hoặc các tổ chức chính trị tương tự phục vụ cho nhà nước CHXHCNVN" // trongTruongHopTemplate is the disclaimer rendered by /trongtruonghop. Three // %s slots: target (escaped), sender mention, sender mention. const trongTruongHopTemplate = "Trong trường hợp nhóm này bị điều tra bởi %s, %s khẳng định không liên quan tới nhóm hoặc những cá nhân khác trong nhóm này. %s không rõ tại sao lại có mặt ở đây vào thời điểm này, có lẽ tài khoản đã được thêm bởi một bên thứ ba." +// trongTruongHopVNGTemplate is the fixed-target disclaimer rendered by +// /trongtruonghopvng. Two %s slots: sender mention, sender mention. +const trongTruongHopVNGTemplate = "Trong trường hợp nhóm này bị điều tra bởi công ty cổ phần tập đoàn VNG nói chung và công ty 2morebits nói riêng, %s khẳng định không liên quan tới nhóm hoặc những cá nhân khác trong nhóm này. %s không rõ tại sao lại có mặt ở đây vào thời điểm này, có lẽ tài khoản đã được thêm bởi một bên thứ ba." + // lastPing is the value stored at the `last_ping` key: { at: }. // int64 ms-epoch (not time.Time → RFC3339) keeps the on-disk shape compact // and consistent with every other timestamp field in the bot's KV. @@ -54,6 +58,8 @@ func New(deps modules.Deps) modules.Module { theAnswerCommand(), trongTruongHopCommand("trongtruonghop"), trongTruongHopCommand("tth"), + trongTruongHopVNGCommand("trongtruonghopvng"), + trongTruongHopVNGCommand("tthvng"), }, } } @@ -104,11 +110,9 @@ func pingStatsCommand(store storage.DocStore[lastPing]) modules.Command { } } -// senderMention renders the mention used inside the trongtruonghop template. -// Prefer @username (Telegram resolves it server-side and enforces a safe -// charset). Fall back to a tg://user?id link with the user's display name when -// the account has no username; escape the name because first/last names can -// legitimately contain '<' or '&'. +// senderMention renders the mention used inside disclaimer templates. Prefer +// @username; when absent, link the sender's display name so Telegram still +// mentions the account. func senderMention(u *models.User) string { if u == nil { return "thành viên" @@ -127,9 +131,9 @@ func trongTruongHopCommand(name string) modules.Command { return modules.Command{ Name: name, Visibility: modules.VisibilityPublic, - Description: "Phát biểu disclaimer cho thành viên hiện tại", + Description: "Phát biểu disclaimer mặc định", Handler: func(ctx context.Context, b *bot.Bot, update *models.Update) error { - if update.Message == nil || update.Message.From == nil { + if update.Message == nil { return nil } arg := strings.TrimSpace(chathelper.ArgAfterCommand(update.Message.Text)) @@ -143,6 +147,22 @@ func trongTruongHopCommand(name string) modules.Command { } } +func trongTruongHopVNGCommand(name string) modules.Command { + return modules.Command{ + Name: name, + Visibility: modules.VisibilityPublic, + Description: "Phát biểu disclaimer VNG mặc định", + Handler: func(ctx context.Context, b *bot.Bot, update *models.Update) error { + if update.Message == nil { + return nil + } + mention := senderMention(update.Message.From) + text := fmt.Sprintf(trongTruongHopVNGTemplate, mention, mention) + return chathelper.ReplyHTML(ctx, b, update.Message, text) + }, + } +} + func theAnswerCommand() modules.Command { return modules.Command{ Name: "the_answer", diff --git a/internal/modules/misc/misc_test.go b/internal/modules/misc/misc_test.go index 6af94bd..5f8e415 100644 --- a/internal/modules/misc/misc_test.go +++ b/internal/modules/misc/misc_test.go @@ -21,14 +21,16 @@ func TestNew_RegistersExpectedCommands(t *testing.T) { mod := New(deps) want := map[string]modules.Visibility{ - "ping": modules.VisibilityPublic, - "ping_stats": modules.VisibilityProtected, - "random": modules.VisibilityPublic, - "wheelofnames": modules.VisibilityPublic, - "wheelofnamesbeta": modules.VisibilityPublic, - "the_answer": modules.VisibilityPrivate, - "trongtruonghop": modules.VisibilityPublic, - "tth": modules.VisibilityPublic, + "ping": modules.VisibilityPublic, + "ping_stats": modules.VisibilityProtected, + "random": modules.VisibilityPublic, + "wheelofnames": modules.VisibilityPublic, + "wheelofnamesbeta": modules.VisibilityPublic, + "the_answer": modules.VisibilityPrivate, + "trongtruonghop": modules.VisibilityPublic, + "tth": modules.VisibilityPublic, + "trongtruonghopvng": modules.VisibilityPublic, + "tthvng": modules.VisibilityPublic, } if len(mod.Commands) != len(want) { t.Fatalf("commands count = %d, want %d", len(mod.Commands), len(want)) diff --git a/telegram-commands.json b/telegram-commands.json index 7be1344..83a2d5a 100644 --- a/telegram-commands.json +++ b/telegram-commands.json @@ -22,12 +22,20 @@ }, { "command": "trongtruonghop", - "description": "Phát biểu disclaimer cho thành viên hiện tại" + "description": "Phát biểu disclaimer mặc định" }, { "command": "tth", "description": "Alias for /trongtruonghop" }, + { + "command": "trongtruonghopvng", + "description": "Phát biểu disclaimer VNG mặc định" + }, + { + "command": "tthvng", + "description": "Alias for /trongtruonghopvng" + }, { "command": "wordle", "description": "Classic wordle; guess the 5-letter word"