├── .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 | [![Go Reference](https://pkg.go.dev/badge/github.com/haashemi/tgo.svg)](https://pkg.go.dev/github.com/haashemi/tgo) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/haashemi/tgo)](https://goreportcard.com/report/github.com/haashemi/tgo) 5 | [![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/haashemi/tgo/go.yml)](https://github.com/haashemi/tgo/actions) 6 | [![License](http://img.shields.io/badge/license-mit-blue.svg)](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 | --------------------------------------------------------------------------------