Initial release of go-telegram

A fully-generated, strongly-typed Go client for the Telegram Bot API.

* 176 methods + 301 types generated from Bot API v10.0
* 1408 auto-generated tests (8 scenarios per method)
* Typed unions throughout — no 'any' in the public surface
* Pluggable HTTP transport and JSON codec (default goccy/go-json)
* Built-in retry middleware honouring Telegram's retry_after
* Generic dispatcher with filters and conversation handlers
* Self-verifying codegen pipeline (regen → audit → emit → run tests)
* 14 example bots covering common patterns
This commit is contained in:
2026-05-09 13:09:27 +01:00
commit ac7cae8fa7
164 changed files with 100239 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
// Package callback provides Filter helpers for *api.CallbackQuery payloads.
package callback
import (
"regexp"
"strings"
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/dispatch"
)
// Data returns a Filter that matches callback queries whose Data matches
// pattern (regex). Panics at registration time on an invalid pattern.
func Data(pattern string) dispatch.Filter[*api.CallbackQuery] {
re := regexp.MustCompile(pattern)
return func(q *api.CallbackQuery) bool {
return q != nil && re.MatchString(q.Data)
}
}
// DataEquals returns a Filter that matches callback queries whose Data equals
// s exactly.
func DataEquals(s string) dispatch.Filter[*api.CallbackQuery] {
return func(q *api.CallbackQuery) bool {
return q != nil && q.Data == s
}
}
// DataPrefix returns a Filter that matches callback queries whose Data starts
// with prefix.
func DataPrefix(prefix string) dispatch.Filter[*api.CallbackQuery] {
return func(q *api.CallbackQuery) bool {
return q != nil && strings.HasPrefix(q.Data, prefix)
}
}
// FromUser returns a Filter that matches callback queries whose From.ID equals
// userID.
func FromUser(userID int64) dispatch.Filter[*api.CallbackQuery] {
return func(q *api.CallbackQuery) bool {
return q != nil && q.From.ID == userID
}
}
@@ -0,0 +1,56 @@
package callback_test
import (
"testing"
"github.com/lukaszraczylo/go-telegram/api"
cbfilter "github.com/lukaszraczylo/go-telegram/dispatch/filters/callback"
"github.com/stretchr/testify/require"
)
func cq(data string, userID int64) *api.CallbackQuery {
return &api.CallbackQuery{
ID: "q",
From: api.User{ID: userID},
Data: data,
}
}
func TestData(t *testing.T) {
f := cbfilter.Data(`^like:\d+$`)
require.True(t, f(cq("like:42", 1)))
require.False(t, f(cq("dislike:42", 1)))
require.False(t, f(nil))
}
func TestData_PanicsOnBadPattern(t *testing.T) {
require.Panics(t, func() { cbfilter.Data(`[bad`) })
}
func TestDataEquals(t *testing.T) {
f := cbfilter.DataEquals("yes")
require.True(t, f(cq("yes", 1)))
require.False(t, f(cq("yes please", 1)))
require.False(t, f(nil))
}
func TestDataPrefix(t *testing.T) {
f := cbfilter.DataPrefix("vote:")
require.True(t, f(cq("vote:up", 1)))
require.False(t, f(cq("novote:up", 1)))
require.False(t, f(nil))
}
func TestFromUser(t *testing.T) {
f := cbfilter.FromUser(7)
require.True(t, f(cq("data", 7)))
require.False(t, f(cq("data", 8)))
require.False(t, f(nil))
}
func TestComposedCallbackFilters(t *testing.T) {
f := cbfilter.DataPrefix("vote:").And(cbfilter.FromUser(7))
require.True(t, f(cq("vote:up", 7)))
require.False(t, f(cq("vote:up", 8)))
require.False(t, f(cq("other", 7)))
}
@@ -0,0 +1,23 @@
// Package chatjoinrequest provides Filter helpers for *api.ChatJoinRequest payloads.
package chatjoinrequest
import (
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/dispatch"
)
// FromUser returns a Filter that matches join requests where the requesting
// user's ID equals uid.
func FromUser(uid int64) dispatch.Filter[*api.ChatJoinRequest] {
return func(r *api.ChatJoinRequest) bool {
return r != nil && r.From.ID == uid
}
}
// InChat returns a Filter that matches join requests directed at the chat
// with the given chat ID.
func InChat(cid int64) dispatch.Filter[*api.ChatJoinRequest] {
return func(r *api.ChatJoinRequest) bool {
return r != nil && r.Chat.ID == cid
}
}
@@ -0,0 +1,37 @@
package chatjoinrequest_test
import (
"testing"
"github.com/lukaszraczylo/go-telegram/api"
cjrfilter "github.com/lukaszraczylo/go-telegram/dispatch/filters/chatjoinrequest"
"github.com/stretchr/testify/require"
)
func joinRequest(fromID, chatID int64) *api.ChatJoinRequest {
return &api.ChatJoinRequest{
Chat: api.Chat{ID: chatID},
From: api.User{ID: fromID},
}
}
func TestFromUser_Matches(t *testing.T) {
f := cjrfilter.FromUser(10)
require.True(t, f(joinRequest(10, 100)))
require.False(t, f(joinRequest(99, 100)))
require.False(t, f(nil))
}
func TestInChat_Matches(t *testing.T) {
f := cjrfilter.InChat(100)
require.True(t, f(joinRequest(10, 100)))
require.False(t, f(joinRequest(10, 200)))
require.False(t, f(nil))
}
func TestComposedFilters(t *testing.T) {
f := cjrfilter.FromUser(10).And(cjrfilter.InChat(100))
require.True(t, f(joinRequest(10, 100)))
require.False(t, f(joinRequest(10, 200)))
require.False(t, f(joinRequest(99, 100)))
}
+41
View File
@@ -0,0 +1,41 @@
// Package chatmember provides Filter helpers for *api.ChatMemberUpdated payloads.
package chatmember
import (
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/dispatch"
)
// NewStatus returns a Filter that matches updates where the new chat member
// status equals s (e.g. "member", "administrator", "kicked", "left").
func NewStatus(s string) dispatch.Filter[*api.ChatMemberUpdated] {
return func(u *api.ChatMemberUpdated) bool {
if u == nil {
return false
}
switch m := u.NewChatMember.(type) {
case *api.ChatMemberOwner:
return m.Status == s
case *api.ChatMemberAdministrator:
return m.Status == s
case *api.ChatMemberMember:
return m.Status == s
case *api.ChatMemberRestricted:
return m.Status == s
case *api.ChatMemberLeft:
return m.Status == s
case *api.ChatMemberBanned:
return m.Status == s
default:
return false
}
}
}
// FromUser returns a Filter that matches updates where the acting user
// (From.ID) equals uid.
func FromUser(uid int64) dispatch.Filter[*api.ChatMemberUpdated] {
return func(u *api.ChatMemberUpdated) bool {
return u != nil && u.From.ID == uid
}
}
@@ -0,0 +1,95 @@
package chatmember_test
import (
"testing"
"github.com/lukaszraczylo/go-telegram/api"
cmfilter "github.com/lukaszraczylo/go-telegram/dispatch/filters/chatmember"
"github.com/stretchr/testify/require"
)
func memberUpdate(status string, fromID int64) *api.ChatMemberUpdated {
var newMember api.ChatMember
switch status {
case "member":
newMember = &api.ChatMemberMember{Status: status}
case "administrator":
newMember = &api.ChatMemberAdministrator{Status: status}
case "kicked":
newMember = &api.ChatMemberBanned{Status: status}
case "left":
newMember = &api.ChatMemberLeft{Status: status}
default:
newMember = &api.ChatMemberMember{Status: status}
}
return &api.ChatMemberUpdated{
From: api.User{ID: fromID},
NewChatMember: newMember,
}
}
func TestNewStatus_Matches(t *testing.T) {
f := cmfilter.NewStatus("member")
require.True(t, f(memberUpdate("member", 1)))
require.False(t, f(memberUpdate("kicked", 1)))
require.False(t, f(nil))
}
func TestNewStatus_Administrator(t *testing.T) {
f := cmfilter.NewStatus("administrator")
require.True(t, f(memberUpdate("administrator", 1)))
require.False(t, f(memberUpdate("member", 1)))
}
func TestNewStatus_Kicked(t *testing.T) {
f := cmfilter.NewStatus("kicked")
require.True(t, f(memberUpdate("kicked", 1)))
require.False(t, f(memberUpdate("left", 1)))
}
func TestNewStatus_Left(t *testing.T) {
f := cmfilter.NewStatus("left")
require.True(t, f(memberUpdate("left", 1)))
require.False(t, f(memberUpdate("member", 1)))
}
func TestFromUser_Matches(t *testing.T) {
f := cmfilter.FromUser(42)
require.True(t, f(memberUpdate("member", 42)))
require.False(t, f(memberUpdate("member", 99)))
require.False(t, f(nil))
}
func TestComposedFilters(t *testing.T) {
f := cmfilter.NewStatus("member").And(cmfilter.FromUser(7))
require.True(t, f(memberUpdate("member", 7)))
require.False(t, f(memberUpdate("member", 8)))
require.False(t, f(memberUpdate("kicked", 7)))
}
func TestNewStatus_Owner(t *testing.T) {
u := &api.ChatMemberUpdated{
From: api.User{ID: 1},
NewChatMember: &api.ChatMemberOwner{Status: "creator"},
}
require.True(t, cmfilter.NewStatus("creator")(u))
require.False(t, cmfilter.NewStatus("member")(u))
}
func TestNewStatus_Restricted(t *testing.T) {
u := &api.ChatMemberUpdated{
From: api.User{ID: 1},
NewChatMember: &api.ChatMemberRestricted{Status: "restricted"},
}
require.True(t, cmfilter.NewStatus("restricted")(u))
require.False(t, cmfilter.NewStatus("member")(u))
}
func TestNewStatus_UnknownType(t *testing.T) {
// nil NewChatMember → default branch → false
u := &api.ChatMemberUpdated{
From: api.User{ID: 1},
NewChatMember: nil,
}
require.False(t, cmfilter.NewStatus("member")(u))
}
+35
View File
@@ -0,0 +1,35 @@
// Package inline provides Filter helpers for *api.InlineQuery payloads.
package inline
import (
"regexp"
"strings"
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/dispatch"
)
// Query returns a Filter that matches inline queries whose Query field matches
// pattern (regex). Panics at registration time on an invalid pattern.
func Query(pattern string) dispatch.Filter[*api.InlineQuery] {
re := regexp.MustCompile(pattern)
return func(q *api.InlineQuery) bool {
return q != nil && re.MatchString(q.Query)
}
}
// QueryEquals returns a Filter that matches inline queries whose Query equals
// s exactly.
func QueryEquals(s string) dispatch.Filter[*api.InlineQuery] {
return func(q *api.InlineQuery) bool {
return q != nil && q.Query == s
}
}
// QueryPrefix returns a Filter that matches inline queries whose Query starts
// with prefix.
func QueryPrefix(prefix string) dispatch.Filter[*api.InlineQuery] {
return func(q *api.InlineQuery) bool {
return q != nil && strings.HasPrefix(q.Query, prefix)
}
}
+45
View File
@@ -0,0 +1,45 @@
package inline_test
import (
"testing"
"github.com/lukaszraczylo/go-telegram/api"
ilfilter "github.com/lukaszraczylo/go-telegram/dispatch/filters/inline"
"github.com/stretchr/testify/require"
)
func iq(query string) *api.InlineQuery {
return &api.InlineQuery{ID: "i", From: api.User{ID: 1}, Query: query}
}
func TestQuery(t *testing.T) {
f := ilfilter.Query(`^find`)
require.True(t, f(iq("find me")))
require.False(t, f(iq("search me")))
require.False(t, f(nil))
}
func TestQuery_PanicsOnBadPattern(t *testing.T) {
require.Panics(t, func() { ilfilter.Query(`[bad`) })
}
func TestQueryEquals(t *testing.T) {
f := ilfilter.QueryEquals("exact")
require.True(t, f(iq("exact")))
require.False(t, f(iq("exact match")))
require.False(t, f(nil))
}
func TestQueryPrefix(t *testing.T) {
f := ilfilter.QueryPrefix("@user")
require.True(t, f(iq("@username")))
require.False(t, f(iq("no prefix")))
require.False(t, f(nil))
}
func TestComposedInlineFilters(t *testing.T) {
f := ilfilter.QueryPrefix("find").Or(ilfilter.QueryEquals("help"))
require.True(t, f(iq("find me")))
require.True(t, f(iq("help")))
require.False(t, f(iq("other")))
}
+142
View File
@@ -0,0 +1,142 @@
// Package message provides Filter helpers for *api.Message payloads.
package message
import (
"regexp"
"strings"
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/dispatch"
)
// Text returns a Filter that matches messages whose Text matches pattern (regex).
// Panics at registration time on an invalid pattern.
func Text(pattern string) dispatch.Filter[*api.Message] {
re := regexp.MustCompile(pattern)
return func(m *api.Message) bool {
return m != nil && re.MatchString(m.Text)
}
}
// TextEquals returns a Filter that matches messages whose Text equals s exactly.
func TextEquals(s string) dispatch.Filter[*api.Message] {
return func(m *api.Message) bool {
return m != nil && m.Text == s
}
}
// TextPrefix returns a Filter that matches messages whose Text starts with prefix.
func TextPrefix(prefix string) dispatch.Filter[*api.Message] {
return func(m *api.Message) bool {
return m != nil && strings.HasPrefix(m.Text, prefix)
}
}
// TextContains returns a Filter that matches messages whose Text contains sub.
func TextContains(sub string) dispatch.Filter[*api.Message] {
return func(m *api.Message) bool {
return m != nil && strings.Contains(m.Text, sub)
}
}
// Command returns a Filter that matches messages whose first entity is a
// bot_command equal to "/<name>" (with or without "@BotName" suffix).
func Command(name string) dispatch.Filter[*api.Message] {
want := "/" + strings.TrimPrefix(name, "/")
return func(m *api.Message) bool {
if m == nil || len(m.Entities) == 0 || m.Text == "" {
return false
}
first := m.Entities[0]
if first.Type != string(api.EntityBotCommand) || first.Offset != 0 {
return false
}
end := int(first.Length)
runes := []rune(m.Text)
if end > len(runes) {
return false
}
cmd := string(runes[:end])
if i := strings.Index(cmd, "@"); i >= 0 {
cmd = cmd[:i]
}
return cmd == want
}
}
// AnyCommand returns a Filter that matches any message starting with a
// bot_command entity at offset 0.
func AnyCommand() dispatch.Filter[*api.Message] {
return func(m *api.Message) bool {
if m == nil || len(m.Entities) == 0 {
return false
}
first := m.Entities[0]
return first.Type == string(api.EntityBotCommand) && first.Offset == 0
}
}
// IsReply returns a Filter that matches messages that have ReplyToMessage set.
func IsReply() dispatch.Filter[*api.Message] {
return func(m *api.Message) bool {
return m != nil && m.ReplyToMessage != nil
}
}
// IsForward returns a Filter that matches messages that have ForwardOrigin set.
func IsForward() dispatch.Filter[*api.Message] {
return func(m *api.Message) bool {
return m != nil && m.ForwardOrigin != nil
}
}
// HasPhoto returns a Filter that matches messages with a Photo attachment.
func HasPhoto() dispatch.Filter[*api.Message] {
return func(m *api.Message) bool {
return m != nil && len(m.Photo) > 0
}
}
// HasDocument returns a Filter that matches messages with a Document attachment.
func HasDocument() dispatch.Filter[*api.Message] {
return func(m *api.Message) bool {
return m != nil && m.Document != nil
}
}
// HasEntity returns a Filter that matches messages whose Entities contain at
// least one entity of type t (e.g. string(api.EntityBotCommand)).
func HasEntity(t string) dispatch.Filter[*api.Message] {
return func(m *api.Message) bool {
if m == nil {
return false
}
for _, e := range m.Entities {
if e.Type == t {
return true
}
}
return false
}
}
// ChatType returns a Filter that matches messages whose Chat.Type equals t.
func ChatType(t api.ChatType) dispatch.Filter[*api.Message] {
return func(m *api.Message) bool {
return m != nil && m.Chat.Type == string(t)
}
}
// FromUser returns a Filter that matches messages whose From.ID equals userID.
func FromUser(userID int64) dispatch.Filter[*api.Message] {
return func(m *api.Message) bool {
return m != nil && m.From != nil && m.From.ID == userID
}
}
// InChat returns a Filter that matches messages whose Chat.ID equals chatID.
func InChat(chatID int64) dispatch.Filter[*api.Message] {
return func(m *api.Message) bool {
return m != nil && m.Chat.ID == chatID
}
}
+188
View File
@@ -0,0 +1,188 @@
package message_test
import (
"testing"
"github.com/lukaszraczylo/go-telegram/api"
msgfilter "github.com/lukaszraczylo/go-telegram/dispatch/filters/message"
"github.com/stretchr/testify/require"
)
func msg(text string) *api.Message {
return &api.Message{
MessageID: 1,
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
Text: text,
}
}
func cmdMsg(cmd string) *api.Message {
text := cmd
return &api.Message{
MessageID: 1,
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
Text: text,
Entities: []api.MessageEntity{
{Type: string(api.EntityBotCommand), Offset: 0, Length: int64(len([]rune(text)))},
},
}
}
func TestText(t *testing.T) {
f := msgfilter.Text(`^hello`)
require.True(t, f(msg("hello world")))
require.False(t, f(msg("world hello")))
require.False(t, f(nil))
}
func TestText_PanicsOnBadPattern(t *testing.T) {
require.Panics(t, func() { msgfilter.Text(`[invalid`) })
}
func TestTextEquals(t *testing.T) {
f := msgfilter.TextEquals("hi")
require.True(t, f(msg("hi")))
require.False(t, f(msg("hi there")))
require.False(t, f(nil))
}
func TestTextPrefix(t *testing.T) {
f := msgfilter.TextPrefix("/start")
require.True(t, f(msg("/start now")))
require.False(t, f(msg("no prefix")))
require.False(t, f(nil))
}
func TestTextContains(t *testing.T) {
f := msgfilter.TextContains("bot")
require.True(t, f(msg("my bot is cool")))
require.False(t, f(msg("nothing here")))
require.False(t, f(nil))
}
func TestCommand(t *testing.T) {
t.Run("matches exact command", func(t *testing.T) {
f := msgfilter.Command("/start")
require.True(t, f(cmdMsg("/start")))
})
t.Run("matches without leading slash", func(t *testing.T) {
f := msgfilter.Command("start")
require.True(t, f(cmdMsg("/start")))
})
t.Run("strips BotName suffix", func(t *testing.T) {
m := &api.Message{
Text: "/start@MyBot",
Entities: []api.MessageEntity{{Type: string(api.EntityBotCommand), Offset: 0, Length: 12}},
}
f := msgfilter.Command("/start")
require.True(t, f(m))
})
t.Run("no match different command", func(t *testing.T) {
f := msgfilter.Command("/stop")
require.False(t, f(cmdMsg("/start")))
})
t.Run("nil message", func(t *testing.T) {
require.False(t, msgfilter.Command("/start")(nil))
})
t.Run("no entities", func(t *testing.T) {
require.False(t, msgfilter.Command("/start")(msg("/start")))
})
}
func TestAnyCommand(t *testing.T) {
f := msgfilter.AnyCommand()
require.True(t, f(cmdMsg("/anything")))
require.False(t, f(msg("plain text")))
require.False(t, f(nil))
}
func TestIsReply(t *testing.T) {
f := msgfilter.IsReply()
m := msg("reply")
m.ReplyToMessage = &api.Message{MessageID: 2}
require.True(t, f(m))
require.False(t, f(msg("no reply")))
require.False(t, f(nil))
}
func TestIsForward(t *testing.T) {
// ForwardOrigin is a MessageOrigin interface; set via a concrete type.
f := msgfilter.IsForward()
m := msg("fwd")
m.ForwardOrigin = &api.MessageOriginUser{Type: "user"}
require.True(t, f(m))
require.False(t, f(msg("no fwd")))
require.False(t, f(nil))
}
func TestHasPhoto(t *testing.T) {
f := msgfilter.HasPhoto()
m := msg("")
m.Photo = []api.PhotoSize{{FileID: "x", Width: 100, Height: 100}}
require.True(t, f(m))
require.False(t, f(msg("no photo")))
require.False(t, f(nil))
}
func TestHasDocument(t *testing.T) {
f := msgfilter.HasDocument()
m := msg("")
m.Document = &api.Document{FileID: "doc1"}
require.True(t, f(m))
require.False(t, f(msg("no doc")))
require.False(t, f(nil))
}
func TestHasEntity(t *testing.T) {
f := msgfilter.HasEntity(string(api.EntityURL))
m := msg("check https://example.com")
m.Entities = []api.MessageEntity{{Type: string(api.EntityURL), Offset: 6, Length: 19}}
require.True(t, f(m))
require.False(t, f(msg("plain")))
require.False(t, f(nil))
}
func TestChatType(t *testing.T) {
f := msgfilter.ChatType(api.ChatTypePrivate)
private := msg("hi")
require.True(t, f(private))
group := msg("hi")
group.Chat.Type = string(api.ChatTypeGroup)
require.False(t, f(group))
require.False(t, f(nil))
}
func TestFromUser(t *testing.T) {
f := msgfilter.FromUser(42)
m := msg("hi")
m.From = &api.User{ID: 42}
require.True(t, f(m))
m2 := msg("hi")
m2.From = &api.User{ID: 99}
require.False(t, f(m2))
require.False(t, f(msg("no from")))
require.False(t, f(nil))
}
func TestInChat(t *testing.T) {
f := msgfilter.InChat(1)
require.True(t, f(msg("hi")))
m2 := msg("hi")
m2.Chat.ID = 2
require.False(t, f(m2))
require.False(t, f(nil))
}
func TestComposedMessageFilters(t *testing.T) {
// private chat AND contains "hello"
f := msgfilter.ChatType(api.ChatTypePrivate).And(msgfilter.TextContains("hello"))
m := msg("say hello")
require.True(t, f(m))
m2 := msg("say hello")
m2.Chat.Type = string(api.ChatTypeGroup)
require.False(t, f(m2))
}
@@ -0,0 +1,23 @@
// Package precheckoutquery provides Filter helpers for *api.PreCheckoutQuery payloads.
package precheckoutquery
import (
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/dispatch"
)
// Currency returns a Filter that matches pre-checkout queries with the given
// ISO 4217 currency code (e.g. "USD", "EUR", "XTR").
func Currency(c string) dispatch.Filter[*api.PreCheckoutQuery] {
return func(q *api.PreCheckoutQuery) bool {
return q != nil && q.Currency == c
}
}
// FromUser returns a Filter that matches pre-checkout queries sent by the
// user with the given ID.
func FromUser(uid int64) dispatch.Filter[*api.PreCheckoutQuery] {
return func(q *api.PreCheckoutQuery) bool {
return q != nil && q.From.ID == uid
}
}
@@ -0,0 +1,38 @@
package precheckoutquery_test
import (
"testing"
"github.com/lukaszraczylo/go-telegram/api"
pcqfilter "github.com/lukaszraczylo/go-telegram/dispatch/filters/precheckoutquery"
"github.com/stretchr/testify/require"
)
func pcq(currency string, fromID int64) *api.PreCheckoutQuery {
return &api.PreCheckoutQuery{
ID: "q",
Currency: currency,
From: api.User{ID: fromID},
}
}
func TestCurrency_Matches(t *testing.T) {
f := pcqfilter.Currency("USD")
require.True(t, f(pcq("USD", 1)))
require.False(t, f(pcq("EUR", 1)))
require.False(t, f(nil))
}
func TestFromUser_Matches(t *testing.T) {
f := pcqfilter.FromUser(5)
require.True(t, f(pcq("USD", 5)))
require.False(t, f(pcq("USD", 9)))
require.False(t, f(nil))
}
func TestComposedFilters(t *testing.T) {
f := pcqfilter.Currency("XTR").And(pcqfilter.FromUser(42))
require.True(t, f(pcq("XTR", 42)))
require.False(t, f(pcq("XTR", 99)))
require.False(t, f(pcq("USD", 42)))
}