feat(misc): add VNG disclaimer aliases

This commit is contained in:
2026-07-07 11:19:27 +07:00
parent 5844f08539
commit 5f95f711f3
5 changed files with 95 additions and 39 deletions
+1 -1
View File
@@ -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 |
+45 -19
View File
@@ -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 := `<a href="tg://user?id=42">Anh</a>`
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 := `<a href="tg://user?id=42">thành viên</a>`
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 := `<a href="tg://user?id=42">Anh Le</a>`
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 := `<a href="tg://user?id=42">Anh Le</a>`
if n := strings.Count(got, wantLink); n != 2 {
t.Errorf("reply contains display-name mention %q %d times, want 2: %q", wantLink, n, got)
}
}
+30 -10
View File
@@ -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: <ms-since-epoch> }.
// 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",
+10 -8
View File
@@ -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))
+9 -1
View File
@@ -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"