diff --git a/cmd/mavend/system_reply_test.go b/cmd/mavend/system_reply_test.go index d04fe54..cfc06d4 100644 --- a/cmd/mavend/system_reply_test.go +++ b/cmd/mavend/system_reply_test.go @@ -57,17 +57,16 @@ func TestReplySystemUnknownDayIsHonest(t *testing.T) { } // TestReplySystemClockCity — the clock arm must not answer local time for a -// question about another city (Vikunja #388). Known cities get their own zone; -// unknown places get an honest "local time only". +// question about another city (Vikunja #388). She keeps one clock, so every +// named place gets the honest "local time only" answer. func TestReplySystemClockCity(t *testing.T) { - // 12:00 UTC — Kyiv is +03 in July, Moscow +03, London +01. now := time.Date(2026, 7, 30, 12, 0, 0, 0, time.UTC) h := systemHandler(now) cases := []struct{ utterance, want string }{ {"который час", "сейчас 12 часов ровно"}, - {"который час в киеве", "в Киеве сейчас 15 часов ровно"}, - {"сколько времени в москве", "в Москве сейчас 15 часов ровно"}, - {"который час в лондоне", "в Лондоне сейчас 13 часов ровно"}, + {"который час в киеве", onlyLocalTimeReply}, + {"сколько времени в москве", onlyLocalTimeReply}, + {"который час в лондоне", onlyLocalTimeReply}, {"который час в бишкеке", onlyLocalTimeReply}, } for _, c := range cases { diff --git a/cmd/mavend/voice.go b/cmd/mavend/voice.go index feb0414..f736b73 100644 --- a/cmd/mavend/voice.go +++ b/cmd/mavend/voice.go @@ -55,9 +55,6 @@ import ( "strings" "sync" "time" - // Embeds the tz database in the binary so time.LoadLocation works even in - // a container image without /usr/share/zoneinfo. Stdlib, offline. - _ "time/tzdata" hexisclient "github.com/kami/hexis/pkg/client" "github.com/kami/maven/internal/audio" @@ -941,56 +938,15 @@ var ruMonths = []string{ "июля", "августа", "сентября", "октября", "ноября", "декабря", } -// onlyLocalTimeReply — the honest answer when the user names a place whose -// time zone we cannot resolve offline. Better than confidently naming the -// wrong city's time. +// onlyLocalTimeReply — the honest answer when the user asks the time somewhere +// other than here. She only keeps one clock, and saying so is better than +// naming the wrong city's time. +// +// There used to be a city→time-zone table here. It was removed on purpose: the +// user only ever asks for local time, so the table was a second list of cities +// to keep in step with the weather one for no gain. const onlyLocalTimeReply = "я знаю только местное время, про другие города пока не скажу." -// cityZone — a city we can answer the clock for: its IANA time zone (resolved -// from the tzdata built into the binary, never over the network) and its -// Russian name in the "в ..." case. -type cityZone struct { - zone string - prepositional string -} - -// cityZones maps a lowercase city stem to its zone. Stems, not full words, so -// "в москве" / "москва" both hit. Keep in sync-ish with the weather city list. -var cityZones = map[string]cityZone{ - "москв": {"Europe/Moscow", "Москве"}, - "moscow": {"Europe/Moscow", "Москве"}, - "питер": {"Europe/Moscow", "Питере"}, - "петербур": {"Europe/Moscow", "Петербурге"}, - "киев": {"Europe/Kyiv", "Киеве"}, - "kyiv": {"Europe/Kyiv", "Киеве"}, - "kiev": {"Europe/Kyiv", "Киеве"}, - "минск": {"Europe/Minsk", "Минске"}, - "лондон": {"Europe/London", "Лондоне"}, - "london": {"Europe/London", "Лондоне"}, - "париж": {"Europe/Paris", "Париже"}, - "paris": {"Europe/Paris", "Париже"}, - "берлин": {"Europe/Berlin", "Берлине"}, - "berlin": {"Europe/Berlin", "Берлине"}, - "нью-йорк": {"America/New_York", "Нью-Йорке"}, - "new york": {"America/New_York", "Нью-Йорке"}, - "токио": {"Asia/Tokyo", "Токио"}, - "tokyo": {"Asia/Tokyo", "Токио"}, - "тбилиси": {"Asia/Tbilisi", "Тбилиси"}, - "екатеринбург": {"Asia/Yekaterinburg", "Екатеринбурге"}, - "новосибирск": {"Asia/Novosibirsk", "Новосибирске"}, - "владивосток": {"Asia/Vladivostok", "Владивостоке"}, -} - -// lookupCityZone finds a known city named in the utterance. -func lookupCityZone(u string) (cityZone, bool) { - for stem, cz := range cityZones { - if strings.Contains(u, stem) { - return cz, true - } - } - return cityZone{}, false -} - // notPlaceAfterV — words that follow "в" without naming a place, so // mentionsUnknownPlace does not mistake them for a city. var notPlaceAfterV = map[string]bool{ @@ -1155,16 +1111,8 @@ func (h *reactiveHandler) replySystem(ctx context.Context, dec router.Decision) switch { case strings.Contains(u, "час") || strings.Contains(u, "врем"): - // "который час в киеве" — answer for the named city when we know its - // time zone locally, never guess. Unknown place: say so plainly. - if city, ok := lookupCityZone(u); ok { - loc, err := time.LoadLocation(city.zone) - if err != nil { - log.Printf("voice: load zone %s: %v", city.zone, err) - return onlyLocalTimeReply - } - return fmt.Sprintf("в %s сейчас %s", city.prepositional, ruClock(now.In(loc))) - } + // "который час в киеве" — she keeps one clock, so any named place gets + // the honest answer. Never local time dressed up as the city's. if mentionsUnknownPlace(u) { return onlyLocalTimeReply }