mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-15 03:12:02 +00:00
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:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user