├── .gitattributes
├── .github
├── dependabot.yml
└── workflows
│ └── go.yml
├── .gitignore
├── LICENSE
├── README.md
├── api.gen.go
├── api.go
├── api_errors.go
├── api_errors_test.go
├── api_extended.go
├── api_extended_test.go
├── api_unmarshalers.go
├── ask.go
├── bot.go
├── cmd
├── helper_interface.go
├── helper_method.go
├── helper_type.go
├── helpers.go
├── main.go
├── parser.go
└── template.gotmpl
├── examples
├── basic
│ └── main.go
├── force_join
│ └── main.go
└── simplest
│ └── main.go
├── file.go
├── filters
├── README.md
├── filter.go
├── filter_utils.go
├── logical.go
├── message.go
└── updates.go
├── go.mod
├── go.sum
├── routers
├── README.md
├── callback
│ ├── context.go
│ ├── router.go
│ └── router_test.go
├── message
│ ├── context.go
│ ├── router.go
│ └── router_test.go
├── router.go
└── router_test.go
└── tgo.go
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | # Maintain dependencies for GitHub Actions
5 | - package-ecosystem: "github-actions"
6 | directory: "/"
7 | schedule:
8 | interval: "weekly"
9 | groups:
10 | all:
11 | patterns:
12 | - "*"
13 |
14 | # Maintain dependencies for Golang
15 | - package-ecosystem: "gomod"
16 | directory: "/"
17 | schedule:
18 | interval: "monthly"
19 | groups:
20 | all:
21 | patterns:
22 | - "*"
23 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Test & Check
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | pull_request:
7 | branches: ["main"]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: Set up Go
16 | uses: actions/setup-go@v5
17 | with:
18 | go-version: "1.19"
19 |
20 | - name: Test
21 | run: go test -v ./...
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | _temp
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Ali Hashemi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TGO
2 |
3 | [](https://pkg.go.dev/github.com/haashemi/tgo)
4 | [](https://goreportcard.com/report/github.com/haashemi/tgo)
5 | [](https://github.com/haashemi/tgo/actions)
6 | [](https://raw.githubusercontent.com/haashemi/tgo/master/LICENSE)
7 |
8 | TGO is a simple, flexible, and fully featured telegram-bot-api framework for Go developers.
9 |
10 | It gives you the ability to implement your own filters, middlewares, and even routers!
11 |
12 | All API methods and types are all code generated from the [telegram's documentation](https://core.telegram.org/bots/api) in `./cmd` at the `api.gen.go` file.
13 |
14 | ## Installation
15 |
16 | As tgo is currently work-in-progress, we recommend you be on the latest git commit instead of git release.
17 |
18 | ```bash
19 | $ go get -u github.com/haashemi/tgo@main
20 | ```
21 |
22 | ## Usage
23 |
24 | More detailed examples can be found in the [examples folder](/examples/)
25 |
26 | > Help us add more useful examples by doing a PR!
27 |
28 | ### Basic
29 |
30 | ```go
31 | package main
32 |
33 | import (
34 | "fmt"
35 | "log"
36 | "time"
37 |
38 | "github.com/haashemi/tgo"
39 | "github.com/haashemi/tgo/filters"
40 | "github.com/haashemi/tgo/routers/message"
41 | )
42 |
43 | const BotToken = "bot_token"
44 |
45 | func main() {
46 | bot := tgo.NewBot(BotToken, tgo.Options{ DefaultParseMode: tgo.ParseModeHTML })
47 |
48 | info, err := bot.GetMe()
49 | if err != nil {
50 | log.Fatalln("Failed to fetch the bot info", err.Error())
51 | }
52 |
53 | mr := message.NewRouter()
54 | mr.Handle(filters.Command("start", info.Username), Start)
55 | bot.AddRouter(mr)
56 |
57 | for {
58 | log.Println("Polling started as", info.Username)
59 |
60 | if err := bot.StartPolling(30, "message"); err != nil {
61 | log.Fatalln("Polling failed >>", err.Error())
62 | time.Sleep(time.Second * 5)
63 | }
64 | }
65 | }
66 |
67 | func Start(ctx *message.Context) {
68 | text := fmt.Sprintf("Hi %s!", ctx.Message.From.FirstName)
69 |
70 | ctx.Reply(&tgo.SendMessage{Text: text})
71 | }
72 | ```
73 |
74 | ### Uploading a file
75 |
76 | ```go
77 | // FileID of the photo that you want to reuse.
78 | photo := tgo.FileFromID("telegram-file-id")
79 |
80 | // Image URL from somewhere else.
81 | photo = tgo.FileFromURL("https://cataas.com/cat")
82 |
83 | // Local file path, used when you ran telegram-bot-api locally.
84 | photo = tgo.FileFromPath("/home/tgo/my-image.png")
85 |
86 | // When you want to upload and image by yourself,
87 | // which is something that you'll usually do.
88 | photo = tgo.FileFromReader("my-awesome-image.png", reader)
89 |
90 | bot.Send(&tgo.SendPhoto{
91 | ChatId: tgo.ID(0000000),
92 | Photo: photo,
93 | })
94 | ```
95 |
96 | ### Ask a Question
97 |
98 | It happens when you want to ask a question from the user and wait a few seconds for their response.
99 |
100 | ```go
101 | // the question we want to ask.
102 | var msg tgo.Sendable = &tgo.SendMessage{ Text: "Do you like tgo?" }
103 |
104 | // Here, we'll ask the question from the user (userId) in the chat (chatId)
105 | // and wait for their response for 30 seconds.
106 | //
107 | // if the user don't responds in 30 seconds, context.DeadlineExceeded error
108 | // will be returned.
109 | question, answer, err := bot.Ask(chatId, userId, msg, time.Seconds*30)
110 | if err != nil {
111 | // handle the error
112 | }
113 | // do whatever you want with the Q & A.
114 | ```
115 |
116 | ### Sessions
117 |
118 | You may want to store some temporarily in-memory data for the user, tgo's Bot Session got you.
119 |
120 | ```go
121 | // simple as that! it will return a *sync.Map
122 | session := bot.GetSession(userID)
123 |
124 | // using message or callback router:
125 | session := ctx.Session()
126 | ```
127 |
128 | ### Routers
129 |
130 | As you've read from the beginning, you're able to implement your own routers in the way you want. There are currently three built-in routers which are `message`, `callback`, and the raw update handlers.
131 |
132 | Read more in the [routers section](/routers/)
133 |
134 | ### Polling
135 |
136 | Polling is the easiest part of the bot. (it has to be.)
137 | You just add your routers using [bot.AddRouter](https://pkg.go.dev/github.com/haashemi/tgo#Bot.AddRouter), and just do [bot.StartPolling](https://pkg.go.dev/github.com/haashemi/tgo#Bot.StartPolling). Here's an example:
138 |
139 | ```go
140 | // initialize the bot
141 | bot := tgo.NewBot("...", tgo.Options{})
142 |
143 | // initialize our routers
144 | mr := messages.NewRouter()
145 | mr.Handle(myFilter1, myHandler1)
146 | mr.Handle(myFilter2, myHandler2)
147 |
148 | mrPrivate := messages.NewRouter(mySecurityMiddleware)
149 | mrPrivate.Handle(myFilter, myPrivateHandler)
150 |
151 | cr := callback.NewRouter()
152 | cr.Handle(myFilter3, myCallbackHandler)
153 |
154 | // add our routers to the bot in the order we want
155 | bot.AddRouter(mr)
156 | bot.AddRouter(mrPrivate)
157 | bot.AddRouter(cr)
158 |
159 | // start polling with the timeout of 30 seconds
160 | // and only listen for message and callback_query updates.
161 | err := bot.StartPolling(30, "message", "callback_query")
162 | // handle the errors here.
163 | ```
164 |
165 | ## Contributions
166 |
167 | 1. Open an issue and describe what you're gonna do.
168 | 2. Fork, Clone, and do a Pull Request!
169 |
170 | All type of contributions are highly appreciated whatever it would be adding new features, fixing bugs, writing tests or docs, improving the current docs' grammars, or even fixing the typos!
171 |
--------------------------------------------------------------------------------
/api.go:
--------------------------------------------------------------------------------
1 | package tgo
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io"
7 | "mime/multipart"
8 | "net/http"
9 | )
10 |
11 | const TelegramHost = "https://api.telegram.org"
12 |
13 | type httpResponse[T any] struct {
14 | OK bool `json:"ok"`
15 | Result T `json:"result,omitempty"`
16 | Error
17 | }
18 |
19 | // API is a telegram bot API client instance.
20 | type API struct {
21 | host string
22 | token string
23 | client *http.Client
24 | }
25 |
26 | // NewAPI creates a new instance of the Telegram API client.
27 | // It takes a token, an optional host (default is "https://api.telegram.org"),
28 | // and an optional http.Client (default is a new http.Client instance).
29 | // It returns a pointer to the API struct.
30 | func NewAPI(token, host string, client *http.Client) *API {
31 | if host == "" {
32 | host = TelegramHost
33 | }
34 |
35 | if client == nil {
36 | client = &http.Client{}
37 | }
38 |
39 | return &API{
40 | host: host,
41 | token: token,
42 | client: client,
43 | }
44 | }
45 |
46 | // Download downloads a file from the Telegram server.
47 | // It takes the filePath obtained from GetFile and returns an http.Response
48 | // and an error. Please note that the filePath is not the same as the fileID.
49 | func (api *API) Download(filePath string) (*http.Response, error) {
50 | return api.client.Get(api.host + "/file/bot" + api.token + "/" + filePath)
51 | }
52 |
53 | func callJson[T any](a *API, method string, rawData any) (T, error) {
54 | var response httpResponse[T]
55 |
56 | body := bytes.NewBuffer(nil)
57 | if err := json.NewEncoder(body).Encode(rawData); err != nil {
58 | return response.Result, err
59 | }
60 |
61 | resp, err := a.client.Post(a.host+"/bot"+a.token+"/"+method, "application/json", body)
62 | if err != nil {
63 | return response.Result, err
64 | }
65 | defer resp.Body.Close()
66 |
67 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil {
68 | return response.Result, err
69 | } else if !response.OK {
70 | return response.Result, response.Error
71 | }
72 |
73 | return response.Result, nil
74 | }
75 |
76 | func callMultipart[T any](a *API, method string, params map[string]string, files map[string]*InputFile) (T, error) {
77 | r, w := io.Pipe()
78 | defer r.Close()
79 |
80 | m := multipart.NewWriter(w)
81 |
82 | go func() {
83 | defer w.Close()
84 | defer m.Close()
85 |
86 | for key, val := range params {
87 | m.WriteField(key, val)
88 | }
89 |
90 | for key, file := range files {
91 | ww, err := m.CreateFormFile(key, file.Value)
92 | if err != nil {
93 | w.CloseWithError(err)
94 | return
95 | } else if _, err = io.Copy(ww, file.Reader); err != nil {
96 | w.CloseWithError(err)
97 | return
98 | }
99 | }
100 | }()
101 |
102 | var response httpResponse[T]
103 |
104 | resp, err := a.client.Post(a.host+"/bot"+a.token+"/"+method, m.FormDataContentType(), r)
105 | if err != nil {
106 | return response.Result, nil
107 | }
108 | defer resp.Body.Close()
109 |
110 | if err = json.NewDecoder(resp.Body).Decode(&response); err != nil {
111 | return response.Result, err
112 | } else if !response.OK {
113 | return response.Result, response.Error
114 | }
115 |
116 | return response.Result, nil
117 | }
118 |
--------------------------------------------------------------------------------
/api_errors.go:
--------------------------------------------------------------------------------
1 | package tgo
2 |
3 | import (
4 | "regexp"
5 | "time"
6 | )
7 |
8 | var RateLimitDescriptionRegex = regexp.MustCompile(`^Too Many Requests: retry after [0-9]+$`)
9 |
10 | type Error struct {
11 | ErrorCode int `json:"error_code,omitempty"`
12 | Description string `json:"description,omitempty"`
13 | Parameters *ResponseParameters `json:"parameters,omitempty"`
14 | }
15 |
16 | func (e Error) Error() string { return e.Description }
17 |
18 | // A list of most of the common/possible API errors.
19 | //
20 | // source: https://github.com/TelegramBotAPI/errors/tree/ab18477b56d5894e61a015c0775cad20721faa8d
21 | var (
22 | // Bot token is incorrect.
23 | ErrUnauthorized = Error{ErrorCode: 401, Description: "Unauthorized"}
24 |
25 | // The chat is unknown to the bot.
26 | ErrChatNotFound = Error{ErrorCode: 400, Description: "Bad Request: chat not found"}
27 |
28 | // UserId is incorrect.
29 | ErrUserNotFound = Error{ErrorCode: 400, Description: "[Error]: Bad Request: user not found"}
30 |
31 | // You're trying to perform an action on a user account that has been deactivated or deleted.
32 | ErrUserIsDeactivated = Error{ErrorCode: 403, Description: "Forbidden: user is deactivated"}
33 |
34 | // Bot was kicked.
35 | ErrBotWasKicked = Error{ErrorCode: 403, Description: "Forbidden: bot was kicked from the group chat"}
36 |
37 | // The user have blocked the bot.
38 | ErrBotBlockedByUser = Error{ErrorCode: 403, Description: "Forbidden: bot was blocked by the user"}
39 |
40 | // You tried to send a message to another bot. This is not possible.
41 | ErrBotCantSendMessageToBots = Error{ErrorCode: 403, Description: "Forbidden: bot can't send messages to bots"}
42 |
43 | // Occurs when a group chat has been converted/migrated to a supergroup.
44 | //
45 | // NOTE: DO NOT use this error as it doesn't contain parameters. use IsGroupMigratedToSupergroupErr instead.
46 | ErrGroupMigratedToSupergroup = Error{ErrorCode: 400, Description: "Bad Request: group chat was migrated to a supergroup chat"}
47 |
48 | // The file id you are trying to retrieve doesn't exist.
49 | ErrInvalidFileID = Error{ErrorCode: 400, Description: "Bad Request: invalid file id"}
50 |
51 | // The current and new message text and reply markups are the same.
52 | ErrMessageNotModified = Error{ErrorCode: 400, Description: "Bad Request: message is not modified"}
53 |
54 | // You have already set up a webhook and are trying to get the updates via getUpdates.
55 | ErrTerminatedByOtherLongPoll = Error{ErrorCode: 409, Description: "Conflict: terminated by other long poll or webhook"}
56 |
57 | // Occurs when the ``action'' property value is invalid.
58 | ErrWrongParameterActionInRequest = Error{ErrorCode: 400, Description: "Bad Request: wrong parameter action in request"}
59 |
60 | // The message text is empty or not provided.
61 | ErrMessageTextIsEmpty = Error{ErrorCode: 400, Description: "Bad Request: message text is empty"}
62 |
63 | // The message text cannot be edited.
64 | ErrMessageCantBeEdited = Error{ErrorCode: 400, Description: "Bad Request: message can't be edited"}
65 |
66 | // You are trying to use getUpdates while a webhook is active.
67 | ErrCantUseGetUpdatesWithWebhook = Error{ErrorCode: 409, Description: "Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first"}
68 | )
69 |
70 | // IsRateLimitErr returns rate-limited duration and yes as true if the error is
71 | // about when you are hitting the API limit.
72 | func IsRateLimitErr(err error) (retryAfter time.Duration, yes bool) {
73 | tgErr, isTgError := err.(Error)
74 | if !isTgError {
75 | return 0, false
76 | }
77 |
78 | codeMatches := tgErr.ErrorCode == 429
79 | descriptionMatches := RateLimitDescriptionRegex.Match([]byte(tgErr.Description))
80 |
81 | if codeMatches && descriptionMatches && tgErr.Parameters != nil {
82 | return time.Duration(tgErr.Parameters.RetryAfter) * time.Second, true
83 | }
84 |
85 | return 0, false
86 | }
87 |
88 | // IsGroupMigratedToSupergroupErr returns new ChatID and yes as true if the error
89 | // is about when a group chat has been converted/migrated to a supergroup.
90 | func IsGroupMigratedToSupergroupErr(err error) (newChatID int64, yes bool) {
91 | tgErr, isTgError := err.(Error)
92 | if !isTgError {
93 | return 0, false
94 | }
95 |
96 | if tgErr.ErrorCode == ErrGroupMigratedToSupergroup.ErrorCode &&
97 | tgErr.Description == ErrGroupMigratedToSupergroup.Description &&
98 | tgErr.Parameters != nil {
99 | return tgErr.Parameters.MigrateToChatId, true
100 | }
101 |
102 | return 0, false
103 | }
104 |
--------------------------------------------------------------------------------
/api_errors_test.go:
--------------------------------------------------------------------------------
1 | package tgo
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "math/rand"
7 | "testing"
8 | "time"
9 | )
10 |
11 | func TestIsRateLimitErr(t *testing.T) {
12 | retryAfterTime := rand.Int63()
13 | rateLimitError := Error{
14 | ErrorCode: 429,
15 | Description: fmt.Sprintf("Too Many Requests: retry after %d", retryAfterTime),
16 | Parameters: &ResponseParameters{RetryAfter: retryAfterTime},
17 | }
18 |
19 | tests := []struct {
20 | Name string
21 | Error error
22 | WantRetryAfter time.Duration
23 | WantYes bool
24 | }{
25 | {Name: "Not tgo error", Error: errors.New("a random error"), WantRetryAfter: time.Duration(0), WantYes: false},
26 | {Name: "Not RateLimit error", Error: ErrBotWasKicked, WantRetryAfter: time.Duration(0), WantYes: false},
27 | {Name: "RateLimit error", Error: rateLimitError, WantRetryAfter: time.Duration(retryAfterTime) * time.Second, WantYes: true},
28 | }
29 |
30 | for _, test := range tests {
31 | t.Run(test.Name, func(t *testing.T) {
32 | haveRetryAfter, haveYes := IsRateLimitErr(test.Error)
33 |
34 | if haveRetryAfter != test.WantRetryAfter {
35 | t.Errorf("unmatched retry after. got: %v, expected: %v", haveRetryAfter, test.WantRetryAfter)
36 | } else if haveYes != test.WantYes {
37 | t.Errorf("unmatched yes boolean. got: %v, expected: %v", haveYes, test.WantYes)
38 | }
39 | })
40 | }
41 | }
42 |
43 | func TestIsGroupMigratedToSupergroupErr(t *testing.T) {
44 | newChatID := rand.Int63()
45 | groupMigratedToSupergroupError := Error{
46 | ErrorCode: ErrGroupMigratedToSupergroup.ErrorCode,
47 | Description: ErrGroupMigratedToSupergroup.Description,
48 | Parameters: &ResponseParameters{MigrateToChatId: newChatID},
49 | }
50 |
51 | tests := []struct {
52 | Name string
53 | Error error
54 | WantNewChatID int64
55 | WantYes bool
56 | }{
57 | {Name: "Not tgo error", Error: errors.New("a random error"), WantNewChatID: 0, WantYes: false},
58 | {Name: "Not GroupMigratedToSupergroup error", Error: ErrInvalidFileID, WantNewChatID: 0, WantYes: false},
59 | {Name: "GroupMigratedToSupergroup error", Error: groupMigratedToSupergroupError, WantNewChatID: newChatID, WantYes: true},
60 | }
61 |
62 | for _, test := range tests {
63 | t.Run(test.Name, func(t *testing.T) {
64 | haveNewChatID, haveYes := IsGroupMigratedToSupergroupErr(test.Error)
65 |
66 | if haveNewChatID != test.WantNewChatID {
67 | t.Errorf("unmatched new chat id. got: %v, expected: %v", haveNewChatID, test.WantNewChatID)
68 | } else if haveYes != test.WantYes {
69 | t.Errorf("unmatched yes boolean. got: %v, expected: %v", haveYes, test.WantYes)
70 | }
71 | })
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/api_extended.go:
--------------------------------------------------------------------------------
1 | package tgo
2 |
3 | // ParseMode is a type that represents the parse mode of a message.
4 | // eg. Markdown, HTML, etc.
5 | type ParseMode string
6 |
7 | const (
8 | // does not parse the message
9 | ParseModeNone ParseMode = ""
10 | // parses the message as Markdown
11 | ParseModeMarkdown ParseMode = "Markdown"
12 | // parses the message as Markdown but using telegram's V2 markdown
13 | ParseModeMarkdownV2 ParseMode = "MarkdownV2"
14 | // parses the message as HTML
15 | ParseModeHTML ParseMode = "HTML"
16 | )
17 |
18 | // Username implements a string ChatID.
19 | type Username string
20 |
21 | // IsChatID does nothing but implements ChatID interface.
22 | func (Username) IsChatID() {}
23 |
24 | // ID implements a int64 ChatID.
25 | type ID int64
26 |
27 | // IsChatID does nothing but implements ChatID interface.
28 | func (ID) IsChatID() {}
29 |
30 | // GetChatID implements Sendable's GetChatID method.
31 | func (x *SendAnimation) GetChatID() ChatID { return x.ChatId }
32 |
33 | // GetChatID implements Sendable's GetChatID method.
34 | func (x *SendAudio) GetChatID() ChatID { return x.ChatId }
35 |
36 | // GetChatID implements Sendable's GetChatID method.
37 | func (x *SendContact) GetChatID() ChatID { return x.ChatId }
38 |
39 | // GetChatID implements Sendable's GetChatID method.
40 | func (x *SendDice) GetChatID() ChatID { return x.ChatId }
41 |
42 | // GetChatID implements Sendable's GetChatID method.
43 | func (x *SendDocument) GetChatID() ChatID { return x.ChatId }
44 |
45 | // GetChatID implements Sendable's GetChatID method.
46 | func (x *SendGame) GetChatID() ChatID { return ID(x.ChatId) }
47 |
48 | // GetChatID implements Sendable's GetChatID method.
49 | func (x *SendInvoice) GetChatID() ChatID { return x.ChatId }
50 |
51 | // GetChatID implements Sendable's GetChatID method.
52 | func (x *SendLocation) GetChatID() ChatID { return x.ChatId }
53 |
54 | // GetChatID implements Sendable's GetChatID method.
55 | func (x *SendMessage) GetChatID() ChatID { return x.ChatId }
56 |
57 | // GetChatID implements Sendable's GetChatID method.
58 | func (x *SendPhoto) GetChatID() ChatID { return x.ChatId }
59 |
60 | // GetChatID implements Sendable's GetChatID method.
61 | func (x *SendPoll) GetChatID() ChatID { return x.ChatId }
62 |
63 | // GetChatID implements Sendable's GetChatID method.
64 | func (x *SendSticker) GetChatID() ChatID { return x.ChatId }
65 |
66 | // GetChatID implements Sendable's GetChatID method.
67 | func (x *SendVenue) GetChatID() ChatID { return x.ChatId }
68 |
69 | // GetChatID implements Sendable's GetChatID method.
70 | func (x *SendVideo) GetChatID() ChatID { return x.ChatId }
71 |
72 | // GetChatID implements Sendable's GetChatID method.
73 | func (x *SendVideoNote) GetChatID() ChatID { return x.ChatId }
74 |
75 | // GetChatID implements Sendable's GetChatID method.
76 | func (x *SendVoice) GetChatID() ChatID { return x.ChatId }
77 |
78 | // SetChatID implements Sendable's SetChatID method.
79 | func (x *SendAnimation) SetChatID(id int64) { x.ChatId = ID(id) }
80 |
81 | // SetChatID implements Sendable's SetChatID method.
82 | func (x *SendAudio) SetChatID(id int64) { x.ChatId = ID(id) }
83 |
84 | // SetChatID implements Sendable's SetChatID method.
85 | func (x *SendContact) SetChatID(id int64) { x.ChatId = ID(id) }
86 |
87 | // SetChatID implements Sendable's SetChatID method.
88 | func (x *SendDice) SetChatID(id int64) { x.ChatId = ID(id) }
89 |
90 | // SetChatID implements Sendable's SetChatID method.
91 | func (x *SendDocument) SetChatID(id int64) { x.ChatId = ID(id) }
92 |
93 | // SetChatID implements Sendable's SetChatID method.
94 | func (x *SendGame) SetChatID(id int64) { x.ChatId = id }
95 |
96 | // SetChatID implements Sendable's SetChatID method.
97 | func (x *SendInvoice) SetChatID(id int64) { x.ChatId = ID(id) }
98 |
99 | // SetChatID implements Sendable's SetChatID method.
100 | func (x *SendLocation) SetChatID(id int64) { x.ChatId = ID(id) }
101 |
102 | // SetChatID implements Sendable's SetChatID method.
103 | func (x *SendMessage) SetChatID(id int64) { x.ChatId = ID(id) }
104 |
105 | // SetChatID implements Sendable's SetChatID method.
106 | func (x *SendPhoto) SetChatID(id int64) { x.ChatId = ID(id) }
107 |
108 | // SetChatID implements Sendable's SetChatID method.
109 | func (x *SendPoll) SetChatID(id int64) { x.ChatId = ID(id) }
110 |
111 | // SetChatID implements Sendable's SetChatID method.
112 | func (x *SendSticker) SetChatID(id int64) { x.ChatId = ID(id) }
113 |
114 | // SetChatID implements Sendable's SetChatID method.
115 | func (x *SendVenue) SetChatID(id int64) { x.ChatId = ID(id) }
116 |
117 | // SetChatID implements Sendable's SetChatID method.
118 | func (x *SendVideo) SetChatID(id int64) { x.ChatId = ID(id) }
119 |
120 | // SetChatID implements Sendable's SetChatID method.
121 | func (x *SendVideoNote) SetChatID(id int64) { x.ChatId = ID(id) }
122 |
123 | // SetChatID implements Sendable's SetChatID method.
124 | func (x *SendVoice) SetChatID(id int64) { x.ChatId = ID(id) }
125 |
126 | // Send implements Sendable's Send method.
127 | func (x *SendAnimation) Send(api *API) (*Message, error) { return api.SendAnimation(x) }
128 |
129 | // Send implements Sendable's Send method.
130 | func (x *SendAudio) Send(api *API) (*Message, error) { return api.SendAudio(x) }
131 |
132 | // Send implements Sendable's Send method.
133 | func (x *SendContact) Send(api *API) (*Message, error) { return api.SendContact(x) }
134 |
135 | // Send implements Sendable's Send method.
136 | func (x *SendDice) Send(api *API) (*Message, error) { return api.SendDice(x) }
137 |
138 | // Send implements Sendable's Send method.
139 | func (x *SendDocument) Send(api *API) (*Message, error) { return api.SendDocument(x) }
140 |
141 | // Send implements Sendable's Send method.
142 | func (x *SendGame) Send(api *API) (*Message, error) { return api.SendGame(x) }
143 |
144 | // Send implements Sendable's Send method.
145 | func (x *SendInvoice) Send(api *API) (*Message, error) { return api.SendInvoice(x) }
146 |
147 | // Send implements Sendable's Send method.
148 | func (x *SendLocation) Send(api *API) (*Message, error) { return api.SendLocation(x) }
149 |
150 | // Send implements Sendable's Send method.
151 | func (x *SendMessage) Send(api *API) (*Message, error) { return api.SendMessage(x) }
152 |
153 | // Send implements Sendable's Send method.
154 | func (x *SendPhoto) Send(api *API) (*Message, error) { return api.SendPhoto(x) }
155 |
156 | // Send implements Sendable's Send method.
157 | func (x *SendPoll) Send(api *API) (*Message, error) { return api.SendPoll(x) }
158 |
159 | // Send implements Sendable's Send method.
160 | func (x *SendSticker) Send(api *API) (*Message, error) { return api.SendSticker(x) }
161 |
162 | // Send implements Sendable's Send method.
163 | func (x *SendVenue) Send(api *API) (*Message, error) { return api.SendVenue(x) }
164 |
165 | // Send implements Sendable's Send method.
166 | func (x *SendVideo) Send(api *API) (*Message, error) { return api.SendVideo(x) }
167 |
168 | // Send implements Sendable's Send method.
169 | func (x *SendVideoNote) Send(api *API) (*Message, error) { return api.SendVideoNote(x) }
170 |
171 | // Send implements Sendable's Send method.
172 | func (x *SendVoice) Send(api *API) (*Message, error) { return api.SendVoice(x) }
173 |
174 | // GetParseMode implements ParseModeSettable's GetParseMode method.
175 | func (x *SendAnimation) GetParseMode() ParseMode { return x.ParseMode }
176 |
177 | // SetParseMode implements ParseModeSettable's SetParseMode method.
178 | func (x *SendAnimation) SetParseMode(mode ParseMode) { x.ParseMode = mode }
179 |
180 | // GetParseMode implements ParseModeSettable's GetParseMode method.
181 | func (x *SendAudio) GetParseMode() ParseMode { return x.ParseMode }
182 |
183 | // SetParseMode implements ParseModeSettable's SetParseMode method.
184 | func (x *SendAudio) SetParseMode(mode ParseMode) { x.ParseMode = mode }
185 |
186 | // GetParseMode implements ParseModeSettable's GetParseMode method.
187 | func (x *SendDocument) GetParseMode() ParseMode { return x.ParseMode }
188 |
189 | // SetParseMode implements ParseModeSettable's SetParseMode method.
190 | func (x *SendDocument) SetParseMode(mode ParseMode) { x.ParseMode = mode }
191 |
192 | // GetParseMode implements ParseModeSettable's GetParseMode method.
193 | func (x *SendMessage) GetParseMode() ParseMode { return x.ParseMode }
194 |
195 | // SetParseMode implements ParseModeSettable's SetParseMode method.
196 | func (x *SendMessage) SetParseMode(mode ParseMode) { x.ParseMode = mode }
197 |
198 | // GetParseMode implements ParseModeSettable's GetParseMode method.
199 | func (x *SendPhoto) GetParseMode() ParseMode { return x.ParseMode }
200 |
201 | // SetParseMode implements ParseModeSettable's SetParseMode method.
202 | func (x *SendPhoto) SetParseMode(mode ParseMode) { x.ParseMode = mode }
203 |
204 | // GetParseMode implements ParseModeSettable's GetParseMode method.
205 | func (x *SendVideo) GetParseMode() ParseMode { return x.ParseMode }
206 |
207 | // SetParseMode implements ParseModeSettable's SetParseMode method.
208 | func (x *SendVideo) SetParseMode(mode ParseMode) { x.ParseMode = mode }
209 |
210 | // GetParseMode implements ParseModeSettable's GetParseMode method.
211 | func (x *SendVoice) GetParseMode() ParseMode { return x.ParseMode }
212 |
213 | // SetParseMode implements ParseModeSettable's SetParseMode method.
214 | func (x *SendVoice) SetParseMode(mode ParseMode) { x.ParseMode = mode }
215 |
216 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
217 | func (x *SendAnimation) SetReplyToMessageId(id int64) {
218 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
219 | }
220 |
221 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
222 | func (x *SendAudio) SetReplyToMessageId(id int64) {
223 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
224 | }
225 |
226 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
227 | func (x *SendContact) SetReplyToMessageId(id int64) {
228 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
229 | }
230 |
231 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
232 | func (x *SendDice) SetReplyToMessageId(id int64) {
233 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
234 | }
235 |
236 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
237 | func (x *SendDocument) SetReplyToMessageId(id int64) {
238 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
239 | }
240 |
241 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
242 | func (x *SendGame) SetReplyToMessageId(id int64) {
243 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
244 | }
245 |
246 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
247 | func (x *SendInvoice) SetReplyToMessageId(id int64) {
248 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
249 | }
250 |
251 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
252 | func (x *SendLocation) SetReplyToMessageId(id int64) {
253 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
254 | }
255 |
256 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
257 | func (x *SendMessage) SetReplyToMessageId(id int64) {
258 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
259 | }
260 |
261 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
262 | func (x *SendPhoto) SetReplyToMessageId(id int64) {
263 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
264 | }
265 |
266 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
267 | func (x *SendPoll) SetReplyToMessageId(id int64) {
268 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
269 | }
270 |
271 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
272 | func (x *SendSticker) SetReplyToMessageId(id int64) {
273 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
274 | }
275 |
276 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
277 | func (x *SendVenue) SetReplyToMessageId(id int64) {
278 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
279 | }
280 |
281 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
282 | func (x *SendVideo) SetReplyToMessageId(id int64) {
283 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
284 | }
285 |
286 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
287 | func (x *SendVideoNote) SetReplyToMessageId(id int64) {
288 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
289 | }
290 |
291 | // SetReplyToMessageId implements Replyable's SetReplyToMessageId method.
292 | func (x *SendVoice) SetReplyToMessageId(id int64) {
293 | x.ReplyParameters = &ReplyParameters{MessageId: id, ChatId: x.GetChatID()}
294 | }
295 |
--------------------------------------------------------------------------------
/api_extended_test.go:
--------------------------------------------------------------------------------
1 | package tgo
2 |
3 | import (
4 | "math/rand"
5 | "testing"
6 | )
7 |
8 | var (
9 | _ ChatID = Username("")
10 | _ ChatID = ID(0)
11 | )
12 |
13 | var (
14 | sendableAnimation Sendable = &SendAnimation{}
15 | sendableAudio Sendable = &SendAudio{}
16 | sendableContact Sendable = &SendContact{}
17 | sendableDice Sendable = &SendDice{}
18 | sendableDocument Sendable = &SendDocument{}
19 | sendableGame Sendable = &SendGame{}
20 | sendableInvoice Sendable = &SendInvoice{}
21 | sendableLocation Sendable = &SendLocation{}
22 | sendableMessage Sendable = &SendMessage{}
23 | sendablePhoto Sendable = &SendPhoto{}
24 | sendablePoll Sendable = &SendPoll{}
25 | sendableSticker Sendable = &SendSticker{}
26 | sendableVenue Sendable = &SendVenue{}
27 | sendableVideo Sendable = &SendVideo{}
28 | sendableVideoNote Sendable = &SendVideoNote{}
29 | sendableVoice Sendable = &SendVoice{}
30 | )
31 |
32 | var (
33 | parseModeSettableAnimation ParseModeSettable = &SendAnimation{}
34 | parseModeSettableAudio ParseModeSettable = &SendAudio{}
35 | parseModeSettableDocument ParseModeSettable = &SendDocument{}
36 | parseModeSettableMessage ParseModeSettable = &SendMessage{}
37 | parseModeSettablePhoto ParseModeSettable = &SendPhoto{}
38 | parseModeSettableVideo ParseModeSettable = &SendVideo{}
39 | parseModeSettableVoice ParseModeSettable = &SendVoice{}
40 | )
41 |
42 | // TODO: Write tests for these.
43 | var (
44 | _ Replyable = &SendAnimation{}
45 | _ Replyable = &SendAudio{}
46 | _ Replyable = &SendContact{}
47 | _ Replyable = &SendDice{}
48 | _ Replyable = &SendDocument{}
49 | _ Replyable = &SendGame{}
50 | _ Replyable = &SendInvoice{}
51 | _ Replyable = &SendLocation{}
52 | _ Replyable = &SendMessage{}
53 | _ Replyable = &SendPhoto{}
54 | _ Replyable = &SendPoll{}
55 | _ Replyable = &SendSticker{}
56 | _ Replyable = &SendVenue{}
57 | _ Replyable = &SendVideo{}
58 | _ Replyable = &SendVideoNote{}
59 | _ Replyable = &SendVoice{}
60 | )
61 |
62 | // TestSendables tests Sendables' GetChatID and SetChatID methods and ensures
63 | // that they sets and returns the right value.
64 | //
65 | // TODO: Test if they call the right API method on Send method.
66 | func TestSendables(t *testing.T) {
67 | var tests = map[string]struct {
68 | T Sendable
69 | ID int64
70 | }{
71 | "Sendable Animation": {T: sendableAnimation, ID: rand.Int63()},
72 | "Sendable Audio": {T: sendableAudio, ID: rand.Int63()},
73 | "Sendable Contact": {T: sendableContact, ID: rand.Int63()},
74 | "Sendable Dice": {T: sendableDice, ID: rand.Int63()},
75 | "Sendable Document": {T: sendableDocument, ID: rand.Int63()},
76 | "Sendable Game": {T: sendableGame, ID: rand.Int63()},
77 | "Sendable Invoice": {T: sendableInvoice, ID: rand.Int63()},
78 | "Sendable Location": {T: sendableLocation, ID: rand.Int63()},
79 | "Sendable Message": {T: sendableMessage, ID: rand.Int63()},
80 | "Sendable Photo": {T: sendablePhoto, ID: rand.Int63()},
81 | "Sendable Poll": {T: sendablePoll, ID: rand.Int63()},
82 | "Sendable Sticker": {T: sendableSticker, ID: rand.Int63()},
83 | "Sendable Venue": {T: sendableVenue, ID: rand.Int63()},
84 | "Sendable Video": {T: sendableVideo, ID: rand.Int63()},
85 | "Sendable VideoNote": {T: sendableVideoNote, ID: rand.Int63()},
86 | "Sendable Voice": {T: sendableVoice, ID: rand.Int63()},
87 | }
88 |
89 | for name, test := range tests {
90 | t.Run(name, func(t *testing.T) {
91 | test.T.SetChatID(test.ID)
92 |
93 | got := test.T.GetChatID()
94 | expected := ID(test.ID)
95 | if got != expected {
96 | t.Errorf("got: %v, expected: %v", got, expected)
97 | }
98 | })
99 | }
100 | }
101 |
102 | // TestParseModeSettables tests ParseModeSettables' GetParseMode and SetParseMode
103 | // methods with all of the available parse modes and ensures that they set and
104 | // return the right parse mode.
105 | func TestParseModeSettables(t *testing.T) {
106 | var tests = map[string]struct {
107 | T ParseModeSettable
108 | parseMode ParseMode
109 | }{
110 | "parseModeSettable Animation None": {T: parseModeSettableAnimation, parseMode: ParseModeNone},
111 | "parseModeSettable Animation Markdown": {T: parseModeSettableAnimation, parseMode: ParseModeMarkdown},
112 | "parseModeSettable Animation MarkdownV2": {T: parseModeSettableAnimation, parseMode: ParseModeMarkdownV2},
113 | "parseModeSettable Animation HTML": {T: parseModeSettableAnimation, parseMode: ParseModeHTML},
114 | "parseModeSettable Audio None": {T: parseModeSettableAudio, parseMode: ParseModeNone},
115 | "parseModeSettable Audio Markdown": {T: parseModeSettableAudio, parseMode: ParseModeMarkdown},
116 | "parseModeSettable Audio MarkdownV2": {T: parseModeSettableAudio, parseMode: ParseModeMarkdownV2},
117 | "parseModeSettable Audio HTML": {T: parseModeSettableAudio, parseMode: ParseModeHTML},
118 | "parseModeSettable Document None": {T: parseModeSettableDocument, parseMode: ParseModeNone},
119 | "parseModeSettable Document Markdown": {T: parseModeSettableDocument, parseMode: ParseModeMarkdown},
120 | "parseModeSettable Document MarkdownV2": {T: parseModeSettableDocument, parseMode: ParseModeMarkdownV2},
121 | "parseModeSettable Document HTML": {T: parseModeSettableDocument, parseMode: ParseModeHTML},
122 | "parseModeSettable Message None": {T: parseModeSettableMessage, parseMode: ParseModeNone},
123 | "parseModeSettable Message Markdown": {T: parseModeSettableMessage, parseMode: ParseModeMarkdown},
124 | "parseModeSettable Message MarkdownV2": {T: parseModeSettableMessage, parseMode: ParseModeMarkdownV2},
125 | "parseModeSettable Message HTML": {T: parseModeSettableMessage, parseMode: ParseModeHTML},
126 | "parseModeSettable Photo None": {T: parseModeSettablePhoto, parseMode: ParseModeNone},
127 | "parseModeSettable Photo Markdown": {T: parseModeSettablePhoto, parseMode: ParseModeMarkdown},
128 | "parseModeSettable Photo MarkdownV2": {T: parseModeSettablePhoto, parseMode: ParseModeMarkdownV2},
129 | "parseModeSettable Photo HTML": {T: parseModeSettablePhoto, parseMode: ParseModeHTML},
130 | "parseModeSettable Video None": {T: parseModeSettableVideo, parseMode: ParseModeNone},
131 | "parseModeSettable Video Markdown": {T: parseModeSettableVideo, parseMode: ParseModeMarkdown},
132 | "parseModeSettable Video MarkdownV2": {T: parseModeSettableVideo, parseMode: ParseModeMarkdownV2},
133 | "parseModeSettable Video HTML": {T: parseModeSettableVideo, parseMode: ParseModeHTML},
134 | "parseModeSettable Voice None": {T: parseModeSettableVoice, parseMode: ParseModeNone},
135 | "parseModeSettable Voice Markdown": {T: parseModeSettableVoice, parseMode: ParseModeMarkdown},
136 | "parseModeSettable Voice MarkdownV2": {T: parseModeSettableVoice, parseMode: ParseModeMarkdownV2},
137 | "parseModeSettable Voice HTML": {T: parseModeSettableVoice, parseMode: ParseModeHTML},
138 | }
139 |
140 | for name, test := range tests {
141 | t.Run(name, func(t *testing.T) {
142 | test.T.SetParseMode(test.parseMode)
143 |
144 | got := test.T.GetParseMode()
145 | expected := test.parseMode
146 | if got != expected {
147 | t.Errorf("got: %v, expected: %v", got, expected)
148 | }
149 | })
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/api_unmarshalers.go:
--------------------------------------------------------------------------------
1 | package tgo
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | )
8 |
9 | func unmarshalMaybeInaccessibleMessage(rawBytes json.RawMessage) (data MaybeInaccessibleMessage, err error) {
10 | if len(rawBytes) == 0 {
11 | return nil, nil
12 | }
13 |
14 | var temp struct {
15 | Date int64 `json:"date"`
16 | }
17 | if err = json.Unmarshal(rawBytes, &temp); err != nil {
18 | return nil, err
19 | }
20 |
21 | switch temp.Date {
22 | case 0:
23 | data = &InaccessibleMessage{}
24 | default:
25 | data = &Message{}
26 | }
27 |
28 | err = json.Unmarshal(rawBytes, data)
29 | return data, err
30 | }
31 |
32 | func unmarshalMessageOrigin(rawBytes json.RawMessage) (data MessageOrigin, err error) {
33 | if len(rawBytes) == 0 {
34 | return nil, nil
35 | }
36 |
37 | var temp struct {
38 | Type string `json:"type"`
39 | }
40 | if err = json.Unmarshal(rawBytes, &temp); err != nil {
41 | return nil, err
42 | }
43 |
44 | switch temp.Type {
45 | case "user":
46 | data = &MessageOriginUser{}
47 | case "hidden_user":
48 | data = &MessageOriginHiddenUser{}
49 | case "chat":
50 | data = &MessageOriginChat{}
51 | case "channel":
52 | data = &MessageOriginChannel{}
53 | default:
54 | return nil, errors.New("unknown type")
55 | }
56 |
57 | err = json.Unmarshal(rawBytes, data)
58 | return data, err
59 | }
60 |
61 | func unmarshalBackgroundFill(rawBytes json.RawMessage) (data BackgroundFill, err error) {
62 | if len(rawBytes) == 0 {
63 | return nil, nil
64 | }
65 |
66 | var temp struct {
67 | Type string `json:"type"`
68 | }
69 | if err = json.Unmarshal(rawBytes, &temp); err != nil {
70 | return nil, err
71 | }
72 |
73 | switch temp.Type {
74 | case "solid":
75 | data = &BackgroundFillSolid{}
76 | case "gradient":
77 | data = &BackgroundFillGradient{}
78 | case "freeform_gradient":
79 | data = &BackgroundFillFreeformGradient{}
80 | default:
81 | return nil, errors.New("unknown type")
82 | }
83 |
84 | err = json.Unmarshal(rawBytes, data)
85 | return data, err
86 | }
87 |
88 | func unmarshalBackgroundType(rawBytes json.RawMessage) (data BackgroundType, err error) {
89 | if len(rawBytes) == 0 {
90 | return nil, nil
91 | }
92 |
93 | var temp struct {
94 | Type string `json:"type"`
95 | }
96 | if err = json.Unmarshal(rawBytes, &temp); err != nil {
97 | return nil, err
98 | }
99 |
100 | switch temp.Type {
101 | case "fill":
102 | data = &BackgroundTypeFill{}
103 | case "wallpaper":
104 | data = &BackgroundTypeWallpaper{}
105 | case "pattern":
106 | data = &BackgroundTypePattern{}
107 | case "chat_theme":
108 | data = &BackgroundTypeChatTheme{}
109 | default:
110 | return nil, errors.New("unknown type")
111 | }
112 |
113 | err = json.Unmarshal(rawBytes, data)
114 | return data, err
115 | }
116 |
117 | func unmarshalChatMember(rawBytes json.RawMessage) (data ChatMember, err error) {
118 | if len(rawBytes) == 0 {
119 | return nil, nil
120 | }
121 |
122 | var temp struct {
123 | Status string `json:"status"`
124 | }
125 | if err = json.Unmarshal(rawBytes, &temp); err != nil {
126 | return nil, err
127 | }
128 |
129 | switch temp.Status {
130 | case "creator":
131 | data = &ChatMemberOwner{}
132 | case "administrator":
133 | data = &ChatMemberAdministrator{}
134 | case "member":
135 | data = &ChatMemberMember{}
136 | case "restricted":
137 | data = &ChatMemberRestricted{}
138 | case "left":
139 | data = &ChatMemberLeft{}
140 | case "kicked":
141 | data = &ChatMemberBanned{}
142 | default:
143 | return nil, errors.New("unknown type")
144 | }
145 |
146 | err = json.Unmarshal(rawBytes, data)
147 | return data, err
148 | }
149 |
150 | func unmarshalReactionType(rawBytes json.RawMessage) (data ReactionType, err error) {
151 | if len(rawBytes) == 0 {
152 | return nil, nil
153 | }
154 |
155 | var temp struct {
156 | Type string `json:"type"`
157 | }
158 | if err = json.Unmarshal(rawBytes, &temp); err != nil {
159 | return nil, err
160 | }
161 |
162 | switch temp.Type {
163 | case "emoji":
164 | data = &ReactionTypeEmoji{}
165 | case "custom_emoji":
166 | data = &ReactionTypeCustomEmoji{}
167 | case "paid":
168 | data = &ReactionTypePaid{}
169 | default:
170 | return nil, errors.New("unknown type")
171 | }
172 |
173 | err = json.Unmarshal(rawBytes, data)
174 | return data, err
175 | }
176 |
177 | func unmarshalMenuButton(rawBytes json.RawMessage) (data MenuButton, err error) {
178 | if len(rawBytes) == 0 {
179 | return nil, nil
180 | }
181 |
182 | var temp struct {
183 | Type string `json:"type"`
184 | }
185 | if err = json.Unmarshal(rawBytes, &temp); err != nil {
186 | return nil, err
187 | }
188 |
189 | switch temp.Type {
190 | case "commands":
191 | data = &MenuButtonCommands{}
192 | case "web_app":
193 | data = &MenuButtonWebApp{}
194 | case "default":
195 | data = &MenuButtonDefault{}
196 | default:
197 | return nil, errors.New("unknown type")
198 | }
199 |
200 | err = json.Unmarshal(rawBytes, data)
201 | return data, err
202 | }
203 |
204 | func unmarshalChatBoostSource(rawBytes json.RawMessage) (data ChatBoostSource, err error) {
205 | if len(rawBytes) == 0 {
206 | return nil, nil
207 | }
208 |
209 | var temp struct {
210 | Source string `json:"source"`
211 | }
212 | if err = json.Unmarshal(rawBytes, &temp); err != nil {
213 | return nil, err
214 | }
215 |
216 | switch temp.Source {
217 | case "premium":
218 | data = &ChatBoostSourcePremium{}
219 | case "gift_code":
220 | data = &ChatBoostSourceGiftCode{}
221 | case "giveaway":
222 | data = &ChatBoostSourceGiveaway{}
223 | default:
224 | return nil, errors.New("unknown type")
225 | }
226 |
227 | err = json.Unmarshal(rawBytes, data)
228 | return data, err
229 | }
230 |
231 | // Note: I have no idea if it's implemented in a good way or not.
232 | func unmarshalInputMessageContent(rawBytes json.RawMessage) (data InputMessageContent, err error) {
233 | if len(rawBytes) == 0 {
234 | return nil, nil
235 | }
236 |
237 | var temp struct {
238 | MessageText *string `json:"message_text,omitempty"`
239 | Latitude *float64 `json:"latitude,omitempty"`
240 | Address *string `json:"address,omitempty"`
241 | PhoneNumber *string `json:"phone_number,omitempty"`
242 | Description *string `json:"description,omitempty"`
243 | }
244 | if err = json.Unmarshal(rawBytes, &temp); err != nil {
245 | return nil, err
246 | }
247 |
248 | switch {
249 | case temp.MessageText != nil:
250 | data = &InputTextMessageContent{}
251 | case temp.Address != nil:
252 | data = &InputVenueMessageContent{}
253 | case temp.Latitude != nil:
254 | data = &InputLocationMessageContent{}
255 | case temp.PhoneNumber != nil:
256 | data = &InputContactMessageContent{}
257 | case temp.Description != nil:
258 | data = &InputInvoiceMessageContent{}
259 | default:
260 | return nil, errors.New("unknown type")
261 | }
262 |
263 | err = json.Unmarshal(rawBytes, data)
264 | return data, err
265 | }
266 |
267 | func unmarshalRevenueWithdrawalState(rawBytes json.RawMessage) (data RevenueWithdrawalState, err error) {
268 | if len(rawBytes) == 0 {
269 | return nil, nil
270 | }
271 |
272 | var temp struct {
273 | Type string `json:"type"`
274 | }
275 | if err = json.Unmarshal(rawBytes, &temp); err != nil {
276 | return nil, err
277 | }
278 |
279 | switch temp.Type {
280 | case "pending":
281 | data = &RevenueWithdrawalStatePending{}
282 | case "succeeded":
283 | data = &RevenueWithdrawalStateSucceeded{}
284 | case "failed":
285 | data = &RevenueWithdrawalStateFailed{}
286 | default:
287 | return nil, errors.New("unknown type")
288 | }
289 |
290 | err = json.Unmarshal(rawBytes, data)
291 | return data, err
292 | }
293 |
294 | func unmarshalTransactionPartner(rawBytes json.RawMessage) (data TransactionPartner, err error) {
295 | if len(rawBytes) == 0 {
296 | return nil, nil
297 | }
298 |
299 | var temp struct {
300 | Type string `json:"type"`
301 | }
302 | if err = json.Unmarshal(rawBytes, &temp); err != nil {
303 | return nil, err
304 | }
305 |
306 | switch temp.Type {
307 | case "fragment":
308 | data = &TransactionPartnerFragment{}
309 | case "user":
310 | data = &TransactionPartnerUser{}
311 | case "other":
312 | data = &TransactionPartnerOther{}
313 | default:
314 | return nil, errors.New("unknown type")
315 | }
316 |
317 | err = json.Unmarshal(rawBytes, data)
318 | return data, err
319 | }
320 |
321 | func unmarshalChatID(data json.RawMessage) (ChatID, error) {
322 | if bytes.HasPrefix(data, []byte("\"")) {
323 | var username Username
324 | err := json.Unmarshal(data, &username)
325 | return username, err
326 | } else {
327 | var id ID
328 | err := json.Unmarshal(data, &id)
329 | return id, err
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/ask.go:
--------------------------------------------------------------------------------
1 | package tgo
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "time"
7 | )
8 |
9 | // GetChatAndSenderID extracts and returns the chat ID and sender ID from a given message.
10 | func GetChatAndSenderID(msg *Message) (chatID, senderID int64) {
11 | chatID = msg.Chat.Id
12 |
13 | if msg.From != nil {
14 | senderID = msg.From.Id
15 | } else if msg.SenderChat != nil {
16 | senderID = msg.SenderChat.Id
17 | } else {
18 | senderID = msg.Chat.Id
19 | }
20 |
21 | return chatID, senderID
22 | }
23 |
24 | // getAskUID returns a unique identifier for the ask operation based on the chat ID and sender ID.
25 | func getAskUID(chatID, senderID int64) string {
26 | return fmt.Sprintf("ask:%d:%d", chatID, senderID)
27 | }
28 |
29 | // waitForAnswer waits for an answer from the given UID within the specified timeout duration.
30 | // It returns the received answer message or an error if the timeout is exceeded.
31 | func (bot *Bot) waitForAnswer(uid string, timeout time.Duration) (*Message, error) {
32 | waiter := make(chan *Message, 1)
33 |
34 | bot.askMut.Lock()
35 | bot.asks[uid] = waiter
36 | bot.askMut.Unlock()
37 |
38 | defer func() {
39 | bot.askMut.Lock()
40 | delete(bot.asks, uid)
41 | bot.askMut.Unlock()
42 |
43 | close(waiter)
44 | }()
45 |
46 | aCtx, cancel := context.WithTimeout(context.Background(), timeout)
47 | defer cancel()
48 |
49 | select {
50 | case answer := <-waiter:
51 | return answer, nil
52 |
53 | case <-aCtx.Done():
54 | return nil, aCtx.Err()
55 | }
56 | }
57 |
58 | // sendAnswerIfAsked sends the message into the asks channel if it was a response to an ask.
59 | // It returns true if the message was the response to an ask or false otherwise.
60 | func (bot *Bot) sendAnswerIfAsked(msg *Message) (sent bool) {
61 | bot.askMut.RLock()
62 | receiver, ok := bot.asks[getAskUID(GetChatAndSenderID(msg))]
63 | bot.askMut.RUnlock()
64 |
65 | if ok {
66 | receiver <- msg
67 | return true
68 | }
69 |
70 | return false
71 | }
72 |
73 | // Ask sends a question message to the specified chat and waits for an answer within the given timeout duration.
74 | // It returns the question message, the received answer message, and any error that occurred.
75 | func (bot *Bot) Ask(chatId, userId int64, msg Sendable, timeout time.Duration) (question, answer *Message, err error) {
76 | if msg.GetChatID() == nil {
77 | msg.SetChatID(chatId)
78 | }
79 | question, err = bot.Send(msg)
80 | if err != nil {
81 | return nil, nil, err
82 | }
83 |
84 | answer, err = bot.waitForAnswer(getAskUID(chatId, userId), timeout)
85 | return question, answer, err
86 | }
87 |
--------------------------------------------------------------------------------
/bot.go:
--------------------------------------------------------------------------------
1 | package tgo
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 | "sync"
7 | "syscall"
8 | "time"
9 | )
10 |
11 | // Sendable is an interface that represents any object that can be sent using an API client.
12 | type Sendable interface {
13 | // GetChatID returns the chat ID associated with the sendable object.
14 | GetChatID() ChatID
15 |
16 | // SetChatID sets the chat ID for the sendable object.
17 | SetChatID(id int64)
18 |
19 | // Send sends the sendable object using the provided API client.
20 | // It returns the sent message and any error encountered.
21 | Send(api *API) (*Message, error)
22 | }
23 |
24 | // ParseModeSettable is an interface that represents any object that can have its ParseMode set
25 | // Or in other words, messages with captions.
26 | type ParseModeSettable interface {
27 | GetParseMode() ParseMode
28 | SetParseMode(mode ParseMode)
29 | }
30 |
31 | // Replyable is an interface that represents any object that can be replied to.
32 | type Replyable interface {
33 | Sendable
34 | SetReplyToMessageId(id int64)
35 | }
36 |
37 | type Filter interface{ Check(update *Update) bool }
38 |
39 | type Router interface {
40 | Setup(bot *Bot) error
41 | HandleUpdate(bot *Bot, upd *Update) (used bool)
42 | }
43 |
44 | type Bot struct {
45 | *API // embedding the api to add all api methods to the bot
46 |
47 | DefaultParseMode ParseMode
48 |
49 | asks map[string]chan<- *Message
50 | askMut sync.RWMutex
51 |
52 | routers []Router
53 |
54 | // contains user-ids with their session
55 | sessions sync.Map
56 | }
57 |
58 | type Options struct {
59 | Host string
60 | Client *http.Client
61 | DefaultParseMode ParseMode
62 | }
63 |
64 | func NewBot(token string, opts Options) (bot *Bot) {
65 | api := NewAPI(token, opts.Host, opts.Client)
66 |
67 | return &Bot{
68 | API: api,
69 | DefaultParseMode: opts.DefaultParseMode,
70 | asks: make(map[string]chan<- *Message),
71 | }
72 | }
73 |
74 | // GetSession returns the stored session as a sync.Map.
75 | // it creates a new session if session id didn't exists.
76 | func (bot *Bot) GetSession(sessionID int64) *sync.Map {
77 | result, ok := bot.sessions.Load(sessionID)
78 | if ok {
79 | return result.(*sync.Map)
80 | }
81 |
82 | session := &sync.Map{}
83 | bot.sessions.Store(sessionID, session)
84 | return session
85 | }
86 |
87 | func (bot *Bot) AddRouter(router Router) error {
88 | if err := router.Setup(bot); err != nil {
89 | return err
90 | }
91 |
92 | bot.routers = append(bot.routers, router)
93 | return nil
94 | }
95 |
96 | // Send sends a message with the preferred ParseMode.
97 | func (b *Bot) Send(msg Sendable) (*Message, error) {
98 | if x, ok := msg.(ParseModeSettable); ok {
99 | if x.GetParseMode() == ParseModeNone {
100 | x.SetParseMode(b.DefaultParseMode)
101 | }
102 | }
103 |
104 | return msg.Send(b.API)
105 | }
106 |
107 | // StartPolling does an infinite GetUpdates with the timeout of the passed timeoutSeconds.
108 | // allowedUpdates by default passes nothing and uses the telegram's default.
109 | //
110 | // see tgo.GetUpdate for more detailed information.
111 | func (bot *Bot) StartPolling(timeoutSeconds int64, allowedUpdates ...string) error {
112 | var offset int64
113 |
114 | for {
115 | data, err := bot.GetUpdates(&GetUpdates{
116 | Offset: offset, // Is there any better way to do this? open an issue/pull-request if you know. thx.
117 | Timeout: timeoutSeconds,
118 | AllowedUpdates: allowedUpdates,
119 | })
120 | if err != nil {
121 | if errors.Is(err, syscall.ECONNRESET) {
122 | time.Sleep(time.Second / 2)
123 | continue
124 | }
125 | return err
126 | }
127 |
128 | for _, update := range data {
129 | offset = update.UpdateId + 1
130 |
131 | go func(update *Update) {
132 | if update.Message != nil && bot.sendAnswerIfAsked(update.Message) {
133 | return
134 | }
135 |
136 | for _, router := range bot.routers {
137 | if used := router.HandleUpdate(bot, update); used {
138 | return
139 | }
140 | }
141 | }(update)
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/cmd/helper_interface.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "strings"
4 |
5 | type InterfaceTemplate struct {
6 | Name string
7 | Description string
8 | InterfaceOf []string
9 | }
10 |
11 | func getInterfaceTemplate(section Section, sections []Section) InterfaceTemplate {
12 | desc := strings.Join(section.Description, "\n// ")
13 | if desc != "" {
14 | desc = "// " + desc
15 | }
16 |
17 | return InterfaceTemplate{
18 | Name: section.Name,
19 | Description: desc,
20 | InterfaceOf: section.InterfaceOf,
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/cmd/helper_method.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | type MethodTemplate struct {
9 | Name string
10 | MethodName string
11 | ReturnType string
12 | ReturnsInterface bool
13 | Description string
14 | EmptyReturnValue string
15 | Fields []MethodField
16 | InputFileFields []MethodField
17 | NestedInputFileFields []MethodField
18 | }
19 |
20 | type MethodField struct {
21 | Name string
22 | FieldName string
23 | Type string
24 | Tag string
25 | Description string
26 | DefaultValue string
27 | StringifyCode string
28 | IsOptional bool
29 | }
30 |
31 | func NewMethodFields(fields []Field, sections []Section) (newFields []MethodField, inputFileFields []MethodField) {
32 | for _, f := range fields {
33 | var optionalField string
34 | if f.IsOptional {
35 | optionalField = ",omitempty"
36 | }
37 |
38 | tf := MethodField{
39 | Name: snakeToPascal(f.Name),
40 | FieldName: f.Name,
41 | Type: getType(f.Name, f.Type, f.IsOptional, sections),
42 | Tag: fmt.Sprintf("`json:\"%s%s\"`", f.Name, optionalField),
43 | Description: f.Description,
44 | IsOptional: f.IsOptional,
45 | }
46 | tf.DefaultValue = defaultValueOfType(tf.Type)
47 | tf.StringifyCode = stringifyTypeAndSetInMap(tf.Name, tf.FieldName, tf.Type)
48 |
49 | if tf.Type == "*InputFile" {
50 | inputFileFields = append(inputFileFields, tf)
51 | }
52 |
53 | newFields = append(newFields, tf)
54 | }
55 |
56 | return
57 | }
58 |
59 | func getMethodTemplate(section Section, sections []Section) MethodTemplate {
60 | fields, inputFileFields := NewMethodFields(section.Fields, sections)
61 | nestedInputFileFields, _ := NewMethodFields(getNestedMediaFields(sections, section), sections)
62 | returnType := getType("", extractReturnType(section.Description), true, sections)
63 |
64 | desc := strings.Join(section.Description, "\n// ")
65 | if desc != "" {
66 | desc = "// " + desc
67 | }
68 |
69 | mt := MethodTemplate{
70 | Name: strings.ToUpper(string(section.Name[0])) + section.Name[1:],
71 | MethodName: section.Name,
72 | ReturnType: returnType,
73 | ReturnsInterface: isInterface(returnType, sections),
74 | Description: desc,
75 | EmptyReturnValue: defaultValueOfType(returnType),
76 | Fields: fields,
77 | InputFileFields: inputFileFields,
78 | NestedInputFileFields: nestedInputFileFields, // TODO
79 | }
80 |
81 | return mt
82 | }
83 |
--------------------------------------------------------------------------------
/cmd/helper_type.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | type TypeTemplate struct {
9 | Name string
10 | Description string
11 | Implements string
12 | Fields []TypeField
13 | ContainsInterface bool
14 | ContainsInputFile bool
15 | }
16 |
17 | type TypeField struct {
18 | Name string
19 | FieldName string
20 | Type string
21 | Tag string
22 | Description string
23 | IsInterface bool
24 | IsInputFile bool
25 | }
26 |
27 | func NewTypeFields(fields []Field, sections []Section) (newFields []TypeField, containsInterface, containsInputFile bool) {
28 | for _, f := range fields {
29 | var optionalField string
30 | if f.IsOptional {
31 | optionalField = ",omitempty"
32 | }
33 |
34 | tf := TypeField{
35 | Name: snakeToPascal(f.Name),
36 | FieldName: f.Name,
37 | Type: getType(f.Name, f.Type, f.IsOptional, sections),
38 | Tag: fmt.Sprintf("`json:\"%s%s\"`", f.Name, optionalField),
39 | Description: f.Description,
40 | }
41 | tf.IsInputFile = tf.Type == "*InputFile"
42 |
43 | if !tf.IsInputFile {
44 | for _, s := range sections {
45 | if s.Name == tf.Type && s.IsInterface {
46 | tf.IsInterface = true
47 | break
48 | }
49 | }
50 | }
51 |
52 | newFields = append(newFields, tf)
53 | }
54 |
55 | for _, f := range newFields {
56 | if f.IsInterface {
57 | containsInterface = true
58 | break
59 | }
60 | }
61 |
62 | for _, f := range newFields {
63 | if f.IsInputFile {
64 | containsInputFile = true
65 | break
66 | }
67 | }
68 |
69 | return
70 | }
71 |
72 | func getTypeTemplate(section Section, sections []Section, implementers map[string]string) TypeTemplate {
73 | fields, containsInterface, containsInputFile := NewTypeFields(section.Fields, sections)
74 |
75 | desc := strings.Join(section.Description, "\n// ")
76 | if desc != "" {
77 | desc = "// " + desc
78 | }
79 |
80 | return TypeTemplate{
81 | Name: section.Name,
82 | Description: desc,
83 | Implements: implementers[section.Name],
84 | Fields: fields,
85 | ContainsInterface: containsInterface,
86 | ContainsInputFile: containsInputFile,
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/cmd/helpers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strings"
7 |
8 | "golang.org/x/text/cases"
9 | "golang.org/x/text/language"
10 | )
11 |
12 | var returnTypePatterns = []*regexp.Regexp{
13 | regexp.MustCompile(`On success, an (?Parray of [A-Za-z]+) of the sent messages is returned`),
14 | regexp.MustCompile(`On success, the stopped (?P[A-Za-z]+) is`),
15 | regexp.MustCompile(`On success, returns a (?P[A-Za-z]+) object`),
16 | regexp.MustCompile(`On success, (?P[A-Za-z]+) is returned`),
17 | regexp.MustCompile(`On success, a (?P[A-Za-z]+) object is returned`),
18 | regexp.MustCompile(`On success, an (?Parray of [A-Za-z]+)s that were sent is returned`),
19 | regexp.MustCompile(`On success, the sent (?P[A-Za-z]+) is returned`),
20 | regexp.MustCompile(`Returns the [a-z ]+ as ?a? (?P[A-Za-z]+) `),
21 | regexp.MustCompile(`Returns the uploaded (?P[A-Za-z]+)`),
22 | regexp.MustCompile(`Returns the (?P[A-Za-z]+)`),
23 | regexp.MustCompile(`an (?PArray of [A-Za-z]+) objects`),
24 | regexp.MustCompile(`a (?P[A-Za-z]+) object`),
25 | regexp.MustCompile(`(?P[A-Za-z]+) is returned`),
26 | regexp.MustCompile(`(?P[A-Za-z]+) is returned, otherwise (?P[a-zA-Z]+) is returned`),
27 | regexp.MustCompile(`(?P[A-Za-z]+) on success`),
28 | }
29 |
30 | func isMethod(s string) bool {
31 | return strings.ToLower(s)[0] == s[0]
32 | }
33 |
34 | func isArray(t string) bool {
35 | return strings.HasPrefix(t, "[]")
36 | }
37 |
38 | func getType(key, s string, isOptional bool, sections []Section) string {
39 | if exactType := strings.TrimPrefix(s, "Array of "); exactType != s {
40 | return "[]" + getType(key, exactType, true, sections)
41 | } else if exactType := strings.TrimPrefix(s, "array of "); exactType != s {
42 | return "[]" + getType(key, exactType, true, sections)
43 | }
44 |
45 | switch key {
46 | case "parse_mode":
47 | return "ParseMode"
48 | case "media":
49 | switch s {
50 | case "InputMedia", "InputMediaAudio, InputMediaDocument, InputMediaPhoto and InputMediaVideo":
51 | return "InputMedia"
52 | case "InputFile", "String", "InputFile or String":
53 | return "*InputFile"
54 | }
55 | }
56 |
57 | switch s {
58 | // Basic types
59 | case "Int", "Integer":
60 | return "int64"
61 | case "String":
62 | return "string"
63 | case "True", "Boolean":
64 | return "bool"
65 | case "Float", "Float number":
66 | return "float64"
67 |
68 | // Special types
69 | case "Integer or String":
70 | return "ChatID"
71 | case "InputFile", "InputFile or String":
72 | return "*InputFile"
73 | case "InputMediaAudio, InputMediaDocument, InputMediaPhoto and InputMediaVideo":
74 | return "InputMedia"
75 | case "InlineKeyboardMarkup or ReplyKeyboardMarkup or ReplyKeyboardRemove or ForceReply":
76 | return "ReplyMarkup"
77 |
78 | // Other types, it should be a struct.
79 | default:
80 | if isOptional {
81 | if isInterface(s, sections) {
82 | return s
83 | }
84 |
85 | return "*" + s
86 | }
87 | return s
88 | }
89 | }
90 |
91 | func isInterface(typeName string, sections []Section) bool {
92 | for _, section := range sections {
93 | if section.Name == typeName && section.IsInterface {
94 | return true
95 | }
96 | }
97 |
98 | return false
99 | }
100 |
101 | func snakeToPascal(s string) string {
102 | return strings.ReplaceAll(cases.Title(language.English).String(strings.ReplaceAll(s, "_", " ")), " ", "")
103 | }
104 |
105 | func extractReturnType(rawDesc []string) string {
106 | var parts []string
107 | for _, part := range strings.Split(strings.Join(rawDesc, ". "), ".") {
108 | tP := strings.ToLower(part)
109 | if strings.Contains(tP, "returns") || strings.Contains(tP, "returned") {
110 | parts = append(parts, strings.TrimSpace(part))
111 | }
112 | }
113 |
114 | if parts == nil {
115 | return ""
116 | }
117 | desc := strings.Join(parts, ". ")
118 |
119 | for _, pattern := range returnTypePatterns {
120 | matches := pattern.FindStringSubmatch(desc)
121 | if len(matches) == 2 {
122 | return matches[1]
123 | } else if len(matches) > 2 {
124 | return "json.RawMessage"
125 | }
126 | }
127 |
128 | return ""
129 | }
130 |
131 | func defaultValueOfType(t string) string {
132 | if strings.HasPrefix(t, "*") || strings.HasPrefix(t, "[]") {
133 | return "nil"
134 | }
135 |
136 | switch t {
137 | case "int64", "float64":
138 | return "0"
139 | case "string":
140 | return `""`
141 | case "bool":
142 | return "false"
143 | case "ReplyMarkup", "InputFile", "ChatID":
144 | return "nil"
145 | case "ParseMode":
146 | return `ParseModeNone`
147 | default:
148 | return "UNKNOWN_FIX_ME_NOW"
149 | }
150 | }
151 |
152 | func getNestedMediaFields(all []Section, s Section) (fields []Field) {
153 | for _, f := range s.Fields {
154 | ft := getType(f.Name, f.Type, f.IsOptional, all)
155 | ft = strings.TrimPrefix(ft, "[]")
156 | ft = strings.TrimPrefix(ft, "*")
157 |
158 | if ft == "InputMedia" || ft == "InputPaidMedia" {
159 | fields = append(fields, f)
160 | continue
161 | }
162 |
163 | for _, xs := range all {
164 | if xs.Name == ft {
165 | for _, xf := range xs.Fields {
166 | if getType(xf.Name, xf.Type, xf.IsOptional, all) == "*InputFile" {
167 | fields = append(fields, f)
168 | break
169 | }
170 | }
171 | break
172 | }
173 | }
174 | }
175 |
176 | return fields
177 | }
178 |
179 | // function naming level: INFINITY
180 | func stringifyTypeAndSetInMap(name, fieldName, pType string) string {
181 | switch pType {
182 | case "*InputFile":
183 | return ""
184 | case "bool":
185 | return fmt.Sprintf(`payload["%s"] = strconv.FormatBool(x.%s)`, fieldName, name)
186 | case "int64":
187 | return fmt.Sprintf(`payload["%s"] = strconv.FormatInt(x.%s, 10)`, fieldName, name)
188 | case "float64":
189 | return fmt.Sprintf(`payload["%s"] = fmt.Sprintf("%%f", x.%s)`, fieldName, name)
190 | case "string":
191 | return fmt.Sprintf(`payload["%s"] = x.%s`, fieldName, name)
192 | case "ParseMode":
193 | return fmt.Sprintf(`payload["%s"] = string(x.%s)`, fieldName, name)
194 | default:
195 | return fmt.Sprintf(`if bb, err := json.Marshal(x.%s); err != nil {
196 | return nil, err
197 | } else {
198 | payload["%s"] = string(bb)
199 | }`, name, fieldName)
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "go/format"
6 | "log"
7 | "os"
8 | "text/template"
9 | "time"
10 | )
11 |
12 | const TelegramDocURL = "https://core.telegram.org/bots/api"
13 |
14 | type TemplateData struct {
15 | Sections []Section
16 | Implementers map[string]string
17 | }
18 |
19 | var Template = template.Must(template.New("tgo").
20 | Funcs(template.FuncMap{
21 | "getInterfaceTemplate": getInterfaceTemplate,
22 | "getTypeTemplate": getTypeTemplate,
23 | "getMethodTemplate": getMethodTemplate,
24 | "isMethod": isMethod,
25 | "isArray": isArray,
26 | }).
27 | ParseFiles("./cmd/template.gotmpl"),
28 | )
29 |
30 | func main() {
31 | doc, err := Fetch()
32 | if err != nil {
33 | log.Fatalln("Failed to fetch the documentation >", err)
34 | return
35 | }
36 |
37 | startTime := time.Now()
38 |
39 | buf := bytes.NewBuffer(nil)
40 | parsedDoc := Parse(doc)
41 |
42 | err = Template.ExecuteTemplate(buf, "template.gotmpl", parsedDoc)
43 | if err != nil {
44 | log.Fatalln("Failed to generate >", err)
45 | return
46 | }
47 |
48 | if mewB, err := format.Source(buf.Bytes()); err != nil {
49 | os.WriteFile("api.gen.go", buf.Bytes(), os.ModePerm)
50 | } else {
51 | os.WriteFile("api.gen.go", mewB, os.ModePerm)
52 | }
53 |
54 | log.Println("generated in", time.Since(startTime))
55 | }
56 |
--------------------------------------------------------------------------------
/cmd/parser.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strings"
7 |
8 | "github.com/PuerkitoBio/goquery"
9 | )
10 |
11 | type Section struct {
12 | Name string
13 | Description []string
14 | Fields []Field
15 | InterfaceOf []string
16 | IsInterface bool
17 | }
18 |
19 | type Field struct {
20 | Name string
21 | Type string
22 | Description string
23 | IsOptional bool
24 | }
25 |
26 | func Fetch() (*goquery.Document, error) {
27 | resp, err := http.Get(TelegramDocURL)
28 | if err != nil {
29 | return nil, fmt.Errorf("failed to fetch telegram doc > %s", err.Error())
30 | }
31 | defer resp.Body.Close()
32 |
33 | doc, err := goquery.NewDocumentFromReader(resp.Body)
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | return doc, nil
39 | }
40 |
41 | func Parse(doc *goquery.Document) (data TemplateData) {
42 | data.Implementers = make(map[string]string)
43 |
44 | // looping through each group
45 | doc.Find("#dev_page_content h4").Each(func(i int, s *goquery.Selection) {
46 | // it doesn't seems to be a group, so we'll ignore it
47 | if strings.Contains(s.Text(), " ") {
48 | return
49 | }
50 |
51 | var section Section
52 | section.Name = s.Text()
53 |
54 | // We implement this ourselves.
55 | if section.Name == "InputFile" {
56 | return
57 | }
58 |
59 | s.NextUntil("h3,h4,hr").Each(func(i int, s *goquery.Selection) {
60 | switch {
61 | case s.Is("p"), s.Is("blockquote"):
62 | section.Description = append(section.Description, strings.Split(s.Text(), "\n")...)
63 |
64 | case s.Is("ul"):
65 | supportedTypes := s.Find("li").Map(func(i int, s *goquery.Selection) string { return s.Text() })
66 | section.Description = append(section.Description, strings.Join(supportedTypes, ", "))
67 |
68 | for _, t := range supportedTypes {
69 | data.Implementers[t] = section.Name
70 | }
71 | if supportedTypes != nil {
72 | section.InterfaceOf = supportedTypes
73 | section.IsInterface = true
74 | }
75 | case s.Is("table"):
76 | s.Find("tbody tr").Each(func(i int, s *goquery.Selection) {
77 | field := Field{}
78 | fields := s.Find("td").Map(func(i int, s *goquery.Selection) string { return s.Text() })
79 |
80 | field.Name = fields[0]
81 | field.Type = fields[1]
82 |
83 | switch len(fields) {
84 | case 3:
85 | field.IsOptional = strings.HasPrefix(fields[2], "Optional.")
86 | field.Description = strings.ReplaceAll(fields[2], "\n", " ")
87 | case 4:
88 | field.IsOptional = fields[2] == "Optional"
89 | field.Description = strings.ReplaceAll(fields[3], "\n", " ")
90 | }
91 |
92 | section.Fields = append(section.Fields, field)
93 | })
94 | }
95 | })
96 |
97 | // Normalize the description
98 | if len(section.Description) != 0 {
99 | section.Description[0] = strings.Replace(section.Description[0], "This object", section.Name, 1)
100 | section.Description[0] = strings.Replace(section.Description[0], "Use this method to", section.Name+" is used to", 1)
101 |
102 | for i := len(section.Description) - 1; i >= 0; i-- {
103 | if section.Description[i] == "" {
104 | section.Description = section.Description[:i]
105 | } else {
106 | break
107 | }
108 | }
109 | }
110 |
111 | data.Sections = append(data.Sections, section)
112 | })
113 |
114 | // extended sections
115 | data.Sections = append(data.Sections, Section{
116 | Name: "ReplyMarkup",
117 | Description: []string{"ReplyMarkup is an interface for InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply"},
118 | IsInterface: true,
119 | })
120 |
121 | data.Sections = append(data.Sections, Section{
122 | Name: "ChatID",
123 | Description: []string{"ChatID is an interface for usernames and chatIDs"},
124 | IsInterface: true,
125 | })
126 |
127 | // extended implementers
128 | data.Implementers["InlineKeyboardMarkup"] = "ReplyMarkup"
129 | data.Implementers["ReplyKeyboardMarkup"] = "ReplyMarkup"
130 | data.Implementers["ReplyKeyboardRemove"] = "ReplyMarkup"
131 | data.Implementers["ForceReply"] = "ReplyMarkup"
132 |
133 | return data
134 | }
135 |
--------------------------------------------------------------------------------
/cmd/template.gotmpl:
--------------------------------------------------------------------------------
1 | {{ define "header" }}
2 | package tgo
3 |
4 | import (
5 | "strconv"
6 | "encoding/json"
7 | )
8 | {{ end }}
9 |
10 | {{ define "interface" }}
11 | {{ .Description }}
12 | type {{ .Name }} interface {
13 | // Is{{ .Name }} does nothing and is only used to enforce type-safety
14 | Is{{ .Name }}()
15 |
16 | {{ if or (eq .Name "InputMedia") (eq .Name "InputPaidMedia") }}getFiles() map[string]*InputFile{{ end }}
17 | }
18 |
19 | {{/* Due to lack of knowledge and its complexity, I decided to write this functuon by hand.
20 |
21 | {{ if .InterfaceOf }}
22 | func unmarshal{{.Name}}(rawBytes json.RawMessage) (data {{.Name}}, err error) {
23 | {{ range $index, $type := .InterfaceOf -}}
24 | data{{$type}} := &{{$type}}{}
25 | if err = json.Unmarshal(rawBytes, data{{$type}}); err == nil {
26 | return data{{$type}}, nil
27 | }{{if eq $index 0 -}}
28 | else if _, ok := err.(*json.UnmarshalTypeError); err != nil && !ok {
29 | return nil, err
30 | }
31 | {{ end }}
32 | {{ end }}
33 |
34 | return nil, errors.New("unknown type")
35 | }
36 | {{ end }}
37 | */}}
38 | {{ end }}
39 |
40 | {{ define "type" }}
41 | {{ .Description }}
42 | type {{ .Name }} struct { {{range $field := .Fields -}}
43 | {{ $field.Name }} {{ $field.Type }} {{ $field.Tag }} // {{ $field.Description }}
44 | {{end -}} }
45 |
46 | {{if .Implements }}
47 | func ({{ .Name }}) Is{{ .Implements }}() {}
48 | {{end}}
49 |
50 | {{ if .ContainsInterface }}
51 | func (x *{{ .Name }}) UnmarshalJSON(rawBytes []byte) (err error) {
52 | if len(rawBytes) == 0 {
53 | return nil
54 | }
55 |
56 | type temp struct {
57 | {{range $field := .Fields -}}
58 | {{ $field.Name }} {{ if $field.IsInterface }}json.RawMessage{{ else }}{{ $field.Type }}{{ end }} {{ $field.Tag }} // {{ $field.Description }}
59 | {{end -}}
60 | }
61 | raw := &temp{}
62 |
63 | if err = json.Unmarshal(rawBytes, raw); err != nil {
64 | return err
65 | }
66 |
67 | {{range $field := .Fields -}}{{ if $field.IsInterface }}
68 | if data, err := unmarshal{{$field.Type}}(raw.{{$field.Name}}); err != nil {
69 | return err
70 | } else {
71 | x.{{$field.Name}} = data
72 | }
73 | {{end }}{{end -}}
74 | {{range $field := .Fields -}}
75 | {{ if not $field.IsInterface }}x.{{ $field.Name }} = raw.{{ $field.Name }}{{ end }}
76 | {{end -}}
77 |
78 | return nil
79 | }
80 | {{ end }}
81 |
82 | {{ if .ContainsInputFile }}
83 | func (x *{{ .Name }}) getFiles() map[string]*InputFile {
84 | media := map[string]*InputFile{}
85 |
86 | {{ range $field := .Fields -}}
87 | {{ if $field.IsInputFile -}}
88 | if x.{{ $field.Name }} != nil {
89 | if x.{{ $field.Name }}.IsUploadable() {
90 | media[x.{{ $field.Name }}.Value] = x.{{ $field.Name }}
91 | }
92 | }
93 | {{ end -}}
94 | {{ end }}
95 |
96 | return media
97 | }
98 | {{ end }}
99 | {{ end }}
100 |
101 | {{define "method" }}
102 | {{ if .Fields -}}
103 | {{ .Description }}
104 | type {{ .Name }} struct {
105 | {{range $field := .Fields -}}
106 | {{ $field.Name }} {{ $field.Type }} {{ $field.Tag }} // {{ $field.Description }}
107 | {{end -}}
108 | }
109 | {{ end }}
110 |
111 | {{ if or .InputFileFields .NestedInputFileFields }}
112 | func (x *{{ .Name }}) getFiles() map[string]*InputFile {
113 | media := map[string]*InputFile{}
114 |
115 | {{ range $field := .InputFileFields -}}
116 | if x.{{ $field.Name }} != nil {
117 | if x.{{ $field.Name }}.IsUploadable() {
118 | media["{{ $field.FieldName }}"] = x.{{ $field.Name }}
119 | }
120 | }
121 | {{ end }}
122 |
123 | {{ range $field := .NestedInputFileFields -}}
124 | {{ if isArray $field.Type -}}
125 | for _, m := range x.{{ $field.Name }} {
126 | for key, value := range m.getFiles() {
127 | media[key] = value
128 | }
129 | }
130 | {{ else -}}
131 | {{ if $field.IsOptional -}}
132 | if x.{{ $field.Name }} != nil {
133 | {{ end -}}
134 |
135 | for key, value := range x.{{ $field.Name }}.getFiles() {
136 | media[key] = value
137 | }
138 |
139 | {{ if $field.IsOptional -}}
140 | }
141 | {{ end -}}
142 | {{ end -}}
143 | {{ end }}
144 |
145 | return media
146 | }
147 |
148 | func (x *{{ .Name }}) getParams() (map[string]string, error) {
149 | payload := map[string]string{}
150 |
151 | {{range $field := .Fields -}}
152 | {{ if $field.StringifyCode -}}
153 | {{ if not $field.IsOptional -}}
154 | {{ $field.StringifyCode }}
155 | {{ else -}}
156 | if x.{{ $field.Name }} {{ if not (eq $field.DefaultValue "false") -}}!= {{ $field.DefaultValue }}{{ end -}} {
157 | {{ $field.StringifyCode }}
158 | }
159 | {{ end -}}
160 | {{ end -}}
161 | {{ end }}
162 |
163 | return payload, nil
164 | }
165 | {{ end }}
166 |
167 | {{ .Description }}
168 | func (api *API) {{ .Name }}({{ if .Fields -}}payload *{{ .Name }}{{ end -}}) ({{ .ReturnType }}, error) {
169 | {{ if or .InputFileFields .NestedInputFileFields -}}
170 | if files := payload.getFiles(); len(files) != 0 {
171 | params, err := payload.getParams()
172 | if err != nil {
173 | return {{ .EmptyReturnValue }}, err
174 | }
175 | {{ if .ReturnsInterface -}}
176 | resp, err := callMultipart[json.RawMessage](api, "{{.MethodName}}", params, files)
177 | if err != nil {
178 | return nil, err
179 | }
180 | return unmarshal{{ .ReturnType }}(resp)
181 | {{ else -}}
182 | return callMultipart[{{ .ReturnType }}](api, "{{.MethodName}}", params, files)
183 | {{ end -}}
184 | }
185 | {{ end -}}
186 | {{ if .ReturnsInterface -}}
187 | resp, err := callJson[json.RawMessage](api, "{{.MethodName}}", {{ if .Fields -}}payload{{ else -}}nil{{ end -}})
188 | if err != nil {
189 | return nil, err
190 | }
191 | return unmarshal{{ .ReturnType }}(resp)
192 | {{ else -}}
193 | return callJson[{{ .ReturnType }}](api, "{{.MethodName}}", {{ if .Fields -}}payload{{ else -}}nil{{ end -}})
194 | {{ end -}}
195 | }
196 | {{ end }}
197 |
198 | {{ $impx := .Implementers }}
199 | {{ $sections := .Sections }}
200 |
201 | {{ template "header" }}
202 | {{ range $section := $sections }}
203 | {{ if or $section.IsInterface $section.InterfaceOf }}
204 | {{ template "interface" (getInterfaceTemplate $section $sections) }}
205 | {{ else if isMethod .Name }}
206 | {{template "method" (getMethodTemplate $section $sections)}}
207 | {{ else }}
208 | {{ template "type" (getTypeTemplate $section $sections $impx) }}
209 | {{ end }}
210 | {{ end }}
--------------------------------------------------------------------------------
/examples/basic/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/haashemi/tgo"
8 | "github.com/haashemi/tgo/filters"
9 | "github.com/haashemi/tgo/routers/message"
10 | )
11 |
12 | const BotToken = "bot_token"
13 |
14 | func main() {
15 | bot := tgo.NewBot(BotToken, tgo.Options{
16 | // it will set this parse mode for all api call via bot.Send, ctx.Send, and ctx.Reply
17 | DefaultParseMode: tgo.ParseModeHTML,
18 | })
19 |
20 | // initialize a new router to handle messages
21 | messageRouter := message.NewRouter()
22 | // It will call the handler when a new message gets received with text/caption of "hi"
23 | messageRouter.Handle(filters.And(filters.IsMessage(), filters.Text("hi")), Hi)
24 |
25 | // Handlers are called in order (at the least in DefaultRouter, other routers may work differently)
26 | // so, if no handlers gets used and the update is a new message, Echo will be called.
27 | messageRouter.Handle(filters.And(filters.IsMessage()), Echo)
28 |
29 | // add our message router to the bot routers; so it will be triggered on updates.
30 | bot.AddRouter(messageRouter)
31 |
32 | botInfo, err := bot.GetMe()
33 | if err != nil {
34 | log.Fatalln("Failed to fetch the bot info", err.Error())
35 | return
36 | }
37 | log.Println("Bot is started as", botInfo.Username)
38 |
39 | // start the long-polling with the timeout of 30 seconds
40 | // and only new messages are allowed as an update (to save traffic or whatever).
41 | if err := bot.StartPolling(30, "message"); err != nil {
42 | log.Fatalln("Polling stopped >>", err.Error())
43 | return
44 | }
45 | log.Println("Bot stopped successfully")
46 | }
47 |
48 | // Hi answers the hi message with a new hi!
49 | func Hi(ctx *message.Context) {
50 | // Get sender's first name with getting the raw message
51 | senderFirstName := ctx.Message.From.FirstName
52 |
53 | // create the text using HTML Markups
54 | text := fmt.Sprintf("Hi %s!", senderFirstName)
55 |
56 | // HTML Parse mode will be automatically set
57 | ctx.Reply(&tgo.SendMessage{
58 | Text: text,
59 | })
60 | }
61 |
62 | // Echo just echoes with text
63 | func Echo(ctx *message.Context) {
64 | // get text or caption of the sent message and send it back!
65 | ctx.Send(&tgo.SendMessage{Text: ctx.String()})
66 | }
67 |
--------------------------------------------------------------------------------
/examples/force_join/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 |
7 | "github.com/haashemi/tgo"
8 | "github.com/haashemi/tgo/filters"
9 | "github.com/haashemi/tgo/routers/message"
10 | )
11 |
12 | // ID of the channel that we want force join bot users to ir
13 | const channelID int64 = -1002145897099
14 |
15 | // bot's token must be passed by ./bot -token "my-awesome-token-here"
16 | var botToken = flag.String("token", "", "Your Telegram bot's token")
17 |
18 | // parsing the flags, nothing special
19 | func init() { flag.Parse() }
20 |
21 | func main() {
22 | bot := tgo.NewBot(*botToken, tgo.Options{})
23 |
24 | info, err := bot.GetMe()
25 | if err != nil {
26 | log.Fatalln("Failed to fetch the bot info", err)
27 | }
28 |
29 | mr := message.NewRouter(forceJoinMiddleware)
30 | mr.Handle(filters.Command("start", info.Username), startHandler)
31 | bot.AddRouter(mr)
32 |
33 | log.Printf("Polling started as @%s\n", info.Username)
34 | log.Fatalln(bot.StartPolling(30))
35 | }
36 |
37 | // Disclaimer: it's not recommended to do GetChatMember on every single message you'll
38 | // receive, as you may hit the Telegram's rate limit. which is not good...
39 | //
40 | // we recommend you to implement a basic caching or something.
41 | func forceJoinMiddleware(ctx *message.Context) (ok bool) {
42 | status, err := ctx.Bot.GetChatMember(&tgo.GetChatMember{
43 | ChatId: tgo.ID(channelID),
44 | UserId: ctx.From.Id,
45 | })
46 | if err != nil {
47 | ctx.Send(&tgo.SendMessage{Text: "Failed to fetch your join status. try again later."})
48 | return
49 | }
50 |
51 | switch status.(type) {
52 | case *tgo.ChatMemberOwner, *tgo.ChatMemberAdministrator, *tgo.ChatMemberMember:
53 | // they're joined! so everything's fine!
54 | return true
55 | }
56 |
57 | // it has to be one of these: *tgo.ChatMemberLeft, *tgo.ChatMemberRestricted, or *tgo.ChatMemberBanned.
58 | ctx.Send(&tgo.SendMessage{Text: "First you need to join to our channel somehow. try again after you joined"})
59 | return false
60 | }
61 |
62 | func startHandler(ctx *message.Context) {
63 | ctx.Send(&tgo.SendMessage{Text: "Ok... Now you know how to implement a force-join thingy using tgo!"})
64 | }
65 |
--------------------------------------------------------------------------------
/examples/simplest/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "time"
7 |
8 | "github.com/haashemi/tgo"
9 | "github.com/haashemi/tgo/filters"
10 | "github.com/haashemi/tgo/routers/message"
11 | )
12 |
13 | const BotToken = "bot_token"
14 |
15 | func main() {
16 | bot := tgo.NewBot(BotToken, tgo.Options{
17 | // it will set this parse mode for all api call via bot.Send, ctx.Send, and ctx.Reply
18 | DefaultParseMode: tgo.ParseModeHTML,
19 | })
20 |
21 | info, err := bot.GetMe()
22 | if err != nil {
23 | log.Fatalln("Failed to fetch the bot info", err.Error())
24 | }
25 |
26 | // initialize a new router to handle messages
27 | mr := message.NewRouter()
28 |
29 | // register a handler for /start command, which also works for groups.
30 | mr.Handle(filters.Command("start", info.Username), Start)
31 |
32 | // add our message router to the bot routers; so it will be triggered on updates.
33 | bot.AddRouter(mr)
34 |
35 | // start polling in an infinite loop
36 | for {
37 | log.Println("Polling started as", info.Username)
38 |
39 | // start the long-polling with the timeout of 30 seconds
40 | // and only new messages are allowed as an update (to save traffic or whatever).
41 | if err := bot.StartPolling(30, "message"); err != nil {
42 | log.Fatalln("Polling failed >>", err.Error())
43 | log.Println("Sleeping for 5 seconds...")
44 | time.Sleep(time.Second * 5)
45 | }
46 | }
47 | }
48 |
49 | // Start says hi to the user!
50 | func Start(ctx *message.Context) {
51 | // Get sender's first name with getting the raw message
52 | senderFirstName := ctx.Message.From.FirstName
53 |
54 | // create the text using HTML Markups
55 | text := fmt.Sprintf("Hi %s!", senderFirstName)
56 |
57 | // HTML Parse mode will be automatically set
58 | ctx.Reply(&tgo.SendMessage{Text: text})
59 | }
60 |
--------------------------------------------------------------------------------
/file.go:
--------------------------------------------------------------------------------
1 | package tgo
2 |
3 | import "io"
4 |
5 | // InputFile represents a file that can be used as input.
6 | type InputFile struct {
7 | Value string // The value of the file (e.g., file ID, URL, or path)
8 | Reader io.Reader // The reader for the file content
9 | }
10 |
11 | // IsInputFile is a marker method to indicate that the struct is an InputFile.
12 | func (InputFile) IsInputFile() {}
13 |
14 | // IsUploadable checks if the InputFile is uploadable.
15 | // An InputFile is considered uploadable if it has a non-nil Reader.
16 | func (ifu *InputFile) IsUploadable() bool {
17 | return ifu.Reader != nil
18 | }
19 |
20 | // MarshalJSON converts the InputFile to JSON.
21 | // If the InputFile has a non-nil Reader, it returns a JSON string with an attachment reference.
22 | // Otherwise, it returns a JSON string with the file value.
23 | func (ifu *InputFile) MarshalJSON() ([]byte, error) {
24 | if ifu.Reader != nil {
25 | return []byte(`"attach://` + ifu.Value + `"`), nil
26 | }
27 | return []byte(`"` + ifu.Value + `"`), nil
28 | }
29 |
30 | // FileFromID creates an InputFile from a file ID.
31 | func FileFromID(fileID string) *InputFile {
32 | return &InputFile{Value: fileID}
33 | }
34 |
35 | // FileFromURL creates an InputFile from a URL.
36 | func FileFromURL(url string) *InputFile {
37 | return &InputFile{Value: url}
38 | }
39 |
40 | // FileFromReader creates an InputFile from a name and a reader.
41 | func FileFromReader(name string, reader io.Reader) *InputFile {
42 | return &InputFile{Value: name, Reader: reader}
43 | }
44 |
45 | // FileFromPath creates an InputFile from a file path.
46 | //
47 | // Note that you should not use this if you plan to use the public telegram bot API.
48 | // This is only available for locally hosted bot API servers.
49 | func FileFromPath(path string) *InputFile {
50 | return &InputFile{Value: "file://" + path}
51 | }
52 |
--------------------------------------------------------------------------------
/filters/README.md:
--------------------------------------------------------------------------------
1 | # TGO Filters
2 |
3 | TGO Filters are set of functions that can be used with routers to determine which handler should be called and which should not.
4 |
5 | ## Example:
6 |
7 | ```go
8 | package main
9 |
10 | import (
11 | ...
12 | "github.com/haashemi/tgo/filters"
13 | )
14 |
15 | func main() {
16 | // ...
17 |
18 | mr := messages.NewRouter()
19 |
20 | // startCommandHandler will only get called if the Command filter approves it.
21 | mr.Handle(filters.Command("start", botUsername), startCommandHandler)
22 |
23 | bot.AddRouter(mr)
24 |
25 | // ...
26 | }
27 | ```
28 |
29 | ## Built-in filters:
30 |
31 | ### Logical filters:
32 |
33 | - `filters.True()`
34 | - `filters.False()`
35 | - `filters.And(...)`
36 | - `filters.Or(...)`
37 | - `filters.Not(...)`
38 |
39 | ### Message filters:
40 |
41 | - `filters.IsPrivate()`
42 | - `filters.Command(...)`
43 | - `filters.Commands(...)`
44 |
45 | ### General filters:
46 |
47 | General filters are currently working with `Message`, `CallbackQuery`, and `InlineQuery`.
48 |
49 | - `filters.Text(...)`
50 | - `filters.Texts(...)`
51 | - `filters.WithPrefix(...)`
52 | - `filters.WithSuffix(...)`
53 | - `filters.Regex(...)`
54 | - `filters.Whitelist(...)`
55 |
56 | ### Update filters:
57 |
58 | - `filters.HasMessage()`
59 | - `filters.IsMessage()`
60 | - `filters.IsEditedMessage()`
61 | - `filters.IsChannelPost()`
62 | - `filters.IsEditedChannelPost()`
63 | - `filters.IsInlineQuery()`
64 | - `filters.IsChosenInlineResult()`
65 | - `filters.IsCallbackQuery()`
66 | - `filters.IsShippingQuery()`
67 | - `filters.IsPreCheckoutQuery()`
68 | - `filters.IsPoll()`
69 | - `filters.IsPollAnswer()`
70 | - `filters.IsMyChatMember()`
71 | - `filters.IsChatMember()`
72 | - `filters.IsChatJoinRequest()`
73 |
74 | ## How to implement your own filter?
75 |
76 | It's simple! just pass your filter function to `filters.NewFilter` and you're done!
77 |
78 | Here is an example:
79 |
80 | ```go
81 | // It can be with a variable, fastest way.
82 | var myInlineFilter = filters.NewFilter(func(update *tgo.Update) bool {
83 | // your filter's logic goes here
84 | })
85 |
86 | // Or it can be its own separated function.
87 | func MyFilter() *tgo.Filter {
88 | return filters.NewFilter(func(update *tgo.Update) bool {
89 | // your filter's logic goes here
90 | })
91 | }
92 | ```
93 |
--------------------------------------------------------------------------------
/filters/filter.go:
--------------------------------------------------------------------------------
1 | // Filters are set of functions that can be used with routers to determine which
2 | // handler should be called and which should not.
3 | package filters // import "github.com/haashemi/tgo/filters"
4 |
5 | import (
6 | "regexp"
7 | "strings"
8 |
9 | "github.com/haashemi/tgo"
10 | )
11 |
12 | // FilterFunc tests the update with its own filters.
13 | type FilterFunc func(update *tgo.Update) bool
14 |
15 | // Filter does nothing and just holds a FilterFunc.
16 | type Filter struct{ f FilterFunc }
17 |
18 | // Check calls the FilterFunc and returns the result.
19 | func (f Filter) Check(update *tgo.Update) bool { return f.f(update) }
20 |
21 | // NewFilter returns a Filter from FilterFunc f.
22 | func NewFilter(f FilterFunc) *Filter { return &Filter{f: f} }
23 |
24 | // Text checks the update's text is equal to the text.
25 | //
26 | // Currently works with Message's caption and text, CallbackQuery's data, and InlineQuery's query.
27 | func Text(text string) tgo.Filter {
28 | return NewFilter(func(update *tgo.Update) bool {
29 | return extractUpdateText(update) == text
30 | })
31 | }
32 |
33 | // Text checks the update's text is in the texts.
34 | //
35 | // Currently works with Message's caption and text, CallbackQuery's data, and InlineQuery's query.
36 | func Texts(texts ...string) tgo.Filter {
37 | return NewFilter(func(update *tgo.Update) bool {
38 | raw := extractUpdateText(update)
39 |
40 | for _, text := range texts {
41 | if raw == text {
42 | return true
43 | }
44 | }
45 |
46 | return false
47 | })
48 | }
49 |
50 | // WithPrefix tests whether the update's text begins with prefix.
51 | //
52 | // Currently works with Message's caption and text, CallbackQuery's data, and InlineQuery's query.
53 | func WithPrefix(prefix string) tgo.Filter {
54 | return NewFilter(func(update *tgo.Update) bool {
55 | return strings.HasPrefix(extractUpdateText(update), prefix)
56 | })
57 | }
58 |
59 | // WithSuffix tests whether the update's text ends with suffix.
60 | //
61 | // Currently works with Message's caption and text, CallbackQuery's data, and InlineQuery's query.
62 | func WithSuffix(suffix string) tgo.Filter {
63 | return NewFilter(func(update *tgo.Update) bool {
64 | return strings.HasSuffix(extractUpdateText(update), suffix)
65 | })
66 | }
67 |
68 | // Regex matches the update's text with the reg.
69 | //
70 | // Currently works with Message's caption and text, CallbackQuery's data, and InlineQuery's query.
71 | func Regex(reg *regexp.Regexp) tgo.Filter {
72 | return NewFilter(func(update *tgo.Update) bool {
73 | return reg.MatchString(extractUpdateText(update))
74 | })
75 | }
76 |
77 | // Whitelist checks if the update is from the whitelisted IDs.
78 | //
79 | // Currently works with Message and CallbackQuery, and InlineQuery.
80 | func Whitelist(IDs ...int64) tgo.Filter {
81 | return NewFilter(func(update *tgo.Update) bool {
82 | var senderID int64
83 |
84 | if update.Message != nil {
85 | senderID = update.Message.From.Id
86 | } else if update.CallbackQuery != nil {
87 | senderID = update.CallbackQuery.From.Id
88 | } else if update.InlineQuery != nil {
89 | senderID = update.InlineQuery.From.Id
90 | }
91 |
92 | for _, id := range IDs {
93 | if id == senderID {
94 | return true
95 | }
96 | }
97 |
98 | return false
99 | })
100 | }
101 |
--------------------------------------------------------------------------------
/filters/filter_utils.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | import "github.com/haashemi/tgo"
4 |
5 | func extractUpdate(update *tgo.Update) any {
6 | switch {
7 | case update.Message != nil:
8 | return update.Message
9 | case update.EditedMessage != nil:
10 | return update.EditedMessage
11 | case update.ChannelPost != nil:
12 | return update.ChannelPost
13 | case update.EditedChannelPost != nil:
14 | return update.EditedChannelPost
15 | case update.InlineQuery != nil:
16 | return update.InlineQuery
17 | case update.ChosenInlineResult != nil:
18 | return update.ChosenInlineResult
19 | case update.CallbackQuery != nil:
20 | return update.CallbackQuery
21 | case update.ShippingQuery != nil:
22 | return update.ShippingQuery
23 | case update.PreCheckoutQuery != nil:
24 | return update.PreCheckoutQuery
25 | case update.Poll != nil:
26 | return update.Poll
27 | case update.PollAnswer != nil:
28 | return update.PollAnswer
29 | case update.MyChatMember != nil:
30 | return update.MyChatMember
31 | case update.ChatMember != nil:
32 | return update.ChatMember
33 | case update.ChatJoinRequest != nil:
34 | return update.ChatJoinRequest
35 | }
36 |
37 | return nil
38 | }
39 |
40 | func extractUpdateText(update *tgo.Update) string {
41 | switch data := extractUpdate(update).(type) {
42 | case *tgo.Message:
43 | if data.Caption != "" {
44 | return data.Caption
45 | }
46 | return data.Text
47 | case *tgo.CallbackQuery:
48 | return data.Data
49 | case *tgo.InlineQuery:
50 | return data.Query
51 | }
52 |
53 | return ""
54 | }
55 |
--------------------------------------------------------------------------------
/filters/logical.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | import "github.com/haashemi/tgo"
4 |
5 | // True does nothing and just always returns true.
6 | func True() tgo.Filter {
7 | return NewFilter(func(update *tgo.Update) bool { return true })
8 | }
9 |
10 | // False does nothing and just always returns false.
11 | func False() tgo.Filter {
12 | return NewFilter(func(update *tgo.Update) bool { return false })
13 | }
14 |
15 | // And Behaves like the && operator; returns true if all of the passes filters passes, otherwise returns false.
16 | func And(filters ...tgo.Filter) tgo.Filter {
17 | return NewFilter(func(update *tgo.Update) bool {
18 | for _, filter := range filters {
19 | if !filter.Check(update) {
20 | return false
21 | }
22 | }
23 |
24 | return true
25 | })
26 | }
27 |
28 | // Or behaves like the || operator; returns true if at least one of the passed filters passes.
29 | // returns false if none of them passes.
30 | func Or(filters ...tgo.Filter) tgo.Filter {
31 | return NewFilter(func(update *tgo.Update) bool {
32 | for _, filter := range filters {
33 | if filter.Check(update) {
34 | return true
35 | }
36 | }
37 |
38 | return false
39 | })
40 | }
41 |
42 | // Not Behaves like the ! operator; returns the opposite of the filter result
43 | func Not(filter tgo.Filter) tgo.Filter {
44 | return NewFilter(func(update *tgo.Update) bool { return !filter.Check(update) })
45 | }
46 |
--------------------------------------------------------------------------------
/filters/message.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/haashemi/tgo"
7 | )
8 |
9 | // IsPrivate checks if the message (and only message) is inside the private chat.
10 | func IsPrivate() tgo.Filter {
11 | return NewFilter(func(update *tgo.Update) bool {
12 | if msg := update.Message; msg != nil {
13 | // if message sender's id is equal to the chat-id,
14 | // then it's a private message.
15 | return msg.From.Id == msg.Chat.Id
16 | }
17 |
18 | return false
19 | })
20 | }
21 |
22 | // Commands tests if the message's (and only message) text or caption
23 | // matches the cmd.
24 | func Command(cmd, botUsername string) tgo.Filter {
25 | return Commands(botUsername, cmd)
26 | }
27 |
28 | // Commands tests if the message's (and only message) text or caption
29 | // matches any of the cmds.
30 | func Commands(botUsername string, cmds ...string) tgo.Filter {
31 | // make sure they are all lower-cased
32 | for index, command := range cmds {
33 | cmds[index] = strings.ToLower("/" + command)
34 | }
35 |
36 | // add a '@' prefix if not set already
37 | if !strings.HasPrefix(botUsername, "@") {
38 | botUsername = "@" + botUsername
39 | }
40 |
41 | return NewFilter(func(update *tgo.Update) bool {
42 | if msg := update.Message; msg != nil {
43 | text := msg.Text
44 | if text == "" {
45 | text = msg.Caption
46 | }
47 |
48 | // As the commands are already lowercased,
49 | // the text itself should get lowercased too.
50 | text = strings.ToLower(text)
51 |
52 | for _, cmd := range cmds {
53 | // valid cases are:
54 | // /command
55 | // /command@username
56 | // /command args...
57 | // /command@username args...
58 | if text == cmd || text == cmd+botUsername || strings.HasPrefix(text, cmd+" ") || strings.HasPrefix(text, cmd+botUsername+" ") {
59 | return true
60 | }
61 | }
62 | }
63 |
64 | return false
65 | })
66 | }
67 |
--------------------------------------------------------------------------------
/filters/updates.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | import "github.com/haashemi/tgo"
4 |
5 | func HasMessage() tgo.Filter {
6 | return NewFilter(func(update *tgo.Update) bool {
7 | return update.Message != nil ||
8 | update.EditedMessage != nil ||
9 | update.ChannelPost != nil ||
10 | update.EditedChannelPost != nil
11 | })
12 | }
13 |
14 | func IsMessage() tgo.Filter {
15 | return NewFilter(func(update *tgo.Update) bool { return update.Message != nil })
16 | }
17 |
18 | func IsEditedMessage() tgo.Filter {
19 | return NewFilter(func(update *tgo.Update) bool { return update.EditedMessage != nil })
20 | }
21 |
22 | func IsChannelPost() tgo.Filter {
23 | return NewFilter(func(update *tgo.Update) bool { return update.ChannelPost != nil })
24 | }
25 |
26 | func IsEditedChannelPost() tgo.Filter {
27 | return NewFilter(func(update *tgo.Update) bool { return update.EditedChannelPost != nil })
28 | }
29 |
30 | func IsInlineQuery() tgo.Filter {
31 | return NewFilter(func(update *tgo.Update) bool { return update.InlineQuery != nil })
32 | }
33 |
34 | func IsChosenInlineResult() tgo.Filter {
35 | return NewFilter(func(update *tgo.Update) bool { return update.ChosenInlineResult != nil })
36 | }
37 |
38 | func IsCallbackQuery() tgo.Filter {
39 | return NewFilter(func(update *tgo.Update) bool { return update.CallbackQuery != nil })
40 | }
41 |
42 | func IsShippingQuery() tgo.Filter {
43 | return NewFilter(func(update *tgo.Update) bool { return update.ShippingQuery != nil })
44 | }
45 |
46 | func IsPreCheckoutQuery() tgo.Filter {
47 | return NewFilter(func(update *tgo.Update) bool { return update.PreCheckoutQuery != nil })
48 | }
49 |
50 | func IsPoll() tgo.Filter {
51 | return NewFilter(func(update *tgo.Update) bool { return update.Poll != nil })
52 | }
53 |
54 | func IsPollAnswer() tgo.Filter {
55 | return NewFilter(func(update *tgo.Update) bool { return update.PollAnswer != nil })
56 | }
57 |
58 | func IsMyChatMember() tgo.Filter {
59 | return NewFilter(func(update *tgo.Update) bool { return update.MyChatMember != nil })
60 | }
61 |
62 | func IsChatMember() tgo.Filter {
63 | return NewFilter(func(update *tgo.Update) bool { return update.ChatMember != nil })
64 | }
65 |
66 | func IsChatJoinRequest() tgo.Filter {
67 | return NewFilter(func(update *tgo.Update) bool { return update.ChatJoinRequest != nil })
68 | }
69 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/haashemi/tgo
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/PuerkitoBio/goquery v1.9.2
7 | golang.org/x/text v0.19.0
8 | )
9 |
10 | require (
11 | github.com/andybalholm/cascadia v1.3.2 // indirect
12 | golang.org/x/net v0.24.0 // indirect
13 | )
14 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
2 | github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
3 | github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
4 | github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
5 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
6 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
7 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
8 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
9 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
10 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
11 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
12 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
13 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
14 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
15 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
16 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
17 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
18 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
19 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
20 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
21 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
22 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
23 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
24 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
25 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
26 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
27 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
28 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
29 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
30 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
31 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
32 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
33 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
34 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
35 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
36 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
37 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
38 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
39 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
40 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
41 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
42 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
43 |
--------------------------------------------------------------------------------
/routers/README.md:
--------------------------------------------------------------------------------
1 | # TGO Routers
2 |
3 | ## ToDo:
4 |
5 | write a nice documentation about what they are, how they works, and how to implement one ourselves!
6 |
7 |
14 |
--------------------------------------------------------------------------------
/routers/callback/context.go:
--------------------------------------------------------------------------------
1 | package callback
2 |
3 | import (
4 | "sync"
5 | "time"
6 |
7 | "github.com/haashemi/tgo"
8 | "github.com/haashemi/tgo/routers/message"
9 | )
10 |
11 | type Context struct {
12 | // CallbackQuery contains the raw received query
13 | *tgo.CallbackQuery
14 |
15 | // Bot is the bot instance which got the update.
16 | Bot *tgo.Bot
17 |
18 | // Storage contains an in-context storage used for middlewares to pass some data
19 | // to the next middleware or even the handler.
20 | Storage sync.Map
21 | }
22 |
23 | // Session returns the user's session storage.
24 | // it will return the chat's session if user-id is zero.
25 | func (ctx *Context) Session() *sync.Map {
26 | return ctx.Bot.GetSession(ctx.From.Id)
27 | }
28 |
29 | // Send sends a message into the message's chat if exist, otherwise sends in the sender's chat, with the preferred ParseMode.
30 | // It will set the target ChatId if not set.
31 | func (ctx *Context) Send(msg tgo.Sendable) (*tgo.Message, error) {
32 | if msg.GetChatID() == nil {
33 | if ctx.Message != nil {
34 | switch ctxMsg := ctx.Message.(type) {
35 | case *tgo.InaccessibleMessage:
36 | msg.SetChatID(ctxMsg.Chat.Id)
37 | case *tgo.Message:
38 | msg.SetChatID(ctxMsg.Chat.Id)
39 | }
40 | } else {
41 | msg.SetChatID(ctx.From.Id)
42 | }
43 | }
44 |
45 | return ctx.Bot.Send(msg)
46 | }
47 |
48 | // Ask asks a question from the callback query sender and waits for the passed timeout for their response.
49 | func (ctx *Context) Ask(msg tgo.Sendable, timeout time.Duration) (question, answer *message.Context, err error) {
50 | chatID := ctx.From.Id
51 | if ctx.Message != nil {
52 | switch ctxMsg := ctx.Message.(type) {
53 | case *tgo.InaccessibleMessage:
54 | chatID = ctxMsg.Chat.Id
55 | case *tgo.Message:
56 | chatID = ctxMsg.Chat.Id
57 | }
58 | }
59 |
60 | rawQuestion, rawAnswer, err := ctx.Bot.Ask(chatID, ctx.From.Id, msg, timeout)
61 |
62 | question = &message.Context{Message: rawQuestion, Bot: ctx.Bot}
63 | answer = &message.Context{Message: rawAnswer, Bot: ctx.Bot}
64 |
65 | return question, answer, err
66 | }
67 |
68 | // Answer answers to the sent callback query.
69 | // it fills the CallbackQueryId field by default.
70 | func (ctx *Context) Answer(options *tgo.AnswerCallbackQuery) error {
71 | options.CallbackQueryId = ctx.CallbackQuery.Id
72 |
73 | _, err := ctx.Bot.AnswerCallbackQuery(options)
74 | return err
75 | }
76 |
--------------------------------------------------------------------------------
/routers/callback/router.go:
--------------------------------------------------------------------------------
1 | package callback
2 |
3 | import "github.com/haashemi/tgo"
4 |
5 | type Handler func(ctx *Context)
6 |
7 | type Middleware func(ctx *Context) (ok bool)
8 |
9 | type Route struct {
10 | filter tgo.Filter
11 | middlewares []Middleware
12 | handler Handler
13 | }
14 |
15 | type Router struct {
16 | middlewares []Middleware
17 | routes []Route
18 | }
19 |
20 | // NewRouter returns a new message router
21 | func NewRouter(middlewares ...Middleware) *Router {
22 | return &Router{
23 | middlewares: middlewares,
24 | }
25 | }
26 |
27 | // Handle adds a new route to the Router
28 | func (r *Router) Handle(filter tgo.Filter, handler Handler, middlewares ...Middleware) {
29 | r.routes = append(r.routes, Route{filter: filter, middlewares: middlewares, handler: handler})
30 | }
31 |
32 | // Setup implements tgo.Router interface
33 | func (r *Router) Setup(bot *tgo.Bot) error { return nil }
34 |
35 | // HandleUpdate implements tgo.Router interface
36 | func (r *Router) HandleUpdate(bot *tgo.Bot, upd *tgo.Update) (used bool) {
37 | if upd.CallbackQuery == nil {
38 | return false
39 | }
40 |
41 | for _, route := range r.routes {
42 | if !route.filter.Check(upd) {
43 | continue
44 | }
45 |
46 | ctx := &Context{CallbackQuery: upd.CallbackQuery, Bot: bot}
47 |
48 | allMiddlewares := append(r.middlewares, route.middlewares...)
49 | for _, middleware := range allMiddlewares {
50 | if !middleware(ctx) {
51 | // we used the update, but as the middleware is failed
52 | // we'll stop the execution and return true as "update is used"
53 | return true
54 | }
55 | }
56 |
57 | route.handler(ctx)
58 |
59 | // filters passed and we used this method, so it's used!
60 | return true
61 | }
62 |
63 | return false
64 | }
65 |
--------------------------------------------------------------------------------
/routers/callback/router_test.go:
--------------------------------------------------------------------------------
1 | package callback
2 |
3 | import "github.com/haashemi/tgo"
4 |
5 | // make sure that *Router implements the tgo.Router correctly.
6 | var TestRouter tgo.Router = &Router{}
7 |
--------------------------------------------------------------------------------
/routers/message/context.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "sync"
5 | "time"
6 |
7 | "github.com/haashemi/tgo"
8 | )
9 |
10 | type Context struct {
11 | // Message contains the raw received message
12 | *tgo.Message
13 |
14 | // Bot is the bot instance which got the update.
15 | Bot *tgo.Bot
16 |
17 | // Storage contains an in-context storage used for middlewares to pass some data
18 | // to the next middleware or even the handler.
19 | Storage sync.Map
20 | }
21 |
22 | // Session returns the user's session storage.
23 | // it will return the chat's session if user-id is zero.
24 | //
25 | // ToDo: make sure that we are getting the chat id in the right way.
26 | func (ctx *Context) Session() *sync.Map {
27 | var id int64
28 |
29 | if ctx.From != nil {
30 | id = ctx.From.Id
31 | } else if ctx.SenderChat != nil {
32 | id = ctx.SenderChat.Id
33 | } else {
34 | id = ctx.Chat.Id
35 | }
36 |
37 | return ctx.Bot.GetSession(id)
38 | }
39 |
40 | // String returns the message's text or media caption
41 | func (m *Context) String() string {
42 | if m.Text != "" {
43 | return m.Text
44 | }
45 |
46 | return m.Caption
47 | }
48 |
49 | // Send sends a message into the current chat with the preferred ParseMode.
50 | // It will set the target ChatId if not set.
51 | func (ctx *Context) Send(msg tgo.Sendable) (*tgo.Message, error) {
52 | if msg.GetChatID() == nil {
53 | msg.SetChatID(ctx.Chat.Id)
54 | }
55 |
56 | return ctx.Bot.Send(msg)
57 | }
58 |
59 | // Reply replies to the current message with the preferred ParseMode.
60 | // It will pass/override the ChatId and ReplyToMessageId field.
61 | func (ctx *Context) Reply(msg tgo.Replyable) (*tgo.Message, error) {
62 | msg.SetChatID(ctx.Chat.Id)
63 | msg.SetReplyToMessageId(ctx.MessageId)
64 |
65 | return ctx.Bot.Send(msg)
66 | }
67 |
68 | // Ask asks a question from the message's sender and waits for the passed timeout for their response.
69 | func (ctx *Context) Ask(msg tgo.Sendable, timeout time.Duration) (question, answer *Context, err error) {
70 | cid, sid := tgo.GetChatAndSenderID(ctx.Message)
71 |
72 | rawQuestion, rawAnswer, err := ctx.Bot.Ask(cid, sid, msg, timeout)
73 |
74 | question = &Context{Message: rawQuestion, Bot: ctx.Bot}
75 | answer = &Context{Message: rawAnswer, Bot: ctx.Bot}
76 |
77 | return question, answer, err
78 | }
79 |
80 | // Delete deletes the received message.
81 | func (ctx *Context) Delete() error {
82 | _, err := ctx.Bot.DeleteMessage(&tgo.DeleteMessage{ChatId: tgo.ID(ctx.Chat.Id), MessageId: ctx.MessageId})
83 | return err
84 | }
85 |
--------------------------------------------------------------------------------
/routers/message/router.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import "github.com/haashemi/tgo"
4 |
5 | type Handler func(ctx *Context)
6 |
7 | type Middleware func(ctx *Context) (ok bool)
8 |
9 | type Route struct {
10 | filter tgo.Filter
11 | middlewares []Middleware
12 | handler Handler
13 | }
14 |
15 | type Router struct {
16 | middlewares []Middleware
17 | routes []Route
18 | }
19 |
20 | // NewRouter returns a new message router
21 | func NewRouter(middlewares ...Middleware) *Router {
22 | return &Router{
23 | middlewares: middlewares,
24 | }
25 | }
26 |
27 | // Handle adds a new route to the Router
28 | func (r *Router) Handle(filter tgo.Filter, handler Handler, middlewares ...Middleware) {
29 | r.routes = append(r.routes, Route{filter: filter, middlewares: middlewares, handler: handler})
30 | }
31 |
32 | // Setup implements tgo.Router interface
33 | func (r *Router) Setup(bot *tgo.Bot) error { return nil }
34 |
35 | // HandleUpdate implements tgo.Router interface
36 | func (r *Router) HandleUpdate(bot *tgo.Bot, upd *tgo.Update) (used bool) {
37 | if upd.Message == nil {
38 | return false
39 | }
40 |
41 | for _, route := range r.routes {
42 | if !route.filter.Check(upd) {
43 | continue
44 | }
45 |
46 | ctx := &Context{Message: upd.Message, Bot: bot}
47 |
48 | allMiddlewares := append(r.middlewares, route.middlewares...)
49 | for _, middleware := range allMiddlewares {
50 | if !middleware(ctx) {
51 | // we used the update, but as the middleware is failed
52 | // we'll stop the execution and return true as "update is used"
53 | return true
54 | }
55 | }
56 |
57 | route.handler(ctx)
58 |
59 | // filters passed and we used this method, so it's used!
60 | return true
61 | }
62 |
63 | return false
64 | }
65 |
--------------------------------------------------------------------------------
/routers/message/router_test.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import "github.com/haashemi/tgo"
4 |
5 | // make sure that *Router implements the tgo.Router correctly.
6 | var TestRouter tgo.Router = &Router{}
7 |
--------------------------------------------------------------------------------
/routers/router.go:
--------------------------------------------------------------------------------
1 | package routers
2 |
3 | import "github.com/haashemi/tgo"
4 |
5 | // Handler is a basic handler used in Route
6 | type Handler func(bot *tgo.Bot, upd *tgo.Update)
7 |
8 | // Middleware is a basic middleware used in Router and Route
9 | type Middleware func(bot *tgo.Bot, upd *tgo.Update) (ok bool)
10 |
11 | // Route is a basic route for the Router
12 | type Route struct {
13 | filter tgo.Filter
14 | middlewares []Middleware
15 | handler Handler
16 | }
17 |
18 | // Router is a basic router for all type of updates.
19 | type Router struct {
20 | middlewares []Middleware
21 | routes []Route
22 | }
23 |
24 | // NewRouter returns a new Router; nothing special
25 | func NewRouter() *Router {
26 | return &Router{}
27 | }
28 |
29 | // Use, adds the passed middlewares to the Router
30 | func (r *Router) Use(middlewares ...Middleware) {
31 | r.middlewares = append(r.middlewares, middlewares...)
32 | }
33 |
34 | // Handle, adds a new route to the Router
35 | func (r *Router) Handle(filter tgo.Filter, handler Handler, middlewares ...Middleware) {
36 | r.routes = append(r.routes, Route{filter: filter, middlewares: middlewares, handler: handler})
37 | }
38 |
39 | // Setup implements tgo.Router interface
40 | func (r *Router) Setup(bot *tgo.Bot) error { return nil }
41 |
42 | // HandleUpdate implements tgo.Router interface
43 | func (r *Router) HandleUpdate(bot *tgo.Bot, upd *tgo.Update) (used bool) {
44 | for _, route := range r.routes {
45 | if !route.filter.Check(upd) {
46 | continue
47 | }
48 |
49 | allMiddlewares := append(r.middlewares, route.middlewares...)
50 | for _, middleware := range allMiddlewares {
51 | if !middleware(bot, upd) {
52 | // we used the update, but as the middleware is failed
53 | // we'll stop the execution and return true as "update is used"
54 | return true
55 | }
56 | }
57 |
58 | route.handler(bot, upd)
59 |
60 | // filters passed and we used this method, so it's used!
61 | return true
62 | }
63 |
64 | return false
65 | }
66 |
--------------------------------------------------------------------------------
/routers/router_test.go:
--------------------------------------------------------------------------------
1 | package routers
2 |
3 | import "github.com/haashemi/tgo"
4 |
5 | // make sure that *Router implements the tgo.Router correctly.
6 | var TestRouter tgo.Router = &Router{}
7 |
--------------------------------------------------------------------------------
/tgo.go:
--------------------------------------------------------------------------------
1 | // TGO is a simple, flexible, and fully featured telegram-bot-api framework for Go developers.
2 | //
3 | // It gives you the ability to implement your own filters, middlewares, and even routers!
4 | //
5 | // All API methods and types are all code generated from the [telegram's documentation] in `./cmd` at the `api.gen.go` file.
6 | //
7 | // Example:
8 | //
9 | // package main
10 | //
11 | // import (
12 | // "fmt"
13 | // "log"
14 | // "time"
15 | //
16 | // "github.com/haashemi/tgo"
17 | // "github.com/haashemi/tgo/filters"
18 | // "github.com/haashemi/tgo/routers/message"
19 | // )
20 | //
21 | // const BotToken = "bot_token"
22 | //
23 | // func main() {
24 | // bot := tgo.NewBot(BotToken, tgo.Options{ DefaultParseMode: tgo.ParseModeHTML })
25 | //
26 | // info, err := bot.GetMe()
27 | // if err != nil {
28 | // log.Fatalln("Failed to fetch the bot info", err.Error())
29 | // }
30 | //
31 | // mr := message.NewRouter()
32 | // mr.Handle(filters.Command("start", info.Username), Start)
33 | // bot.AddRouter(mr)
34 | //
35 | // for {
36 | // log.Println("Polling started as", info.Username)
37 | //
38 | // if err := bot.StartPolling(30, "message"); err != nil {
39 | // log.Fatalln("Polling failed >>", err.Error())
40 | // time.Sleep(time.Second * 5)
41 | // }
42 | // }
43 | // }
44 | //
45 | // func Start(ctx *message.Context) {
46 | // text := fmt.Sprintf("Hi %s!", ctx.Message.From.FirstName)
47 | //
48 | // ctx.Reply(&tgo.SendMessage{Text: text})
49 | // }
50 | //
51 | // [telegram's documentation]: https://core.telegram.org/bots/api
52 | package tgo
53 |
--------------------------------------------------------------------------------