├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── all_test.go ├── api.go ├── api_17.go ├── api_integration_test.go ├── api_internal_test.go ├── api_pre_17.go ├── api_test.go ├── cmd └── tbot │ └── main.go ├── configs.go ├── configs_edit.go ├── configs_message.go ├── configs_message_test.go ├── configs_test.go ├── constants.go ├── errors.go ├── errors_internal_test.go ├── examples ├── api │ └── main.go ├── callback │ └── main.go ├── commands │ └── main.go ├── echo │ └── main.go ├── inline │ └── main.go └── proxy │ └── main.go ├── helpers.go ├── interfaces.go ├── scripts ├── checkfmt.sh ├── coverage.sh └── coverage_i.sh ├── telebot ├── bot.go ├── bot_internal_test.go ├── bot_test.go ├── handlers.go ├── handlers_test.go ├── middleware.go ├── middleware_test.go ├── recover_middleware.go ├── recover_middleware_test.go └── session_middleware.go ├── testdata ├── files │ ├── AgADAQADracxG87oCAwifY52FDYYdyoW2ykABBedWvJj470nY0wBAAEC.jpg │ ├── AgADAQADracxG87oCAwifY52FDYYdyoW2ykABEFzMZ03BS96ZUwBAAEC.jpg │ ├── AgADAQADracxG87oCAwifY52FDYYdyoW2ykABL6tT4iSzvK7ZEwBAAEC.jpg │ └── AgADAQADracxG87oCAwifY52FDYYdyoW2ykABPxYFPTyioocYkwBAAEC.jpg └── integration_user_profile_photos.json ├── testutils └── mocks.go ├── types.go ├── types_inline.go ├── utils.go └── utils_test.go /.coveralls.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bot-api/telegram/b7abf87c449e690eb3563f53987186c95702c49f/.coveralls.yml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | gover.coverprofile 26 | *.coverprofile 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.5 5 | - 1.6 6 | - tip 7 | 8 | install: 9 | - make tools 10 | - go get -t ./... 11 | 12 | script: make travis 13 | 14 | notifications: 15 | email: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 bot-api 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test test_v test_i lint vet fmt coverage checkfmt prepare errcheck race 2 | 3 | NO_COLOR=\033[0m 4 | OK_COLOR=\033[32;01m 5 | ERROR_COLOR=\033[31;01m 6 | WARN_COLOR=\033[33;01m 7 | PKGSDIRS=$(shell find -L . -type f -name "*.go" -not -path "./Godeps/*") 8 | 9 | all: prepare 10 | 11 | travis: checkfmt vet errcheck coverage race lint 12 | 13 | prepare: fmt vet checkfmt errcheck test race lint 14 | 15 | test_v: 16 | @echo "$(OK_COLOR)Test packages$(NO_COLOR)" 17 | @go test -timeout 1m -cover -v ./... 18 | 19 | test: 20 | @echo "$(OK_COLOR)Test packages$(NO_COLOR)" 21 | @go test -timeout 10s -cover ./... 22 | 23 | test_i: 24 | ifdef API_BOT_TOKEN 25 | @echo "$(OK_COLOR)Run integration tests$(NO_COLOR)" 26 | @./scripts/coverage_i.sh 27 | endif 28 | 29 | lint: 30 | @echo "$(OK_COLOR)Run lint$(NO_COLOR)" 31 | @test -z "$$(golint -min_confidence 0.3 ./... | grep -v Godeps/_workspace/src/ | tee /dev/stderr)" 32 | 33 | vet: 34 | @echo "$(OK_COLOR)Run vet$(NO_COLOR)" 35 | @go vet ./... 36 | 37 | errcheck: 38 | @echo "$(OK_COLOR)Run errchk$(NO_COLOR)" 39 | @errcheck 40 | 41 | race: 42 | @echo "$(OK_COLOR)Test for races$(NO_COLOR)" 43 | @go test -race . 44 | 45 | checkfmt: 46 | @echo "$(OK_COLOR)Check formats$(NO_COLOR)" 47 | @./scripts/checkfmt.sh . 48 | 49 | fmt: 50 | @echo "$(OK_COLOR)Formatting$(NO_COLOR)" 51 | @echo $(PKGSDIRS) | xargs -I '{p}' -n1 goimports -w {p} 52 | @echo $(PKGSDIRS) | xargs -I '{p}' -n1 gofmt -w -s {p} 53 | 54 | coverage: 55 | @echo "$(OK_COLOR)Make coverage report$(NO_COLOR)" 56 | @./scripts/coverage.sh 57 | -goveralls -coverprofile=gover.coverprofile -service=travis-ci 58 | 59 | tools: 60 | @echo "$(OK_COLOR)Install tools$(NO_COLOR)" 61 | go get golang.org/x/tools/cmd/goimports 62 | go get github.com/golang/lint/golint 63 | go get github.com/kisielk/errcheck 64 | go get github.com/mattn/goveralls 65 | go get github.com/modocache/gover 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Bot Api [![GoDoc](https://godoc.org/github.com/bot-api/telegram?status.svg)](http://godoc.org/github.com/bot-api/telegram) [![Build Status](https://travis-ci.org/bot-api/telegram.svg?branch=master)](https://travis-ci.org/bot-api/telegram) [![Coverage Status](https://coveralls.io/repos/github/bot-api/telegram/badge.svg?branch=master)](https://coveralls.io/github/bot-api/telegram?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/bot-api/telegram)](https://goreportcard.com/report/github.com/bot-api/telegram) 2 | 3 | Supported go version: 1.5, 1.6, tip 4 | 5 | 6 | Implementation of the telegram bot API, inspired by github.com/go-telegram-bot-api/telegram-bot-api. 7 | 8 | The main difference between telegram-bot-api and this version is supporting net/context. 9 | Also, this library handles errors more correctly at this time (telegram-bot-api v4). 10 | 11 | 12 | ## Package contains: 13 | 14 | 1. Client for telegram bot api. 15 | 2. Bot with: 16 | 1. Middleware support 17 | 1. Command middleware to handle commands. 18 | 2. Recover middleware to recover on panics. 19 | 2. Webhook support 20 | 21 | 22 | 23 | # Get started 24 | 25 | Get last telegram api: 26 | 27 | `go get github.com/bot-api/telegram` 28 | 29 | ## If you want to use telegram bot api directly: 30 | 31 | `go run ./examples/api/main.go -debug -token BOT_TOKEN` 32 | 33 | ```go 34 | package main 35 | 36 | import ( 37 | "log" 38 | "flag" 39 | 40 | "github.com/bot-api/telegram" 41 | "golang.org/x/net/context" 42 | ) 43 | 44 | 45 | func main() { 46 | token := flag.String("token", "", "telegram bot token") 47 | debug := flag.Bool("debug", false, "show debug information") 48 | flag.Parse() 49 | 50 | if *token == "" { 51 | log.Fatal("token flag required") 52 | } 53 | 54 | api := telegram.New(*token) 55 | api.Debug(*debug) 56 | 57 | ctx, cancel := context.WithCancel(context.Background()) 58 | defer cancel() 59 | 60 | if user, err := api.GetMe(ctx); err != nil { 61 | log.Panic(err) 62 | } else { 63 | log.Printf("bot info: %#v", user) 64 | } 65 | 66 | updatesCh := make(chan telegram.Update) 67 | 68 | go telegram.GetUpdates(ctx, api, telegram.UpdateCfg{ 69 | Timeout: 10, // Timeout in seconds for long polling. 70 | Offset: 0, // Start with the oldest update 71 | }, updatesCh) 72 | 73 | for update := range updatesCh { 74 | log.Printf("got update from %s", update.Message.From.Username) 75 | if update.Message == nil { 76 | continue 77 | } 78 | msg := telegram.CloneMessage(update.Message, nil) 79 | // echo with the same message 80 | if _, err := api.Send(ctx, msg); err != nil { 81 | log.Printf("send error: %v", err) 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | ## If you want to use bot 88 | 89 | `go run ./examples/echo/main.go -debug -token BOT_TOKEN` 90 | 91 | ```go 92 | package main 93 | // Simple echo bot, that responses with the same message 94 | 95 | import ( 96 | "flag" 97 | "log" 98 | 99 | "github.com/bot-api/telegram" 100 | "github.com/bot-api/telegram/telebot" 101 | "golang.org/x/net/context" 102 | ) 103 | 104 | func main() { 105 | token := flag.String("token", "", "telegram bot token") 106 | debug := flag.Bool("debug", false, "show debug information") 107 | flag.Parse() 108 | 109 | if *token == "" { 110 | log.Fatal("token flag is required") 111 | } 112 | 113 | api := telegram.New(*token) 114 | api.Debug(*debug) 115 | bot := telebot.NewWithAPI(api) 116 | bot.Use(telebot.Recover()) // recover if handler panic 117 | 118 | netCtx, cancel := context.WithCancel(context.Background()) 119 | defer cancel() 120 | 121 | bot.HandleFunc(func(ctx context.Context) error { 122 | update := telebot.GetUpdate(ctx) // take update from context 123 | if update.Message == nil { 124 | return nil 125 | } 126 | api := telebot.GetAPI(ctx) // take api from context 127 | msg := telegram.CloneMessage(update.Message, nil) 128 | _, err := api.Send(ctx, msg) 129 | return err 130 | 131 | }) 132 | 133 | // Use command middleware, that helps to work with commands 134 | bot.Use(telebot.Commands(map[string]telebot.Commander{ 135 | "start": telebot.CommandFunc( 136 | func(ctx context.Context, arg string) error { 137 | 138 | api := telebot.GetAPI(ctx) 139 | update := telebot.GetUpdate(ctx) 140 | _, err := api.SendMessage(ctx, 141 | telegram.NewMessagef(update.Chat().ID, 142 | "received start with arg %s", arg, 143 | )) 144 | return err 145 | }), 146 | })) 147 | 148 | 149 | err := bot.Serve(netCtx) 150 | if err != nil { 151 | log.Fatal(err) 152 | } 153 | } 154 | ``` 155 | 156 | Use callback query and edit bot's message 157 | 158 | `go run ./examples/callback/main.go -debug -token BOT_TOKEN` 159 | 160 | 161 | ```go 162 | 163 | bot.HandleFunc(func(ctx context.Context) error { 164 | update := telebot.GetUpdate(ctx) // take update from context 165 | api := telebot.GetAPI(ctx) // take api from context 166 | 167 | if update.CallbackQuery != nil { 168 | data := update.CallbackQuery.Data 169 | if strings.HasPrefix(data, "sex:") { 170 | cfg := telegram.NewEditMessageText( 171 | update.Chat().ID, 172 | update.CallbackQuery.Message.MessageID, 173 | fmt.Sprintf("You sex: %s", data[4:]), 174 | ) 175 | api.AnswerCallbackQuery( 176 | ctx, 177 | telegram.NewAnswerCallback( 178 | update.CallbackQuery.ID, 179 | "Your configs changed", 180 | ), 181 | ) 182 | _, err := api.EditMessageText(ctx, cfg) 183 | return err 184 | } 185 | } 186 | 187 | msg := telegram.NewMessage(update.Chat().ID, 188 | "Your sex:") 189 | msg.ReplyMarkup = telegram.InlineKeyboardMarkup{ 190 | InlineKeyboard: telegram.NewVInlineKeyboard( 191 | "sex:", 192 | []string{"Female", "Male",}, 193 | []string{"female", "male",}, 194 | ), 195 | } 196 | _, err := api.SendMessage(ctx, msg) 197 | return err 198 | 199 | }) 200 | 201 | 202 | ``` 203 | 204 | Take a look at `./examples/` to know more how to use bot and telegram api. 205 | 206 | 207 | # TODO: 208 | 209 | - [x] Handlers 210 | - [x] Middleware 211 | - [x] Command middleware 212 | - [ ] Session middleware 213 | - [ ] Log middleware 214 | - [ ] Menu middleware 215 | - [ ] Examples 216 | - [x] Command 217 | - [x] CallbackAnswer 218 | - [x] Inline 219 | - [x] Proxy 220 | - [ ] Menu 221 | - [x] Add travis-ci integration 222 | - [x] Add coverage badge 223 | - [x] Add integration tests 224 | 225 | 226 | - [ ] Add gopkg version 227 | - [ ] Improve documentation 228 | - [ ] Benchmark ffjson and easyjson. 229 | - [ ] Add GAE example. 230 | 231 | 232 | # Supported API methods: 233 | - [x] getMe 234 | - [x] sendMessage 235 | - [x] forwardMessage 236 | - [x] sendPhoto 237 | - [x] sendAudio 238 | - [x] sendDocument 239 | - [x] sendSticker 240 | - [x] sendVideo 241 | - [x] sendVoice 242 | - [x] sendLocation 243 | - [x] sendChatAction 244 | - [x] getUserProfilePhotos 245 | - [x] getUpdates 246 | - [x] setWebhook 247 | - [x] getFile 248 | - [x] answerInlineQuery inline bots 249 | 250 | # Supported API v2 methods: 251 | - [x] sendVenue 252 | - [x] sendContact 253 | - [x] editMessageText 254 | - [x] editMessageCaption 255 | - [x] editMessageReplyMarkup 256 | - [x] kickChatMember 257 | - [x] unbanChatMember 258 | - [x] answerCallbackQuery 259 | - [x] getChat 260 | - [x] getChatMember 261 | - [x] getChatMembersCount 262 | - [x] getChatAdministrators 263 | - [x] leaveChat 264 | 265 | # Supported Inline modes 266 | 267 | 268 | - [x] InlineQueryResultArticle 269 | - [x] InlineQueryResultAudio 270 | - [x] InlineQueryResultContact 271 | - [x] InlineQueryResultDocument 272 | - [x] InlineQueryResultGif 273 | - [x] InlineQueryResultLocation 274 | - [x] InlineQueryResultMpeg4Gif 275 | - [x] InlineQueryResultPhoto 276 | - [x] InlineQueryResultVenue 277 | - [x] InlineQueryResultVideo 278 | - [x] InlineQueryResultVoice 279 | - [ ] InlineQueryResultCachedAudio 280 | - [ ] InlineQueryResultCachedDocument 281 | - [ ] InlineQueryResultCachedGif 282 | - [ ] InlineQueryResultCachedMpeg4Gif 283 | - [ ] InlineQueryResultCachedPhoto 284 | - [ ] InlineQueryResultCachedSticker 285 | - [ ] InlineQueryResultCachedVideo 286 | - [ ] InlineQueryResultCachedVoice 287 | - [ ] InputTextMessageContent 288 | - [ ] InputLocationMessageContent 289 | 290 | 291 | Other bots: 292 | I like this handler system 293 | https://bitbucket.org/master_groosha/telegram-proxy-bot/src/07a6b57372603acae7bdb78f771be132d063b899/proxy_bot.py?fileviewer=file-view-default 294 | 295 | -------------------------------------------------------------------------------- /all_test.go: -------------------------------------------------------------------------------- 1 | package telegram_test 2 | 3 | // all_test contains common test structures and interfaces 4 | 5 | import ( 6 | "errors" 7 | "net/url" 8 | 9 | "github.com/bot-api/telegram" 10 | ) 11 | 12 | type valuesI interface { 13 | Values() (url.Values, error) 14 | } 15 | 16 | // cfgTT is a configs test table structure 17 | type cfgTT struct { 18 | exp url.Values 19 | expErr error 20 | cfg valuesI 21 | } 22 | 23 | type replyBadMarkup struct { 24 | telegram.MarkReplyMarkup 25 | } 26 | 27 | var marshalError = errors.New("Can't be marshalled") 28 | 29 | func (m replyBadMarkup) MarshalJSON() ([]byte, error) { 30 | return nil, marshalError 31 | } 32 | 33 | type badInlineQueryResult struct { 34 | telegram.MarkInlineQueryResult 35 | } 36 | 37 | func (m badInlineQueryResult) MarshalJSON() ([]byte, error) { 38 | return nil, marshalError 39 | } 40 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | // Package telegram provides implementation for Telegram Bot API 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "mime/multipart" 13 | "net/http" 14 | "net/url" 15 | "strings" 16 | 17 | "golang.org/x/net/context" 18 | ) 19 | 20 | const ( 21 | // APIEndpoint is the endpoint for all API methods, 22 | // with formatting for Sprintf. 23 | APIEndpoint = "https://api.telegram.org/bot%s/%s" 24 | // FileEndpoint is the endpoint for downloading a file from Telegram. 25 | FileEndpoint = "https://api.telegram.org/file/bot%s/%s" 26 | ) 27 | 28 | // HTTPDoer interface helps to test api 29 | type HTTPDoer interface { 30 | Do(*http.Request) (*http.Response, error) 31 | } 32 | 33 | // DebugFunc describes function for debugging. 34 | type DebugFunc func(msg string, fields map[string]interface{}) 35 | 36 | // DefaultDebugFunc prints debug message to default logger 37 | var DefaultDebugFunc = func(msg string, fields map[string]interface{}) { 38 | log.Printf("%s %v", msg, fields) 39 | } 40 | 41 | // API implements Telegram bot API 42 | // described on https://core.telegram.org/bots/api 43 | type API struct { 44 | // token is a unique authentication string, 45 | // obtained by each bot when it is created. 46 | // The token looks something like 47 | // 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 48 | token string 49 | client HTTPDoer 50 | apiEndpoint string 51 | fileEndpoint string 52 | debug bool 53 | debugFunc DebugFunc 54 | } 55 | 56 | // New returns API instance with default http client 57 | func New(token string) *API { 58 | return NewWithClient(token, http.DefaultClient) 59 | } 60 | 61 | // NewWithClient returns API instance with custom http client 62 | func NewWithClient(token string, client HTTPDoer) *API { 63 | return &API{ 64 | token: token, 65 | client: client, 66 | apiEndpoint: APIEndpoint, 67 | fileEndpoint: FileEndpoint, 68 | debugFunc: DefaultDebugFunc, 69 | } 70 | } 71 | 72 | // Invoke is a generic method that helps to make request to Telegram Api. 73 | // Use particular methods instead (e.x. GetMe, GetUpdates etc). 74 | // The only case when this method seems useful is 75 | // when Telegram Api has method 76 | // that still doesn't exist in this implementation. 77 | func (c *API) Invoke(ctx context.Context, m Method, dst interface{}) error { 78 | params, err := m.Values() 79 | if err != nil { 80 | return err 81 | } 82 | var req *http.Request 83 | if mf, casted := m.(Filer); casted && !mf.Exist() { 84 | // upload a file, if FileID doesn't exist 85 | req, err = c.getUploadRequest( 86 | m.Name(), 87 | params, 88 | mf.Field(), 89 | mf.File(), 90 | ) 91 | } else { 92 | req, err = c.getFormRequest(m.Name(), params) 93 | } 94 | if err != nil { 95 | return err 96 | } 97 | return c.makeRequest(ctx, req, dst) 98 | } 99 | 100 | // Debug enables sending debug messages to default log 101 | func (c *API) Debug(val bool) { 102 | c.debug = val 103 | } 104 | 105 | // DebugFunc replaces default debug function 106 | func (c *API) DebugFunc(f DebugFunc) { 107 | c.debugFunc = f 108 | } 109 | 110 | // Telegram Bot API methods 111 | 112 | // GetMe returns basic information about the bot in form of a User object 113 | func (c *API) GetMe(ctx context.Context) (*User, error) { 114 | u := &User{} 115 | if err := c.Invoke(ctx, MeCfg{}, u); err != nil { 116 | return nil, err 117 | } 118 | return u, nil 119 | } 120 | 121 | // GetChat returns up to date information about the chat 122 | // (current name of the user for one-on-one conversations, 123 | // current username of a user, group or channel, etc.). 124 | // Returns a Chat object on success. 125 | func (c *API) GetChat(ctx context.Context, cfg GetChatCfg) (*Chat, error) { 126 | chat := &Chat{} 127 | if err := c.Invoke(ctx, cfg, chat); err != nil { 128 | return nil, err 129 | } 130 | return chat, nil 131 | } 132 | 133 | // GetChatAdministrators returns a list of administrators in a chat. 134 | // On success, returns an Array of ChatMember objects 135 | // that contains information about all chat administrators 136 | // except other bots. If the chat is a group or a supergroup 137 | // and no administrators were appointed, only the creator will be returned. 138 | func (c *API) GetChatAdministrators( 139 | ctx context.Context, 140 | cfg GetChatAdministratorsCfg) ([]ChatMember, error) { 141 | 142 | chatMembers := []ChatMember{} 143 | if err := c.Invoke(ctx, cfg, &chatMembers); err != nil { 144 | return nil, err 145 | } 146 | return chatMembers, nil 147 | } 148 | 149 | // GetChatMembersCount returns the number of members in a chat. 150 | func (c *API) GetChatMembersCount( 151 | ctx context.Context, 152 | cfg GetChatMembersCountCfg) (int, error) { 153 | 154 | var count int 155 | if err := c.Invoke(ctx, cfg, &count); err != nil { 156 | return count, err 157 | } 158 | return count, nil 159 | } 160 | 161 | // GetChatMember returns information about a member of a chat. 162 | func (c *API) GetChatMember( 163 | ctx context.Context, 164 | cfg GetChatMemberCfg) (*ChatMember, error) { 165 | 166 | member := &ChatMember{} 167 | if err := c.Invoke(ctx, cfg, member); err != nil { 168 | return nil, err 169 | } 170 | return member, nil 171 | } 172 | 173 | // KickChatMember kicks a user from a group or a supergroup. 174 | // In the case of supergroups, the user will not be able to return 175 | // to the group on their own using invite links, etc., unless unbanned first. 176 | // The bot must be an administrator in the group for this to work. 177 | // Returns True on success. 178 | func (c *API) KickChatMember( 179 | ctx context.Context, 180 | cfg KickChatMemberCfg) (bool, error) { 181 | 182 | var result bool 183 | if err := c.Invoke(ctx, cfg, &result); err != nil { 184 | return result, err 185 | } 186 | return result, nil 187 | } 188 | 189 | // UnbanChatMember unbans a previously kicked user in a supergroup. 190 | // The user will not return to the group automatically, 191 | // but will be able to join via link, etc. 192 | // The bot must be an administrator in the group for this to work. 193 | // Returns True on success. 194 | func (c *API) UnbanChatMember( 195 | ctx context.Context, 196 | cfg UnbanChatMemberCfg) (bool, error) { 197 | 198 | var result bool 199 | if err := c.Invoke(ctx, cfg, &result); err != nil { 200 | return result, err 201 | } 202 | return result, nil 203 | } 204 | 205 | // LeaveChat method helps your bot to leave a group, supergroup or channel. 206 | // Returns True on success. 207 | func (c *API) LeaveChat( 208 | ctx context.Context, 209 | cfg LeaveChatCfg) (bool, error) { 210 | 211 | var result bool 212 | if err := c.Invoke(ctx, cfg, &result); err != nil { 213 | return result, err 214 | } 215 | return result, nil 216 | } 217 | 218 | // GetUpdates requests incoming updates using long polling. 219 | // This method will not work if an outgoing webhook is set up. 220 | // In order to avoid getting duplicate updates, 221 | // recalculate offset after each server response. 222 | func (c *API) GetUpdates( 223 | ctx context.Context, 224 | cfg UpdateCfg) ([]Update, error) { 225 | 226 | updates := []Update{} 227 | if err := c.Invoke(ctx, cfg, &updates); err != nil { 228 | return nil, err 229 | } 230 | return updates, nil 231 | } 232 | 233 | // GetUserProfilePhotos requests a list of profile pictures for a user. 234 | func (c *API) GetUserProfilePhotos( 235 | ctx context.Context, 236 | cfg UserProfilePhotosCfg) (*UserProfilePhotos, error) { 237 | 238 | photos := &UserProfilePhotos{} 239 | if err := c.Invoke(ctx, cfg, photos); err != nil { 240 | return nil, err 241 | } 242 | return photos, nil 243 | } 244 | 245 | // SendChatAction tells the user that something is happening 246 | // on the bot's side. The status is set for 5 seconds or less 247 | // (when a message arrives from your bot, 248 | // Telegram clients clear its typing status). 249 | func (c *API) SendChatAction(ctx context.Context, cfg ChatActionCfg) error { 250 | return c.Invoke(ctx, cfg, nil) 251 | } 252 | 253 | // GetFile returns a File which can download a file from Telegram. 254 | // 255 | // Requires FileID. 256 | func (c *API) GetFile(ctx context.Context, cfg FileCfg) (*File, error) { 257 | var file File 258 | err := c.Invoke(ctx, cfg, &file) 259 | if err != nil { 260 | return nil, err 261 | } 262 | file.Link = fmt.Sprintf(c.fileEndpoint, c.token, file.FilePath) 263 | return &file, nil 264 | } 265 | 266 | // DownloadFile downloads file from telegram servers to w 267 | // 268 | // Requires FileID 269 | func (c *API) DownloadFile(ctx context.Context, cfg FileCfg, w io.Writer) error { 270 | f, err := c.GetFile(ctx, cfg) 271 | if err != nil { 272 | return err 273 | } 274 | req, err := http.NewRequest("GET", f.Link, nil) 275 | if err != nil { 276 | return err 277 | } 278 | resp, err := c.client.Do(req) 279 | if err != nil { 280 | return err 281 | } 282 | defer func() { 283 | if err := resp.Body.Close(); err != nil { 284 | if c.debug { 285 | c.print("body close error", map[string]interface{}{ 286 | "error": err.Error(), 287 | }) 288 | } 289 | } 290 | }() 291 | _, err = io.Copy(w, resp.Body) 292 | if err != nil { 293 | return err 294 | } 295 | return nil 296 | } 297 | 298 | // AnswerCallbackQuery sends a response to an inline query callback. 299 | func (c *API) AnswerCallbackQuery( 300 | ctx context.Context, 301 | cfg AnswerCallbackCfg) (bool, error) { 302 | 303 | var result bool 304 | return result, c.Invoke(ctx, cfg, &result) 305 | } 306 | 307 | // Edit method allows you to change an existing message in the message history 308 | // instead of sending a new one with a result of an action. 309 | // This is most useful for messages with inline keyboards using callback queries, 310 | // but can also help reduce clutter in conversations with regular chat bots. 311 | // Please note, that it is currently only possible to edit messages without 312 | // reply_markup or with inline keyboards. 313 | // 314 | // You can use this method directly or one of: 315 | // EditMessageText, EditMessageCaption, EditMessageReplyMarkup, 316 | func (c *API) Edit(ctx context.Context, cfg Method) (*EditResult, error) { 317 | er := &EditResult{} 318 | return er, c.Invoke(ctx, cfg, er) 319 | } 320 | 321 | // Send method sends message. 322 | // 323 | // TODO m0sth8: rewrite this doc 324 | func (c *API) Send(ctx context.Context, cfg Messenger) (*Message, error) { 325 | msg := cfg.Message() 326 | return msg, c.Invoke(ctx, cfg, msg) 327 | } 328 | 329 | // === Methods based on Send method 330 | 331 | // SendMessage sends text message. 332 | func (c *API) SendMessage( 333 | ctx context.Context, 334 | cfg MessageCfg) (*Message, error) { 335 | 336 | return c.Send(ctx, cfg) 337 | } 338 | 339 | // SendSticker sends message with sticker. 340 | func (c *API) SendSticker( 341 | ctx context.Context, 342 | cfg StickerCfg) (*Message, error) { 343 | 344 | return c.Send(ctx, cfg) 345 | } 346 | 347 | // SendVenue sends venue message. 348 | func (c *API) SendVenue( 349 | ctx context.Context, 350 | cfg VenueCfg) (*Message, error) { 351 | 352 | return c.Send(ctx, cfg) 353 | } 354 | 355 | // SendContact sends phone contact message. 356 | func (c *API) SendContact( 357 | ctx context.Context, 358 | cfg ContactCfg) (*Message, error) { 359 | 360 | return c.Send(ctx, cfg) 361 | } 362 | 363 | // SendPhoto sends photo. 364 | func (c *API) SendPhoto( 365 | ctx context.Context, 366 | cfg PhotoCfg) (*Message, error) { 367 | 368 | return c.Send(ctx, cfg) 369 | } 370 | 371 | // SendAudio sends Audio. 372 | func (c *API) SendAudio( 373 | ctx context.Context, 374 | cfg AudioCfg) (*Message, error) { 375 | 376 | return c.Send(ctx, cfg) 377 | } 378 | 379 | // SendVideo sends Video. 380 | func (c *API) SendVideo( 381 | ctx context.Context, 382 | cfg VideoCfg) (*Message, error) { 383 | 384 | return c.Send(ctx, cfg) 385 | } 386 | 387 | // SendVoice sends Voice. 388 | func (c *API) SendVoice( 389 | ctx context.Context, 390 | cfg VoiceCfg) (*Message, error) { 391 | 392 | return c.Send(ctx, cfg) 393 | } 394 | 395 | // SendDocument sends Document. 396 | func (c *API) SendDocument( 397 | ctx context.Context, 398 | cfg DocumentCfg) (*Message, error) { 399 | 400 | return c.Send(ctx, cfg) 401 | } 402 | 403 | // ForwardMessage forwards messages of any kind. 404 | func (c *API) ForwardMessage( 405 | ctx context.Context, 406 | cfg ForwardMessageCfg) (*Message, error) { 407 | 408 | return c.Send(ctx, cfg) 409 | } 410 | 411 | // === Methods based on Edit method 412 | 413 | // EditMessageText modifies the text of message. 414 | // Use this method to edit only the text of messages 415 | // sent by the bot or via the bot (for inline bots). 416 | // On success, if edited message is sent by the bot, 417 | // the edited Message is returned, otherwise True is returned. 418 | func (c *API) EditMessageText( 419 | ctx context.Context, 420 | cfg EditMessageTextCfg) (*EditResult, error) { 421 | 422 | return c.Edit(ctx, cfg) 423 | } 424 | 425 | // EditMessageCaption modifies the caption of message. 426 | // Use this method to edit only the caption of messages 427 | // sent by the bot or via the bot (for inline bots). 428 | // On success, if edited message is sent by the bot, 429 | // the edited Message is returned, otherwise True is returned. 430 | func (c *API) EditMessageCaption( 431 | ctx context.Context, 432 | cfg EditMessageCaptionCfg) (*EditResult, error) { 433 | 434 | return c.Edit(ctx, cfg) 435 | } 436 | 437 | // EditMessageReplyMarkup modifies the reply markup of message. 438 | // Use this method to edit only the reply markup of messages 439 | // sent by the bot or via the bot (for inline bots). 440 | // On success, if edited message is sent by the bot, 441 | // the edited Message is returned, otherwise True is returned. 442 | func (c *API) EditMessageReplyMarkup( 443 | ctx context.Context, 444 | cfg EditMessageReplyMarkupCfg) (*EditResult, error) { 445 | 446 | return c.Edit(ctx, cfg) 447 | } 448 | 449 | // SetWebhook sets a webhook. 450 | // Use this method to specify a url and receive incoming updates 451 | // via an outgoing webhook. Whenever there is an update for the bot, 452 | // we will send an HTTPS POST request to the specified url, 453 | // containing a JSON‐serialized Update. 454 | // In case of an unsuccessful request, 455 | // we will give up after a reasonable amount of attempts. 456 | // 457 | // If this is set, GetUpdates will not get any data! 458 | // 459 | // If you do not have a legitimate TLS certificate, 460 | // you need to include your self signed certificate with the config. 461 | func (c *API) SetWebhook(ctx context.Context, cfg WebhookCfg) error { 462 | return c.Invoke(ctx, cfg, nil) 463 | } 464 | 465 | // AnswerInlineQuery sends answers to an inline query. 466 | // On success, True is returned. No more than 50 results per query are allowed. 467 | func (c *API) AnswerInlineQuery(ctx context.Context, cfg AnswerInlineQueryCfg) (bool, error) { 468 | var result bool 469 | return result, c.Invoke(ctx, cfg, &result) 470 | } 471 | 472 | // Internal methods 473 | 474 | func (c *API) print(msg string, fields map[string]interface{}) { 475 | if c.debugFunc != nil { 476 | c.debugFunc(msg, fields) 477 | } 478 | } 479 | 480 | func (c *API) getFormRequest( 481 | method string, 482 | params url.Values) (*http.Request, error) { 483 | 484 | urlStr := fmt.Sprintf(c.apiEndpoint, c.token, method) 485 | body := params.Encode() 486 | if c.debug { 487 | c.print("request", map[string]interface{}{ 488 | "url": urlStr, 489 | "data": body, 490 | }) 491 | } 492 | 493 | req, err := http.NewRequest( 494 | "POST", 495 | urlStr, 496 | strings.NewReader(body), 497 | ) 498 | if err != nil { 499 | return nil, err 500 | } 501 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 502 | 503 | return req, nil 504 | } 505 | 506 | func (c *API) getUploadRequest( 507 | method string, 508 | params url.Values, 509 | field string, 510 | file InputFile) (*http.Request, error) { 511 | 512 | urlStr := fmt.Sprintf(c.apiEndpoint, c.token, method) 513 | 514 | if c.debug { 515 | c.print("file request", map[string]interface{}{ 516 | "url": urlStr, 517 | "data": params.Encode(), 518 | "file_field": field, 519 | "file_name": file.Name(), 520 | }) 521 | } 522 | 523 | buf := &bytes.Buffer{} 524 | 525 | w := multipart.NewWriter(buf) 526 | 527 | for key, values := range params { 528 | for _, value := range values { 529 | err := w.WriteField(key, value) 530 | if err != nil { 531 | return nil, fmt.Errorf( 532 | "can't write field %s, cause %s", 533 | key, err.Error(), 534 | ) 535 | } 536 | } 537 | } 538 | fw, err := w.CreateFormFile(field, file.Name()) 539 | if err != nil { 540 | return nil, err 541 | } 542 | if _, err = io.Copy(fw, file.Reader()); err != nil { 543 | return nil, err 544 | } 545 | if err = w.Close(); err != nil { 546 | return nil, err 547 | } 548 | 549 | req, err := http.NewRequest( 550 | "POST", 551 | urlStr, 552 | buf, 553 | ) 554 | if err != nil { 555 | return nil, err 556 | } 557 | req.Header.Set("Content-Type", w.FormDataContentType()) 558 | 559 | return req, nil 560 | } 561 | 562 | func (c *API) makeRequest( 563 | ctx context.Context, 564 | req *http.Request, 565 | dst interface{}) error { 566 | 567 | var err error 568 | var resp *http.Response 569 | 570 | resp, err = makeRequest(ctx, c.client, req) 571 | 572 | if err != nil { 573 | return err 574 | } 575 | defer func() { 576 | if err := resp.Body.Close(); err != nil { 577 | if c.debug { 578 | c.print("body close error", map[string]interface{}{ 579 | "error": err.Error(), 580 | }) 581 | } 582 | } 583 | }() 584 | if c.debug { 585 | c.print("response", map[string]interface{}{ 586 | "status_code": resp.StatusCode, 587 | }) 588 | } 589 | if resp.StatusCode == http.StatusForbidden { 590 | // read all from body to save keep-alive connection. 591 | if _, err = io.Copy(ioutil.Discard, resp.Body); err != nil { 592 | if c.debug { 593 | c.print("discard error", map[string]interface{}{ 594 | "error": err.Error(), 595 | }) 596 | } 597 | } 598 | return errForbidden 599 | } 600 | 601 | data, err := ioutil.ReadAll(resp.Body) 602 | if err != nil { 603 | return err 604 | } 605 | if c.debug { 606 | c.print("response", map[string]interface{}{ 607 | "data": string(data), 608 | }) 609 | } 610 | 611 | apiResponse := APIResponse{} 612 | err = json.Unmarshal(data, &apiResponse) 613 | if err != nil { 614 | return err 615 | } 616 | if !apiResponse.Ok { 617 | if apiResponse.ErrorCode == 401 { 618 | return errUnauthorized 619 | } 620 | return &APIError{ 621 | Description: apiResponse.Description, 622 | ErrorCode: apiResponse.ErrorCode, 623 | } 624 | } 625 | if dst != nil && apiResponse.Result != nil { 626 | err = json.Unmarshal(*apiResponse.Result, dst) 627 | } 628 | return err 629 | } 630 | -------------------------------------------------------------------------------- /api_17.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package telegram 4 | 5 | import ( 6 | "net/http" 7 | "net/url" 8 | 9 | "golang.org/x/net/context" 10 | ) 11 | 12 | // these errors are from net/http for go 1.7 13 | const ( 14 | errRequestCanceled = "net/http: request canceled" 15 | errRequestCanceledConn = "net/http: request canceled while waiting for connection" 16 | ) 17 | 18 | func makeRequest(ctx context.Context, client HTTPDoer, req *http.Request) (*http.Response, error) { 19 | var ( 20 | resp *http.Response 21 | err error 22 | ) 23 | if httpClient, ok := client.(*http.Client); ok { 24 | resp, err = httpClient.Do(req.WithContext(ctx)) 25 | } else { 26 | // TODO: implement cancel logic for non http.Client 27 | resp, err = client.Do(req) 28 | } 29 | if err != nil { 30 | if urlErr, casted := err.(*url.Error); casted { 31 | if urlErr.Err == context.Canceled { 32 | return resp, context.Canceled 33 | } 34 | errMsg := urlErr.Err.Error() 35 | if errMsg == errRequestCanceled || 36 | errMsg == errRequestCanceledConn { 37 | return resp, context.Canceled 38 | } 39 | } 40 | } 41 | return resp, err 42 | } 43 | -------------------------------------------------------------------------------- /api_integration_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | package telegram_test 4 | 5 | import ( 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "encoding/json" 11 | "fmt" 12 | "io/ioutil" 13 | "net/http" 14 | 15 | "github.com/bot-api/telegram" 16 | "golang.org/x/net/context" 17 | "golang.org/x/net/context/ctxhttp" 18 | "gopkg.in/stretchr/testify.v1/assert" 19 | "gopkg.in/stretchr/testify.v1/require" 20 | ) 21 | 22 | var ( 23 | apiBotToken string 24 | botUserID int64 = 201910478 25 | ) 26 | 27 | func init() { 28 | apiBotToken = os.Getenv("API_BOT_TOKEN") 29 | } 30 | 31 | func TestI_Api_GetMe(t *testing.T) { 32 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 33 | defer cancel() 34 | 35 | { 36 | api := telegram.New(apiBotToken) 37 | user, err := api.GetMe(ctx) 38 | 39 | require.NoError(t, err) 40 | assert.Equal(t, &telegram.User{ 41 | ID: 201910478, 42 | FirstName: "Chatter", 43 | LastName: "", 44 | Username: "PoboltaemBot", 45 | }, user) 46 | } 47 | { 48 | // send bad token 49 | api := telegram.New("3" + apiBotToken[1:]) 50 | api.Debug(true) 51 | _, err := api.GetMe(ctx) 52 | 53 | require.EqualError(t, err, "unauthorized") 54 | } 55 | } 56 | 57 | func TestI_Api_GetChat(t *testing.T) { 58 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 59 | defer cancel() 60 | 61 | { 62 | api := telegram.New(apiBotToken) 63 | chat, err := api.GetChat( 64 | ctx, 65 | telegram.GetChatCfg{ 66 | BaseChat: telegram.BaseChat{ 67 | ID: -139295199, 68 | }, 69 | }, 70 | ) 71 | 72 | require.NoError(t, err) 73 | assert.Equal(t, &telegram.Chat{ 74 | ID: -139295199, 75 | Type: telegram.GroupChatType, 76 | Title: "Group for Integration tests", 77 | }, chat) 78 | } 79 | { 80 | // send bad token 81 | api := telegram.New("3" + apiBotToken[1:]) 82 | api.Debug(true) 83 | _, err := api.GetChat( 84 | ctx, 85 | telegram.GetChatCfg{ 86 | BaseChat: telegram.BaseChat{ 87 | ID: -139295199, 88 | }, 89 | }, 90 | ) 91 | 92 | require.EqualError(t, err, "unauthorized") 93 | } 94 | } 95 | 96 | func TestI_Api_GetChatAdministrators(t *testing.T) { 97 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 98 | defer cancel() 99 | 100 | { 101 | api := telegram.New(apiBotToken) 102 | chatAdministrators, err := api.GetChatAdministrators( 103 | ctx, 104 | telegram.GetChatAdministratorsCfg{ 105 | BaseChat: telegram.BaseChat{ 106 | ID: -139295199, 107 | }, 108 | }, 109 | ) 110 | 111 | require.NoError(t, err) 112 | assert.Equal(t, []telegram.ChatMember{ 113 | { 114 | User: telegram.User{ 115 | ID: 87409032, 116 | FirstName: "Slava", 117 | LastName: "Bakhmutov", 118 | Username: "m0sth8", 119 | }, 120 | Status: "creator", 121 | }, 122 | }, chatAdministrators) 123 | } 124 | } 125 | 126 | func TestI_Api_GetChatMembersCount(t *testing.T) { 127 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 128 | defer cancel() 129 | 130 | { 131 | api := telegram.New(apiBotToken) 132 | membersCount, err := api.GetChatMembersCount( 133 | ctx, 134 | telegram.GetChatMembersCountCfg{ 135 | BaseChat: telegram.BaseChat{ 136 | ID: -139295199, 137 | }, 138 | }, 139 | ) 140 | 141 | require.NoError(t, err) 142 | assert.Equal(t, 3, membersCount) 143 | } 144 | } 145 | 146 | func TestI_Api_GetChatMember(t *testing.T) { 147 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 148 | defer cancel() 149 | 150 | { 151 | api := telegram.New(apiBotToken) 152 | member, err := api.GetChatMember( 153 | ctx, 154 | telegram.GetChatMemberCfg{ 155 | BaseChat: telegram.BaseChat{ 156 | ID: -139295199, 157 | }, 158 | UserID: 87409032, 159 | }, 160 | ) 161 | 162 | require.NoError(t, err) 163 | assert.Equal(t, &telegram.ChatMember{ 164 | User: telegram.User{ 165 | ID: 87409032, 166 | FirstName: "Slava", 167 | LastName: "Bakhmutov", 168 | Username: "m0sth8", 169 | }, 170 | Status: telegram.CreatorMemberStatus, 171 | }, member) 172 | } 173 | } 174 | 175 | func TestI_Api_GetUserProfilePhotos(t *testing.T) { 176 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 177 | defer cancel() 178 | 179 | { 180 | api := telegram.New(apiBotToken) 181 | photos, err := api.GetUserProfilePhotos( 182 | ctx, 183 | telegram.NewUserProfilePhotos(botUserID), 184 | ) 185 | 186 | require.NoError(t, err) 187 | expected := &telegram.UserProfilePhotos{} 188 | err = parseTestData( 189 | "integration_user_profile_photos.json", 190 | expected) 191 | require.NoError(t, err) 192 | assert.Equal(t, expected, photos) 193 | } 194 | } 195 | 196 | func TestI_Api_GetFile(t *testing.T) { 197 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 198 | defer cancel() 199 | 200 | api := telegram.New(apiBotToken) 201 | api.Debug(true) 202 | 203 | photos := &telegram.UserProfilePhotos{} 204 | err := parseTestData( 205 | "integration_user_profile_photos.json", 206 | photos) 207 | require.NoError(t, err) 208 | 209 | for _, photo := range photos.Photos[0] { 210 | file, err := api.GetFile(ctx, telegram.FileCfg{ 211 | FileID: photo.FileID, 212 | }) 213 | require.NoError(t, err) 214 | assert.Equal(t, photo.FileSize, file.FileSize) 215 | assert.Equal(t, photo.FileID, file.FileID) 216 | assert.NotEmpty(t, file.FilePath) 217 | assert.NotEmpty(t, file.Link) 218 | resp, err := ctxhttp.Get(ctx, http.DefaultClient, file.Link) 219 | require.NoError(t, err) 220 | defer resp.Body.Close() 221 | actualData, err := ioutil.ReadAll(resp.Body) 222 | require.NoError(t, err) 223 | assert.Len(t, actualData, file.FileSize) 224 | expectedData, err := ioutil.ReadFile( 225 | fmt.Sprintf("./testdata/files/%s.jpg", file.FileID)) 226 | require.NoError(t, err) 227 | assert.Equal(t, expectedData, actualData) 228 | //ioutil.WriteFile( 229 | // fmt.Sprintf("./testdata/files/%s.jpg", file.FileID), 230 | // fileData, 0666, 231 | //) 232 | } 233 | 234 | } 235 | 236 | func parseTestData(filename string, dst interface{}) error { 237 | f, err := os.Open(fmt.Sprintf("./testdata/%s", filename)) 238 | if err != nil { 239 | return err 240 | } 241 | return json.NewDecoder(f).Decode(dst) 242 | } 243 | -------------------------------------------------------------------------------- /api_internal_test.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "mime/multipart" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "strings" 13 | "testing" 14 | "time" 15 | 16 | "golang.org/x/net/context" 17 | "gopkg.in/stretchr/testify.v1/assert" 18 | "gopkg.in/stretchr/testify.v1/require" 19 | ) 20 | 21 | func TestNew(t *testing.T) { 22 | api := New("token123") 23 | assert.Equal(t, "token123", api.token) 24 | assert.Equal(t, 25 | http.DefaultClient, 26 | api.client, 27 | "Expected client to be http.DefaultClient") 28 | assert.Equal(t, APIEndpoint, api.apiEndpoint) 29 | assert.Equal(t, FileEndpoint, api.fileEndpoint) 30 | } 31 | 32 | func TestNewWithClient(t *testing.T) { 33 | customClient := &http.Client{} 34 | apiActual := NewWithClient("token123", customClient) 35 | assert.Equal(t, apiActual.client, customClient) 36 | } 37 | 38 | func TestApi_Debug(t *testing.T) { 39 | api := New("token123") 40 | assert.False(t, api.debug, "Expected debug to be false by default") 41 | api.Debug(true) 42 | assert.True(t, api.debug, "Expected debug to be true") 43 | api.Debug(false) 44 | assert.False(t, api.debug, "Expected debug to be false") 45 | 46 | } 47 | 48 | func TestApi_makeRequest_testContextCancel(t *testing.T) { 49 | // Use real http.Client for this test to test ctxhttp 50 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 51 | defer cancel() 52 | 53 | handlerCh := make(chan bool, 1) 54 | testHandle := func(res http.ResponseWriter, req *http.Request) { 55 | // handler received request 56 | handlerCh <- true 57 | // handler waits till context is done 58 | select { 59 | case <-ctx.Done(): 60 | return 61 | } 62 | } 63 | testServ := httptest.NewServer(http.HandlerFunc(testHandle)) 64 | api := New("token") 65 | api.apiEndpoint = fmt.Sprintf("%s/bot%%s/%%s", testServ.URL) 66 | 67 | reqCtx, cancelReq := context.WithTimeout( 68 | context.Background(), 69 | time.Millisecond*500) 70 | errCh := make(chan error, 1) 71 | go func() { 72 | req, err := http.NewRequest( 73 | "GET", 74 | testServ.URL, 75 | nil, 76 | ) 77 | if err != nil { 78 | errCh <- err 79 | return 80 | } 81 | err = api.makeRequest(reqCtx, req, nil) 82 | errCh <- err 83 | }() 84 | // wait till http handler receives request 85 | <-handlerCh 86 | cancelReq() 87 | // receive error from makeRequest 88 | err := <-errCh 89 | assert.Equal(t, context.Canceled, err, "Actual err %v", err) 90 | 91 | } 92 | 93 | type fakeClient struct { 94 | res *http.Response 95 | err error 96 | req *http.Request 97 | ctx context.Context 98 | } 99 | 100 | func (c *fakeClient) Do(req *http.Request) (*http.Response, error) { 101 | c.req = req 102 | return c.res, c.err 103 | } 104 | 105 | func newFakeClient(ctx context.Context, res *http.Response, err error) *fakeClient { 106 | return &fakeClient{ 107 | res: res, 108 | err: err, 109 | ctx: ctx, 110 | } 111 | } 112 | 113 | func TestApi_makeRequest(t *testing.T) { 114 | testTable := []struct { 115 | method string 116 | params url.Values 117 | dst interface{} 118 | 119 | resp string 120 | respCode int 121 | 122 | expErr string 123 | expForm url.Values 124 | expURL string 125 | expDst interface{} 126 | }{ 127 | { 128 | params: url.Values{ 129 | "key1": {"value1"}, 130 | }, 131 | method: "search_method", 132 | resp: "{\"ok\": true, \"result\": \"data\"}", 133 | dst: stringP(""), 134 | expDst: stringP("data"), 135 | expURL: "/bottoken/search_method", 136 | }, 137 | { 138 | resp: "not a json string", 139 | expErr: "invalid character 'o' in literal null (expecting 'u')", 140 | }, 141 | { 142 | resp: "{\"ok\": false, \"description\": \"someError\"}", 143 | expErr: "apiError: someError", 144 | }, 145 | { 146 | resp: "{\"ok\": true, \"result\": \"wrong json\"}", 147 | dst: intP(0), 148 | expErr: "json: cannot unmarshal string into Go value of type int", 149 | }, 150 | { 151 | respCode: http.StatusForbidden, 152 | expErr: "forbidden", 153 | }, 154 | } 155 | 156 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 157 | defer cancel() 158 | 159 | api := New("token") 160 | api.debug = true 161 | 162 | for i, tt := range testTable { 163 | t.Logf("Experiment %d", i) 164 | var dst interface{} 165 | if tt.dst != nil { 166 | dst = tt.dst 167 | } else { 168 | dst = nil 169 | } 170 | fResBody := newNopCloser(bytes.NewBufferString(tt.resp)) 171 | fRes := &http.Response{ 172 | Body: fResBody, 173 | StatusCode: http.StatusOK, 174 | } 175 | if tt.respCode != 0 { 176 | fRes.StatusCode = tt.respCode 177 | } 178 | fc := newFakeClient(ctx, fRes, nil) 179 | api.client = fc 180 | req, err := api.getFormRequest(tt.method, tt.params) 181 | require.NoError(t, err) 182 | err = api.makeRequest(ctx, req, dst) 183 | // check error from makeRequest 184 | if tt.expErr != "" { 185 | assert.EqualError(t, err, tt.expErr) 186 | continue 187 | } else { 188 | if !assert.NoError(t, err) { 189 | continue 190 | } 191 | } 192 | 193 | if tt.expURL != "" { 194 | assert.Equal(t, tt.expURL, fc.req.URL.Path) 195 | } 196 | assert.Equal(t, 197 | fc.req.Header.Get("Content-Type"), 198 | "application/x-www-form-urlencoded", 199 | ) 200 | reqBody, err := ioutil.ReadAll(fc.req.Body) 201 | if err != nil { 202 | t.Fatal(err) 203 | } 204 | // check request body 205 | if tt.expForm != nil { 206 | assert.Equal(t, tt.expForm.Encode(), string(reqBody)) 207 | } else { 208 | assert.Equal(t, tt.params.Encode(), string(reqBody)) 209 | } 210 | 211 | // check dst 212 | assert.Equal(t, tt.expDst, tt.dst) 213 | 214 | // body should be closed 215 | assert.True(t, 216 | fResBody.closed, 217 | "response body should be closed") 218 | // body should be empty 219 | data := make([]byte, 512) 220 | n, err := fRes.Body.Read(data) 221 | if !assert.Equal(t, 0, n, "response body should be empty") { 222 | t.Logf("Body has next data %s", string(data[:n])) 223 | } 224 | assert.Equal(t, io.EOF, err) 225 | 226 | } 227 | 228 | } 229 | 230 | func TestApi_getFormRequest(t *testing.T) { 231 | api := New("token") 232 | api.debug = true 233 | req, err := api.getFormRequest("method", url.Values{ 234 | "key1": {"value1"}, 235 | "key2": {"value2"}, 236 | }) 237 | require.NoError(t, err) 238 | assert.Equal(t, 239 | "https://api.telegram.org/bottoken/method", 240 | req.URL.String()) 241 | data, err := ioutil.ReadAll(req.Body) 242 | require.NoError(t, err) 243 | assert.Equal(t, "key1=value1&key2=value2", string(data)) 244 | assert.Equal(t, 245 | "application/x-www-form-urlencoded", 246 | req.Header.Get("Content-Type")) 247 | 248 | // check errors 249 | api.apiEndpoint = "not a url" 250 | _, err = api.getFormRequest("method", url.Values{}) 251 | require.Error(t, err) 252 | assert.EqualError(t, 253 | err, "parse not a url%!(EXTRA string=token,"+ 254 | " string=method): invalid URL escape \"%!(\"") 255 | } 256 | 257 | func TestApi_getUploadRequest(t *testing.T) { 258 | api := New("token") 259 | api.debug = true 260 | buf := bytes.NewBufferString("file content") 261 | req, err := api.getUploadRequest( 262 | "method", 263 | url.Values{ 264 | "key1": {"value1"}, 265 | "key2": {"value2"}, 266 | }, 267 | "field_name", 268 | NewInputFile("filename", buf), 269 | ) 270 | require.NoError(t, err) 271 | assert.Equal(t, 272 | "https://api.telegram.org/bottoken/method", 273 | req.URL.String()) 274 | contentType := req.Header.Get("Content-Type") 275 | require.True(t, strings.HasPrefix(contentType, 276 | "multipart/form-data; boundary=")) 277 | boundary := contentType[30:] 278 | 279 | r := multipart.NewReader(req.Body, boundary) 280 | for i := 0; i < 3; i++ { 281 | part, err := r.NextPart() 282 | require.NoError(t, err) 283 | data, err := ioutil.ReadAll(part) 284 | require.NoError(t, err) 285 | if part.FormName() == "key1" { 286 | assert.Equal(t, "value1", string(data)) 287 | } 288 | if part.FormName() == "key2" { 289 | assert.Equal(t, "value2", string(data)) 290 | } 291 | if part.FormName() == "field_name" { 292 | assert.Equal(t, "file content", string(data)) 293 | assert.Equal(t, 294 | "application/octet-stream", 295 | part.Header.Get("Content-Type"), 296 | ) 297 | } 298 | } 299 | 300 | // check errors 301 | api.apiEndpoint = "not a url" 302 | buf = bytes.NewBufferString("file content") 303 | req, err = api.getUploadRequest( 304 | "method", 305 | url.Values{ 306 | "key1": {"value1"}, 307 | "key2": {"value2"}, 308 | }, 309 | "field_name", 310 | NewInputFile("filename", buf), 311 | ) 312 | require.Error(t, err) 313 | assert.EqualError(t, 314 | err, "parse not a url%!(EXTRA string=token,"+ 315 | " string=method): invalid URL escape \"%!(\"") 316 | } 317 | 318 | // helpers 319 | 320 | func stringP(s string) *string { 321 | return &s 322 | } 323 | 324 | func intP(s int) *int { 325 | return &s 326 | } 327 | 328 | type nopCloser struct { 329 | io.Reader 330 | closed bool 331 | } 332 | 333 | func (c *nopCloser) Close() error { 334 | c.closed = true 335 | return nil 336 | } 337 | 338 | // NopCloser returns a ReadCloser with a no-op Close method wrapping 339 | // the provided Reader r. 340 | func newNopCloser(r io.Reader) *nopCloser { 341 | return &nopCloser{r, false} 342 | } 343 | -------------------------------------------------------------------------------- /api_pre_17.go: -------------------------------------------------------------------------------- 1 | // +build !go1.7 2 | 3 | package telegram 4 | 5 | import ( 6 | "net/http" 7 | 8 | "golang.org/x/net/context" 9 | "golang.org/x/net/context/ctxhttp" 10 | ) 11 | 12 | func makeRequest(ctx context.Context, client HTTPDoer, req *http.Request) (*http.Response, error) { 13 | var ( 14 | resp *http.Response 15 | err error 16 | ) 17 | if httpClient, ok := client.(*http.Client); ok { 18 | resp, err = ctxhttp.Do(ctx, httpClient, req) 19 | } else { 20 | // TODO: implement cancel logic for non http.Client 21 | resp, err = client.Do(req) 22 | } 23 | return resp, err 24 | } 25 | -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | // telegram_test package tests only public interface 2 | package telegram_test 3 | 4 | import ( 5 | "bytes" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/bot-api/telegram" 11 | "github.com/m0sth8/httpmock" 12 | "golang.org/x/net/context" 13 | "gopkg.in/stretchr/testify.v1/assert" 14 | "gopkg.in/stretchr/testify.v1/require" 15 | ) 16 | 17 | var apiToken = "token" 18 | 19 | var forbiddenResponder = httpmock.NewStringResponder( 20 | http.StatusForbidden, "forbidden") 21 | 22 | func TestAPI_GetMe(t *testing.T) { 23 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 24 | defer cancel() 25 | httpmock.Activate() 26 | defer httpmock.DeactivateAndReset() 27 | api := telegram.New(apiToken) 28 | 29 | testTable := []struct { 30 | resp httpmock.Responder 31 | 32 | expErr string 33 | expUser *telegram.User 34 | expURL string 35 | }{ 36 | { 37 | resp: httpmock.NewStringResponder(200, ` 38 | { 39 | "ok": true, 40 | "result": { 41 | "id": 100, 42 | "first_name": "first_name", 43 | "last_name": "last_name", 44 | "username": "username" 45 | } 46 | }`), 47 | expUser: &telegram.User{ 48 | ID: 100, 49 | FirstName: "first_name", 50 | LastName: "last_name", 51 | Username: "username", 52 | }, 53 | expURL: "/bottoken/getMe", 54 | }, 55 | { 56 | resp: forbiddenResponder, 57 | expErr: "forbidden", 58 | expURL: "/bottoken/getMe", 59 | }, 60 | } 61 | for i, tt := range testTable { 62 | t.Logf("Experiment %d", i) 63 | httpmock.RegisterResponder( 64 | "POST", 65 | "https://api.telegram.org"+tt.expURL, 66 | tt.resp, 67 | ) 68 | 69 | user, err := api.GetMe(ctx) 70 | if tt.expErr != "" { 71 | assert.EqualError(t, err, tt.expErr) 72 | } else { 73 | assert.NoError(t, err) 74 | } 75 | assert.Equal(t, tt.expUser, user) 76 | 77 | httpmock.Reset() 78 | 79 | } 80 | } 81 | 82 | func TestAPI_GetUpdates(t *testing.T) { 83 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 84 | defer cancel() 85 | httpmock.Activate() 86 | defer httpmock.DeactivateAndReset() 87 | api := telegram.New(apiToken) 88 | 89 | testTable := []struct { 90 | cfg telegram.UpdateCfg 91 | resp httpmock.Responder 92 | 93 | expErr string 94 | expResult []telegram.Update 95 | expURL string 96 | }{ 97 | { 98 | cfg: telegram.UpdateCfg{ 99 | Offset: 10, 100 | Limit: 100, 101 | Timeout: 1000, 102 | }, 103 | resp: httpmock.NewStringResponder(200, ` 104 | { 105 | "ok": true, 106 | "result": [ 107 | { 108 | "update_id": 100, 109 | "message": { 110 | "message_id": 135 111 | } 112 | } 113 | ] 114 | }`), 115 | expResult: []telegram.Update{ 116 | { 117 | UpdateID: 100, 118 | Message: &telegram.Message{ 119 | MessageID: 135, 120 | }, 121 | }, 122 | }, 123 | expURL: "/bottoken/getUpdates", 124 | }, 125 | { 126 | cfg: telegram.UpdateCfg{ 127 | Offset: 10, 128 | Limit: 100, 129 | Timeout: 1000, 130 | }, 131 | resp: httpmock.NewStringResponder(200, ` 132 | { 133 | "ok": true, 134 | "result": [] 135 | }`), 136 | expResult: []telegram.Update{}, 137 | expURL: "/bottoken/getUpdates", 138 | }, 139 | { 140 | cfg: telegram.UpdateCfg{}, 141 | resp: forbiddenResponder, 142 | expErr: "forbidden", 143 | expURL: "/bottoken/getUpdates", 144 | }, 145 | } 146 | for i, tt := range testTable { 147 | t.Logf("Experiment %d", i) 148 | 149 | httpmock.RegisterResponder( 150 | "POST", 151 | "https://api.telegram.org"+tt.expURL, 152 | tt.resp, 153 | ) 154 | 155 | result, err := api.GetUpdates(ctx, tt.cfg) 156 | if tt.expErr != "" { 157 | assert.EqualError(t, err, tt.expErr) 158 | } else { 159 | assert.NoError(t, err) 160 | } 161 | assert.Equal(t, result, tt.expResult) 162 | 163 | httpmock.Reset() 164 | 165 | } 166 | } 167 | 168 | func TestAPI_GetUserProfilePhotos(t *testing.T) { 169 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 170 | defer cancel() 171 | httpmock.Activate() 172 | defer httpmock.DeactivateAndReset() 173 | api := telegram.New(apiToken) 174 | 175 | testTable := []struct { 176 | cfg telegram.UserProfilePhotosCfg 177 | resp httpmock.Responder 178 | 179 | expErr string 180 | expResult *telegram.UserProfilePhotos 181 | expURL string 182 | }{ 183 | { 184 | cfg: telegram.UserProfilePhotosCfg{ 185 | UserID: 100, 186 | }, 187 | resp: httpmock.NewStringResponder(200, ` 188 | { 189 | "ok": true, 190 | "result": 191 | { 192 | "total_count": 2, 193 | "photos": [ 194 | [ 195 | { 196 | "file_id": "value1", 197 | "file_size": 345, 198 | "width": 100, 199 | "height": 200 200 | } 201 | ], 202 | [ 203 | { 204 | "file_id": "value2", 205 | "file_size": 500, 206 | "width": 200, 207 | "height": 100 208 | } 209 | ] 210 | ] 211 | } 212 | }`), 213 | expResult: &telegram.UserProfilePhotos{ 214 | TotalCount: 2, 215 | Photos: [][]telegram.PhotoSize{ 216 | { 217 | { 218 | MetaFile: telegram.MetaFile{ 219 | FileID: "value1", 220 | FileSize: 345, 221 | }, 222 | Size: telegram.Size{ 223 | Width: 100, 224 | Height: 200, 225 | }, 226 | }, 227 | }, 228 | { 229 | { 230 | MetaFile: telegram.MetaFile{ 231 | FileID: "value2", 232 | FileSize: 500, 233 | }, 234 | Size: telegram.Size{ 235 | Width: 200, 236 | Height: 100, 237 | }, 238 | }, 239 | }, 240 | }, 241 | }, 242 | expURL: "/bottoken/getUserProfilePhotos", 243 | }, 244 | { 245 | cfg: telegram.UserProfilePhotosCfg{}, 246 | resp: nil, 247 | expErr: "UserID required", 248 | expURL: "/bottoken/getUserProfilePhotos", 249 | }, 250 | { 251 | cfg: telegram.UserProfilePhotosCfg{ 252 | UserID: 10, 253 | Limit: 1000, 254 | }, 255 | resp: nil, 256 | expErr: "field Limit is invalid: should be between 1 and 100", 257 | expURL: "/bottoken/getUserProfilePhotos", 258 | }, 259 | { 260 | cfg: telegram.UserProfilePhotosCfg{ 261 | UserID: 10, 262 | }, 263 | resp: forbiddenResponder, 264 | expErr: "forbidden", 265 | expURL: "/bottoken/getUserProfilePhotos", 266 | }, 267 | } 268 | for i, tt := range testTable { 269 | t.Logf("Experiment %d", i) 270 | 271 | httpmock.RegisterResponder( 272 | "POST", 273 | "https://api.telegram.org"+tt.expURL, 274 | tt.resp, 275 | ) 276 | 277 | result, err := api.GetUserProfilePhotos(ctx, tt.cfg) 278 | if tt.expErr != "" { 279 | assert.EqualError(t, err, tt.expErr) 280 | } else { 281 | assert.NoError(t, err) 282 | } 283 | assert.Equal(t, result, tt.expResult) 284 | 285 | httpmock.Reset() 286 | 287 | } 288 | } 289 | 290 | func TestAPI_DownloadFile(t *testing.T) { 291 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 292 | defer cancel() 293 | httpmock.Activate() 294 | defer httpmock.DeactivateAndReset() 295 | api := telegram.New(apiToken) 296 | 297 | httpmock.RegisterResponder( 298 | "POST", 299 | "https://api.telegram.org/bottoken/getFile", 300 | httpmock.NewStringResponder(200, ` 301 | { 302 | "ok": true, 303 | "result": 304 | { 305 | "file_path": "path_to_file", 306 | "file_id": "file_id" 307 | } 308 | }`, 309 | ), 310 | ) 311 | httpmock.RegisterResponder( 312 | "GET", 313 | "https://api.telegram.org/file/bottoken/path_to_file", 314 | httpmock.NewStringResponder(200, "FILE DATA"), 315 | ) 316 | f, err := api.GetFile(ctx, telegram.FileCfg{FileID: "file_id"}) 317 | require.NoError(t, err) 318 | assert.Equal(t, "file_id", f.FileID) 319 | assert.Equal(t, "https://api.telegram.org/file/bottoken/path_to_file", f.Link) 320 | 321 | buf := bytes.NewBuffer(nil) 322 | err = api.DownloadFile(ctx, telegram.FileCfg{FileID: "file_id"}, buf) 323 | require.NoError(t, err) 324 | assert.Equal(t, "FILE DATA", buf.String()) 325 | 326 | } 327 | -------------------------------------------------------------------------------- /cmd/tbot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/bot-api/telegram" 15 | "github.com/chzyer/readline" 16 | "golang.org/x/net/context" 17 | ) 18 | 19 | var nl = []byte("\n") 20 | 21 | var cmdUsage = map[string]string{ 22 | "getMe": "Returns information about bot. ", 23 | "getUpdates": "Get updates. Usage: getUpdates [offset, [limit, [timeout]]]", 24 | } 25 | 26 | var completer = readline.NewPrefixCompleter( 27 | readline.PcItem("help"), 28 | readline.PcItem("quit"), 29 | readline.PcItem("q"), 30 | readline.PcItem("getMe"), 31 | readline.PcItem("getUpdates"), 32 | readline.PcItem("getFile"), 33 | readline.PcItem("getUserProfilePhotos"), 34 | readline.PcItem("sendChatAction"), 35 | readline.PcItem("sendMessage"), 36 | readline.PcItem("sendLocation"), 37 | readline.PcItem("openChat"), 38 | readline.PcItem("setWebhook"), 39 | readline.PcItem("removeWebhook"), 40 | ) 41 | 42 | func readToken(rl *readline.Instance) (string, error) { 43 | for { 44 | token, err := rl.ReadPassword("Give me the token: ") 45 | if err != nil { 46 | return "", err 47 | } 48 | if len(token) > 0 { 49 | return string(token), nil 50 | } 51 | } 52 | } 53 | 54 | func usage(rl *readline.Instance, completer *readline.PrefixCompleter) { 55 | w := rl.Stdout() 56 | io.WriteString(w, "Available commands:\n") 57 | io.WriteString(w, completer.Tree(" ")) 58 | } 59 | 60 | func help(rl *readline.Instance, cmd string) { 61 | fmt.Fprintln(rl.Stdout(), cmdUsage[cmd]) 62 | } 63 | 64 | func writeJSON(rl *readline.Instance, method string, obj interface{}) { 65 | data, err := json.MarshalIndent(obj, "", " ") 66 | if err != nil { 67 | io.WriteString( 68 | rl.Stderr(), 69 | fmt.Sprintf("%s json error: %s\n", method, err.Error()), 70 | ) 71 | } 72 | 73 | fmt.Fprintf(rl.Stdout(), "%s:", method) 74 | _, err = rl.Stdout().Write(data) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | fmt.Fprintln(rl.Stdout()) 79 | } 80 | 81 | func getMe(rl *readline.Instance, ctx context.Context, cl *telegram.API) { 82 | me, err := cl.GetMe(ctx) 83 | if err != nil { 84 | log.Printf("getMe error: %s\n", err.Error()) 85 | return 86 | } 87 | writeJSON(rl, "getMe", me) 88 | 89 | } 90 | 91 | func openChat( 92 | ctx context.Context, 93 | rl *readline.Instance, 94 | cl *telegram.API, 95 | args ...string) error { 96 | 97 | if len(args) != 1 { 98 | return fmt.Errorf("usage: openChat chatID") 99 | } 100 | chatID, err := strconv.ParseInt(args[0], 10, 64) 101 | if err != nil { 102 | return err 103 | } 104 | updateCh := make(chan *telegram.Update) 105 | upCtx, cancel := context.WithCancel(ctx) 106 | defer cancel() 107 | go func( 108 | ctx context.Context, 109 | cfg telegram.UpdateCfg, 110 | out chan<- *telegram.Update) { 111 | loop: 112 | for { 113 | updates, err := cl.GetUpdates(ctx, cfg) 114 | if err != nil { 115 | if err == context.Canceled { 116 | close(out) 117 | return 118 | } 119 | log.Println(err) 120 | if telegram.IsForbiddenError(err) { 121 | close(out) 122 | return 123 | } 124 | log.Println("Failed to get updates, retrying in 3 seconds...") 125 | select { 126 | case <-ctx.Done(): 127 | return 128 | case <-time.After(time.Second * 3): 129 | continue loop 130 | } 131 | } 132 | 133 | for _, update := range updates { 134 | if update.UpdateID >= cfg.Offset { 135 | cfg.Offset = update.UpdateID + 1 136 | select { 137 | case <-ctx.Done(): 138 | return 139 | case out <- &update: 140 | } 141 | } 142 | } 143 | } 144 | }(upCtx, telegram.UpdateCfg{Timeout: 30}, updateCh) 145 | 146 | go func() { 147 | for { 148 | line, err := rl.Readline() 149 | if err != nil { 150 | // io.EOF, readline.ErrInterrupt 151 | break 152 | } 153 | line = strings.TrimSpace(line) 154 | if strings.HasPrefix(line, "/q") { 155 | cancel() 156 | return 157 | } 158 | msg := telegram.NewMessage(chatID, line) 159 | msg.ParseMode = telegram.MarkdownMode 160 | _, err = cl.SendMessage(ctx, msg) 161 | if err != nil { 162 | log.Println(err.Error()) 163 | } 164 | } 165 | }() 166 | 167 | for { 168 | select { 169 | case <-ctx.Done(): 170 | return nil 171 | case update, ok := <-updateCh: 172 | if !ok { 173 | return nil 174 | } 175 | if update.Message.Chat.ID != chatID { 176 | continue 177 | } 178 | fmt.Fprintf( 179 | rl.Stdout(), 180 | "@%s: %s\n", 181 | update.Message.From.Username, 182 | update.Message.Text, 183 | ) 184 | rl.Refresh() 185 | } 186 | } 187 | } 188 | 189 | func getUpdates(rl *readline.Instance, ctx context.Context, cl *telegram.API, 190 | cfg telegram.UpdateCfg) { 191 | 192 | updates, err := cl.GetUpdates(ctx, cfg) 193 | if err != nil { 194 | io.WriteString(rl.Stderr(), 195 | fmt.Sprintf("getUpdates error: %s\n", err.Error())) 196 | return 197 | } 198 | writeJSON(rl, "getUpdates", updates) 199 | } 200 | 201 | func main() { 202 | token := flag.String("token", "", "telegram bot token") 203 | debug := flag.Bool("debug", false, "show debug information") 204 | flag.Parse() 205 | 206 | rl, err := readline.NewEx(&readline.Config{ 207 | Prompt: "> ", 208 | HistoryFile: "/tmp/tbot.history", 209 | AutoComplete: completer, 210 | }) 211 | if err != nil { 212 | panic(err) 213 | } 214 | defer rl.Close() 215 | 216 | log.SetOutput(rl.Stderr()) 217 | if *token == "" { 218 | *token, err = readToken(rl) 219 | if err != nil { 220 | panic(err) 221 | } 222 | } 223 | if *debug { 224 | log.Printf("token: %s\n", *token) 225 | } 226 | 227 | cl := telegram.New(*token) 228 | cl.Debug(*debug) 229 | 230 | ctx, cancel := context.WithCancel(context.Background()) 231 | defer cancel() 232 | 233 | me, err := cl.GetMe(ctx) 234 | if err != nil { 235 | log.Printf("getMe error: %s\n", err.Error()) 236 | log.Println("Exited") 237 | return 238 | } 239 | rl.SetPrompt(fmt.Sprintf("@%s> ", me.Username)) 240 | 241 | fmt.Fprintln(rl.Stdout(), "Type 'help' for more information.") 242 | loop: 243 | for { 244 | line, err := rl.Readline() 245 | if err != nil { // io.EOF, readline.ErrInterrupt 246 | break 247 | } 248 | line = strings.TrimSpace(line) 249 | switch { 250 | case line == "quit" || line == "q": 251 | break loop 252 | case line == "help": 253 | usage(rl, completer) 254 | case strings.HasPrefix(line, "help "): 255 | help(rl, line[5:]) 256 | case line == "getMe": 257 | getMe(rl, ctx, cl) 258 | case line == "getUpdates": 259 | getUpdates(rl, ctx, cl, telegram.UpdateCfg{}) 260 | case strings.HasPrefix(line, "getUpdates"): 261 | args := strings.Split(line[len("getUpdates "):], " ") 262 | cfg := telegram.UpdateCfg{} 263 | switch len(args) { 264 | case 3: 265 | cfg.Timeout, err = strconv.Atoi(args[2]) 266 | if err != nil { 267 | log.Println(err.Error()) 268 | continue loop 269 | } 270 | fallthrough 271 | case 2: 272 | cfg.Limit, err = strconv.Atoi(args[1]) 273 | if err != nil { 274 | log.Println(err.Error()) 275 | continue loop 276 | } 277 | fallthrough 278 | case 1: 279 | cfg.Offset, err = strconv.ParseInt(args[0], 10, 64) 280 | if err != nil { 281 | log.Println(err.Error()) 282 | continue loop 283 | } 284 | } 285 | getUpdates(rl, ctx, cl, cfg) 286 | 287 | case strings.HasPrefix(line, "sendChatAction "): 288 | args := strings.Split(line[len("sendChatAction "):], " ") 289 | if len(args) != 2 { 290 | log.Println("usage: sendChatAction @to action") 291 | continue loop 292 | } 293 | chatID, err := strconv.ParseInt(args[0], 10, 64) 294 | if err != nil { 295 | log.Println(err.Error()) 296 | continue loop 297 | } 298 | err = cl.SendChatAction( 299 | ctx, 300 | telegram.NewChatAction(chatID, args[1]), 301 | ) 302 | if err != nil { 303 | log.Println(err.Error()) 304 | } 305 | 306 | case strings.HasPrefix(line, "sendMessage "): 307 | args := strings.Split(line[len("sendMessage "):], " ") 308 | if len(args) < 2 { 309 | log.Println("usage: sendMessage @to text") 310 | continue loop 311 | } 312 | cfg := telegram.MessageCfg{ 313 | Text: strings.Join(args[1:], " "), 314 | } 315 | if strings.HasPrefix(args[0], "@") { 316 | cfg.ChannelUsername = args[0] 317 | } else { 318 | chatID, err := strconv.Atoi(args[0]) 319 | if err != nil { 320 | log.Println(err.Error()) 321 | continue loop 322 | } 323 | cfg.ID = int64(chatID) 324 | } 325 | _, err = cl.SendMessage(ctx, cfg) 326 | if err != nil { 327 | log.Println(err.Error()) 328 | } 329 | case strings.HasPrefix(line, "getUserProfilePhotos "): 330 | args := strings.Split(line[len("getUserProfilePhotos "):], " ") 331 | if len(args) != 1 { 332 | log.Println("usage: getUserProfilePhotos user_id") 333 | continue loop 334 | } 335 | userID, err := strconv.ParseInt(args[0], 10, 64) 336 | if err != nil { 337 | log.Println(err.Error()) 338 | continue loop 339 | } 340 | cfg := telegram.NewUserProfilePhotos(userID) 341 | photos, err := cl.GetUserProfilePhotos(ctx, cfg) 342 | if err != nil { 343 | log.Println(err.Error()) 344 | continue loop 345 | } 346 | writeJSON(rl, "getUserProfilePhotos", photos) 347 | 348 | case strings.HasPrefix(line, "getFile "): 349 | args := strings.Split(line[len("getFile "):], " ") 350 | if len(args) != 1 { 351 | log.Println("usage: getFile file_id") 352 | continue loop 353 | } 354 | cfg := telegram.FileCfg{ 355 | FileID: args[0], 356 | } 357 | file, err := cl.GetFile(ctx, cfg) 358 | if err != nil { 359 | log.Println(err.Error()) 360 | continue loop 361 | } 362 | writeJSON(rl, "getFile", file) 363 | case line == "openChat": 364 | err := openChat(ctx, rl, cl) 365 | if err != nil { 366 | log.Println(err.Error()) 367 | continue loop 368 | } 369 | case strings.HasPrefix(line, "openChat "): 370 | args := strings.Split(line[len("openChat "):], " ") 371 | 372 | err := openChat(ctx, rl, cl, args...) 373 | if err != nil { 374 | log.Println(err.Error()) 375 | continue loop 376 | } 377 | 378 | case strings.HasPrefix(line, "sendLocation "): 379 | args := strings.Split(line[len("sendLocation "):], " ") 380 | if len(args) != 3 { 381 | log.Println("usage: sendLocation @to lat lon") 382 | continue loop 383 | } 384 | chatID, err := strconv.ParseInt(args[0], 10, 64) 385 | if err != nil { 386 | log.Println(err.Error()) 387 | continue loop 388 | } 389 | lat, err := strconv.ParseFloat(args[1], 64) 390 | if err != nil { 391 | log.Println(err.Error()) 392 | continue loop 393 | } 394 | lon, err := strconv.ParseFloat(args[2], 64) 395 | if err != nil { 396 | log.Println(err.Error()) 397 | continue loop 398 | } 399 | cfg := telegram.NewLocation(chatID, lat, lon) 400 | _, err = cl.Send(ctx, cfg) 401 | if err != nil { 402 | log.Println(err.Error()) 403 | continue loop 404 | } 405 | case line == "removeWebhook": 406 | err := cl.SetWebhook(ctx, telegram.NewWebhook("")) 407 | if err != nil { 408 | log.Println(err.Error()) 409 | continue loop 410 | } 411 | 412 | case strings.HasPrefix(line, "setWebhook "): 413 | args := strings.Split(line[len("setWebhook "):], " ") 414 | hookURL := "" 415 | if len(args) > 0 { 416 | hookURL = args[0] 417 | } 418 | var inputFile telegram.InputFile 419 | if len(args) > 1 { 420 | cert, err := os.Open(args[1]) 421 | if err != nil { 422 | log.Println(err.Error()) 423 | continue loop 424 | } 425 | inputFile = telegram.NewInputFile("cert.pem", cert) 426 | } 427 | cfg := telegram.NewWebhookWithCert(hookURL, 428 | inputFile) 429 | err = cl.SetWebhook(ctx, cfg) 430 | if inputFile != nil { 431 | if rc, ok := inputFile.Reader().(io.ReadCloser); ok { 432 | rc.Close() 433 | } 434 | } 435 | if err != nil { 436 | log.Println(err.Error()) 437 | continue loop 438 | } 439 | } 440 | 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /configs.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "encoding/json" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | // BaseChat describes chat settings. It's an abstract type. 10 | // 11 | // You must set ID or ChannelUsername. 12 | type BaseChat struct { 13 | // Unique identifier for the target chat 14 | ID int64 15 | // Username of the target channel (in the format @channelusername) 16 | ChannelUsername string 17 | } 18 | 19 | // Values returns RequiredError if neither ID or ChannelUsername are empty. 20 | // Prefers ChannelUsername if both ID and ChannelUsername are not empty. 21 | func (c BaseChat) Values() (url.Values, error) { 22 | v := url.Values{} 23 | if c.ChannelUsername != "" { 24 | v.Add("chat_id", c.ChannelUsername) 25 | return v, nil 26 | } 27 | if c.ID != 0 { 28 | v.Add("chat_id", strconv.FormatInt(c.ID, 10)) 29 | return v, nil 30 | } 31 | return nil, NewRequiredError("ID", "ChannelUsername") 32 | } 33 | 34 | // SetChatID sets new chat id 35 | func (c *BaseChat) SetChatID(id int64) { 36 | c.ID = id 37 | } 38 | 39 | // GetChatCfg contains information about a getChat request. 40 | type GetChatCfg struct { 41 | BaseChat 42 | } 43 | 44 | // Name returns method name 45 | func (cfg GetChatCfg) Name() string { 46 | return getChatMethod 47 | } 48 | 49 | // Values returns a url.Values representation of GetChatCfg. 50 | // Returns RequiredError if Chat is not set. 51 | func (cfg GetChatCfg) Values() (url.Values, error) { 52 | return cfg.BaseChat.Values() 53 | } 54 | 55 | // GetChatAdministratorsCfg contains information about a getChat request. 56 | type GetChatAdministratorsCfg struct { 57 | BaseChat 58 | } 59 | 60 | // Name returns method name 61 | func (cfg GetChatAdministratorsCfg) Name() string { 62 | return getChatAdministratorsMethod 63 | } 64 | 65 | // Values returns a url.Values representation of GetChatCfg. 66 | // Returns RequiredError if Chat is not set. 67 | func (cfg GetChatAdministratorsCfg) Values() (url.Values, error) { 68 | return cfg.BaseChat.Values() 69 | } 70 | 71 | // GetChatMembersCountCfg contains information about a getChatMemberCount request. 72 | type GetChatMembersCountCfg struct { 73 | BaseChat 74 | } 75 | 76 | // Name returns method name 77 | func (cfg GetChatMembersCountCfg) Name() string { 78 | return getChatMembersCountMethod 79 | } 80 | 81 | // Values returns a url.Values representation of GetChatMembersCountCfg. 82 | // Returns RequiredError if Chat is not set. 83 | func (cfg GetChatMembersCountCfg) Values() (url.Values, error) { 84 | return cfg.BaseChat.Values() 85 | } 86 | 87 | // GetChatMemberCfg contains information about a getChatMember request. 88 | type GetChatMemberCfg struct { 89 | BaseChat 90 | UserID int64 `json:"user_id"` 91 | } 92 | 93 | // Name returns method name 94 | func (cfg GetChatMemberCfg) Name() string { 95 | return getChatMemberMethod 96 | } 97 | 98 | // Values returns a url.Values representation of GetChatMemberCfg. 99 | // Returns RequiredError if Chat or UserID are not set. 100 | func (cfg GetChatMemberCfg) Values() (url.Values, error) { 101 | v, err := cfg.BaseChat.Values() 102 | if err != nil { 103 | return nil, err 104 | } 105 | if cfg.UserID == 0 { 106 | return nil, NewRequiredError("UserID") 107 | } 108 | v.Add("user_id", strconv.FormatInt(cfg.UserID, 10)) 109 | return v, nil 110 | } 111 | 112 | // KickChatMemberCfg contains information about a kickChatMember request. 113 | type KickChatMemberCfg struct { 114 | BaseChat 115 | UserID int64 `json:"user_id"` 116 | } 117 | 118 | // Name returns method name 119 | func (cfg KickChatMemberCfg) Name() string { 120 | return kickChatMemberMethod 121 | } 122 | 123 | // Values returns a url.Values representation of KickChatMemberCfg. 124 | // Returns RequiredError if Chat or UserID are not set. 125 | func (cfg KickChatMemberCfg) Values() (url.Values, error) { 126 | v, err := cfg.BaseChat.Values() 127 | if err != nil { 128 | return nil, err 129 | } 130 | if cfg.UserID == 0 { 131 | return nil, NewRequiredError("UserID") 132 | } 133 | v.Add("user_id", strconv.FormatInt(cfg.UserID, 10)) 134 | return v, nil 135 | } 136 | 137 | // UnbanChatMemberCfg contains information about a unbanChatMember request. 138 | type UnbanChatMemberCfg struct { 139 | BaseChat 140 | UserID int64 `json:"user_id"` 141 | } 142 | 143 | // Name returns method name 144 | func (cfg UnbanChatMemberCfg) Name() string { 145 | return unbanChatMemberMethod 146 | } 147 | 148 | // Values returns a url.Values representation of UnbanChatMemberCfg. 149 | // Returns RequiredError if Chat or UserID are not set. 150 | func (cfg UnbanChatMemberCfg) Values() (url.Values, error) { 151 | v, err := cfg.BaseChat.Values() 152 | if err != nil { 153 | return nil, err 154 | } 155 | if cfg.UserID == 0 { 156 | return nil, NewRequiredError("UserID") 157 | } 158 | v.Add("user_id", strconv.FormatInt(cfg.UserID, 10)) 159 | return v, nil 160 | } 161 | 162 | // LeaveChatCfg contains information about a leaveChat request. 163 | type LeaveChatCfg struct { 164 | BaseChat 165 | } 166 | 167 | // Name returns method name 168 | func (cfg LeaveChatCfg) Name() string { 169 | return leaveChatMethod 170 | } 171 | 172 | // Values returns a url.Values representation of LeaveChatCfg. 173 | // Returns RequiredError if Chat is not set. 174 | func (cfg LeaveChatCfg) Values() (url.Values, error) { 175 | return cfg.BaseChat.Values() 176 | } 177 | 178 | // MeCfg contains information about a getMe request. 179 | type MeCfg struct{} 180 | 181 | // Name returns method name 182 | func (cfg MeCfg) Name() string { 183 | return getMeMethod 184 | } 185 | 186 | // Values for getMe is empty 187 | func (cfg MeCfg) Values() (url.Values, error) { 188 | return nil, nil 189 | } 190 | 191 | // UpdateCfg contains information about a getUpdates request. 192 | type UpdateCfg struct { 193 | // Identifier of the first update to be returned. 194 | // Must be greater by one than the highest 195 | // among the identifiers of previously received updates. 196 | // By default, updates starting with the earliest 197 | // unconfirmed update are returned. An update is considered confirmed 198 | // as soon as getUpdates is called with an offset 199 | // higher than its update_id. The negative offset 200 | // can be specified to retrieve updates starting 201 | // from -offset update from the end of the updates queue. 202 | // All previous updates will forgotten. 203 | Offset int64 204 | // Limits the number of updates to be retrieved. 205 | // Values between 1—100 are accepted. Defaults to 100. 206 | Limit int 207 | // Timeout in seconds for long polling. 208 | // Defaults to 0, i.e. usual short polling 209 | Timeout int 210 | } 211 | 212 | // Name returns method name 213 | func (cfg UpdateCfg) Name() string { 214 | return getUpdatesMethod 215 | } 216 | 217 | // Values returns getUpdate params. 218 | // It returns error if Limit is not between 0 and 100. 219 | // Zero params are not included to request. 220 | func (cfg UpdateCfg) Values() (url.Values, error) { 221 | if cfg.Limit < 0 || cfg.Limit > 100 { 222 | return nil, NewValidationError( 223 | "Limit", 224 | "should be between 1 and 100", 225 | ) 226 | } 227 | v := url.Values{} 228 | if cfg.Offset != 0 { 229 | v.Add("offset", strconv.FormatInt(cfg.Offset, 10)) 230 | } 231 | if cfg.Limit > 0 { 232 | v.Add("limit", strconv.Itoa(cfg.Limit)) 233 | } 234 | if cfg.Timeout > 0 { 235 | v.Add("timeout", strconv.Itoa(cfg.Timeout)) 236 | } 237 | return v, nil 238 | } 239 | 240 | // ChatActionCfg contains information about a SendChatAction request. 241 | // Action field is required. 242 | type ChatActionCfg struct { 243 | BaseChat 244 | // Type of action to broadcast. 245 | // Choose one, depending on what the user is about to receive: 246 | // typing for text messages, upload_photo for photos, 247 | // record_video or upload_video for videos, 248 | // record_audio or upload_audio for audio files, 249 | // upload_document for general files, 250 | // find_location for location data. 251 | // Use one of constants: ActionTyping, ActionFindLocation, etc 252 | Action string 253 | } 254 | 255 | // Name returns method name 256 | func (cfg ChatActionCfg) Name() string { 257 | return sendChatActionMethod 258 | } 259 | 260 | // Values returns a url.Values representation of ChatActionCfg. 261 | // Returns a RequiredError if Action is empty. 262 | func (cfg ChatActionCfg) Values() (url.Values, error) { 263 | v, err := cfg.BaseChat.Values() 264 | if err != nil { 265 | return nil, err 266 | } 267 | if cfg.Action == "" { 268 | return nil, NewRequiredError("Action") 269 | } 270 | v.Add("action", cfg.Action) 271 | return v, nil 272 | } 273 | 274 | // UserProfilePhotosCfg contains information about a 275 | // GetUserProfilePhotos request. 276 | type UserProfilePhotosCfg struct { 277 | UserID int64 278 | // Sequential number of the first photo to be returned. 279 | // By default, all photos are returned. 280 | Offset int 281 | // Limits the number of photos to be retrieved. 282 | // Values between 1—100 are accepted. Defaults to 100. 283 | Limit int 284 | } 285 | 286 | // Name returns method name 287 | func (cfg UserProfilePhotosCfg) Name() string { 288 | return getUserProfilePhotosMethod 289 | } 290 | 291 | // Values returns a url.Values representation of UserProfilePhotosCfg. 292 | // Returns RequiredError if UserID is empty. 293 | func (cfg UserProfilePhotosCfg) Values() (url.Values, error) { 294 | if cfg.Limit < 0 || cfg.Limit > 100 { 295 | return nil, NewValidationError( 296 | "Limit", 297 | "should be between 1 and 100", 298 | ) 299 | } 300 | v := url.Values{} 301 | if cfg.UserID == 0 { 302 | return nil, NewRequiredError("UserID") 303 | } 304 | v.Add("user_id", strconv.FormatInt(cfg.UserID, 10)) 305 | if cfg.Offset != 0 { 306 | v.Add("offset", strconv.Itoa(cfg.Offset)) 307 | } 308 | if cfg.Limit != 0 { 309 | v.Add("limit", strconv.Itoa(cfg.Limit)) 310 | } 311 | return v, nil 312 | } 313 | 314 | // FileCfg has information about a file hosted on Telegram. 315 | type FileCfg struct { 316 | FileID string 317 | } 318 | 319 | // Name returns method name 320 | func (cfg FileCfg) Name() string { 321 | return getFileMethod 322 | } 323 | 324 | // Values returns a url.Values representation of FileCfg. 325 | func (cfg FileCfg) Values() (url.Values, error) { 326 | v := url.Values{} 327 | v.Add("file_id", cfg.FileID) 328 | return v, nil 329 | } 330 | 331 | // WebhookCfg contains information about a SetWebhook request. 332 | // Implements Method and Filer interface 333 | type WebhookCfg struct { 334 | URL string 335 | // self generated TLS certificate 336 | Certificate InputFile 337 | } 338 | 339 | // Name method returns Telegram API method name for sending Location. 340 | func (cfg WebhookCfg) Name() string { 341 | return setWebhookMethod 342 | } 343 | 344 | // Values returns a url.Values representation of Webhook config. 345 | func (cfg WebhookCfg) Values() (url.Values, error) { 346 | v := url.Values{} 347 | v.Add("url", cfg.URL) 348 | return v, nil 349 | } 350 | 351 | // Field returns name for webhook file data 352 | func (cfg WebhookCfg) Field() string { 353 | return "certificate" 354 | } 355 | 356 | // File returns certificate data 357 | func (cfg WebhookCfg) File() InputFile { 358 | return cfg.Certificate 359 | } 360 | 361 | // Exist is true if we don't have a certificate to upload. 362 | // It's kind of confusing. 363 | func (cfg WebhookCfg) Exist() bool { 364 | return cfg.Certificate == nil 365 | } 366 | 367 | // Reset method sets new Certificate 368 | func (cfg *WebhookCfg) Reset(i InputFile) { 369 | cfg.Certificate = i 370 | } 371 | 372 | // GetFileID for webhook is always empty 373 | func (cfg *WebhookCfg) GetFileID() string { 374 | return "" 375 | } 376 | 377 | // AnswerCallbackCfg contains information on making a anserCallbackQuery response. 378 | type AnswerCallbackCfg struct { 379 | CallbackQueryID string `json:"callback_query_id"` 380 | Text string `json:"text"` 381 | ShowAlert bool `json:"show_alert"` 382 | } 383 | 384 | // Name returns method name 385 | func (cfg AnswerCallbackCfg) Name() string { 386 | return answerCallbackQueryMethod 387 | } 388 | 389 | // Values returns a url.Values representation of AnswerCallbackCfg. 390 | // Returns a RequiredError if Action is empty. 391 | func (cfg AnswerCallbackCfg) Values() (url.Values, error) { 392 | v := url.Values{} 393 | v.Add("callback_query_id", cfg.CallbackQueryID) 394 | v.Add("text", cfg.Text) 395 | v.Add("show_alert", strconv.FormatBool(cfg.ShowAlert)) 396 | return v, nil 397 | } 398 | 399 | // AnswerInlineQueryCfg contains information on making an InlineQuery response. 400 | type AnswerInlineQueryCfg struct { 401 | // Unique identifier for the answered query 402 | InlineQueryID string `json:"inline_query_id"` 403 | Results []InlineQueryResult `json:"results"` 404 | // The maximum amount of time in seconds 405 | // that the result of the inline query may be cached on the server. 406 | // Defaults to 300. 407 | CacheTime int `json:"cache_time,omitempty"` 408 | // Pass True, if results may be cached on the server side 409 | // only for the user that sent the query. 410 | // By default, results may be returned to any user 411 | // who sends the same query 412 | IsPersonal bool `json:"is_personal,omitempty"` 413 | // Pass the offset that a client should send in the next query 414 | // with the same text to receive more results. 415 | // Pass an empty string if there are no more results 416 | // or if you don‘t support pagination. 417 | // Offset length can’t exceed 64 bytes. 418 | NextOffset string `json:"next_offset,omitempty"` 419 | // If passed, clients will display a button with specified text 420 | // that switches the user to a private chat with the bot and 421 | // sends the bot a start message with the parameter switch_pm_parameter 422 | SwitchPMText string `json:"switch_pm_text,omitempty"` 423 | // Parameter for the start message sent to the bot 424 | // when user presses the switch button 425 | SwitchPMParameter string `json:"switch_pm_parameter"` 426 | } 427 | 428 | // Name returns method name 429 | func (cfg AnswerInlineQueryCfg) Name() string { 430 | return answerInlineQueryMethod 431 | } 432 | 433 | // Values returns a url.Values representation of AnswerInlineQueryCfg. 434 | // Returns a RequiredError if Action is empty. 435 | func (cfg AnswerInlineQueryCfg) Values() (url.Values, error) { 436 | v := url.Values{} 437 | if cfg.Results == nil || len(cfg.Results) == 0 { 438 | return nil, NewRequiredError("Results") 439 | } 440 | data, err := json.Marshal(cfg.Results) 441 | if err != nil { 442 | return nil, err 443 | } 444 | v.Add("results", string(data)) 445 | v.Add("inline_query_id", cfg.InlineQueryID) 446 | if cfg.CacheTime > 0 { 447 | v.Add("cache_time", strconv.Itoa(cfg.CacheTime)) 448 | } 449 | if cfg.IsPersonal { 450 | v.Add("is_personal", strconv.FormatBool(cfg.IsPersonal)) 451 | } 452 | if cfg.NextOffset != "" { 453 | v.Add("next_offset", cfg.NextOffset) 454 | } 455 | if cfg.SwitchPMText != "" { 456 | v.Add("switch_pm_text", cfg.SwitchPMText) 457 | } 458 | if cfg.SwitchPMParameter != "" { 459 | v.Add("switch_pm_parameter", cfg.SwitchPMParameter) 460 | } 461 | return v, nil 462 | } 463 | -------------------------------------------------------------------------------- /configs_edit.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "encoding/json" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | // BaseEdit is base type of all chat edits. 10 | type BaseEdit struct { 11 | // Required if inline_message_id is not specified. 12 | // Unique identifier for the target chat or 13 | // username of the target channel (in the format @channelusername) 14 | ChatID int64 15 | ChannelUsername string 16 | // Required if inline_message_id is not specified. 17 | // Unique identifier of the sent message 18 | MessageID int64 19 | // Required if chat_id and message_id are not specified. 20 | // Identifier of the inline message 21 | InlineMessageID string 22 | // Only InlineKeyboardMarkup supported right now. 23 | ReplyMarkup ReplyMarkup 24 | } 25 | 26 | // Values returns a url.Values representation of BaseEdit. 27 | func (m BaseEdit) Values() (url.Values, error) { 28 | v := url.Values{} 29 | 30 | if m.ChannelUsername != "" { 31 | v.Add("chat_id", m.ChannelUsername) 32 | } else { 33 | v.Add("chat_id", strconv.FormatInt(m.ChatID, 10)) 34 | } 35 | if m.MessageID != 0 { 36 | v.Add("message_id", strconv.FormatInt(m.MessageID, 10)) 37 | } 38 | if m.InlineMessageID != "" { 39 | v.Add("inline_message_id", m.InlineMessageID) 40 | } 41 | 42 | if m.ReplyMarkup != nil { 43 | data, err := json.Marshal(m.ReplyMarkup) 44 | if err != nil { 45 | return nil, err 46 | } 47 | v.Add("reply_markup", string(data)) 48 | } 49 | 50 | return v, nil 51 | } 52 | 53 | // EditMessageTextCfg allows you to modify the text in a message. 54 | type EditMessageTextCfg struct { 55 | BaseEdit 56 | // New text of the message 57 | Text string 58 | // Send Markdown or HTML, if you want Telegram apps 59 | // to show bold, italic, fixed-width text 60 | // or inline URLs in your bot's message. Optional. 61 | ParseMode string 62 | // Disables link previews for links in this message. Optional. 63 | DisableWebPagePreview bool 64 | } 65 | 66 | // Values returns a url.Values representation of EditMessageTextCfg. 67 | func (cfg EditMessageTextCfg) Values() (url.Values, error) { 68 | v, err := cfg.BaseEdit.Values() 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | v.Add("text", cfg.Text) 74 | if cfg.ParseMode != "" { 75 | v.Add("parse_mode", cfg.ParseMode) 76 | } 77 | if cfg.DisableWebPagePreview { 78 | v.Add("disable_web_page_preview", "true") 79 | } 80 | 81 | return v, nil 82 | } 83 | 84 | // Name returns method name 85 | func (EditMessageTextCfg) Name() string { 86 | return editMessageTextMethod 87 | } 88 | 89 | // EditMessageCaptionCfg allows you to modify the caption of a message. 90 | type EditMessageCaptionCfg struct { 91 | BaseEdit 92 | // New caption of the message 93 | Caption string 94 | } 95 | 96 | // Values returns a url.Values representation of EditMessageCaptionCfg. 97 | func (cfg EditMessageCaptionCfg) Values() (url.Values, error) { 98 | v, err := cfg.BaseEdit.Values() 99 | if err != nil { 100 | return nil, err 101 | } 102 | v.Add("text", cfg.Caption) 103 | 104 | return v, nil 105 | } 106 | 107 | // Name returns method name 108 | func (EditMessageCaptionCfg) Name() string { 109 | return editMessageCaptionMethod 110 | } 111 | 112 | // EditMessageReplyMarkupCfg allows you to modify the reply markup of a message. 113 | type EditMessageReplyMarkupCfg struct { 114 | BaseEdit 115 | } 116 | 117 | // Values returns a url.Values representation of EditMessageReplyMarkupCfg. 118 | func (cfg EditMessageReplyMarkupCfg) Values() (url.Values, error) { 119 | return cfg.BaseEdit.Values() 120 | } 121 | 122 | // Name returns method name 123 | func (EditMessageReplyMarkupCfg) Name() string { 124 | return editMessageReplyMarkupMethod 125 | } 126 | -------------------------------------------------------------------------------- /configs_message.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/url" 8 | "strconv" 9 | ) 10 | 11 | // Assert interfaces 12 | var _ Filer = (*PhotoCfg)(nil) 13 | var _ Filer = (*AudioCfg)(nil) 14 | var _ Filer = (*VideoCfg)(nil) 15 | var _ Filer = (*VoiceCfg)(nil) 16 | var _ Filer = (*DocumentCfg)(nil) 17 | var _ Filer = (*StickerCfg)(nil) 18 | var _ Filer = (*WebhookCfg)(nil) 19 | 20 | // BaseMessage is a base type for all message config types. 21 | // Implements Messenger interface. 22 | type BaseMessage struct { 23 | BaseChat 24 | // If the message is a reply, ID of the original message 25 | ReplyToMessageID int64 26 | // Additional interface options. 27 | // A JSON-serialized object for a custom reply keyboard, 28 | // instructions to hide keyboard or to force a reply from the user. 29 | ReplyMarkup ReplyMarkup 30 | // Sends the message silently. 31 | // iOS users will not receive a notification, 32 | // Android users will receive a notification with no sound. 33 | // Other apps coming soon. 34 | DisableNotification bool 35 | } 36 | 37 | // Values returns url.Values representation of BaseMessage 38 | func (m BaseMessage) Values() (url.Values, error) { 39 | v, err := m.BaseChat.Values() 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | if m.ReplyToMessageID != 0 { 45 | v.Add( 46 | "reply_to_message_id", 47 | strconv.FormatInt(m.ReplyToMessageID, 10), 48 | ) 49 | } 50 | 51 | if m.ReplyMarkup != nil { 52 | data, err := json.Marshal(m.ReplyMarkup) 53 | if err != nil { 54 | return nil, err 55 | } 56 | v.Add("reply_markup", string(data)) 57 | } 58 | if m.DisableNotification { 59 | v.Add( 60 | "disable_notification", 61 | strconv.FormatBool(m.DisableNotification), 62 | ) 63 | } 64 | 65 | return v, nil 66 | } 67 | 68 | // Message returns instance of *Message type. 69 | func (BaseMessage) Message() *Message { 70 | return &Message{} 71 | } 72 | 73 | // MessageCfg contains information about a SendMessage request. 74 | // Use it to send text messages. 75 | // Implements Messenger interface. 76 | type MessageCfg struct { 77 | BaseMessage 78 | Text string 79 | // Send Markdown or HTML, if you want Telegram apps to show 80 | // bold, italic, fixed-width text or inline URLs in your bot's message. 81 | // Use one of constants: ModeHTML, ModeMarkdown. 82 | ParseMode string 83 | // Disables link previews for links in this message. 84 | DisableWebPagePreview bool 85 | } 86 | 87 | // Name returns method name 88 | func (cfg MessageCfg) Name() string { 89 | return sendMessageMethod 90 | } 91 | 92 | // Values returns a url.Values representation of MessageCfg. 93 | // Returns RequiredError if Text is empty. 94 | func (cfg MessageCfg) Values() (url.Values, error) { 95 | v, err := cfg.BaseMessage.Values() 96 | if err != nil { 97 | return nil, err 98 | } 99 | if cfg.Text == "" { 100 | return nil, NewRequiredError("Text") 101 | } 102 | v.Add("text", cfg.Text) 103 | if cfg.DisableWebPagePreview { 104 | v.Add( 105 | "disable_web_page_preview", 106 | strconv.FormatBool(cfg.DisableWebPagePreview), 107 | ) 108 | } 109 | if cfg.ParseMode != "" { 110 | v.Add("parse_mode", cfg.ParseMode) 111 | } 112 | 113 | return v, nil 114 | } 115 | 116 | // LocationCfg contains information about a SendLocation request. 117 | type LocationCfg struct { 118 | BaseMessage 119 | Location 120 | } 121 | 122 | // Values returns a url.Values representation of LocationCfg. 123 | func (cfg LocationCfg) Values() (url.Values, error) { 124 | v, err := cfg.BaseMessage.Values() 125 | if err != nil { 126 | return nil, err 127 | } 128 | updateValues(v, cfg.Location.Values()) 129 | 130 | return v, nil 131 | } 132 | 133 | // Name method returns Telegram API method name for sending Location. 134 | func (cfg LocationCfg) Name() string { 135 | return sendLocationMethod 136 | } 137 | 138 | // ContactCfg contains information about a SendContact request. 139 | // Use it to send information about a venue 140 | // Implements Messenger interface. 141 | type ContactCfg struct { 142 | BaseMessage 143 | Contact 144 | } 145 | 146 | // Name returns method name 147 | func (cfg ContactCfg) Name() string { 148 | return sendContactMethod 149 | } 150 | 151 | // Values returns a url.Values representation of ContactCfg. 152 | // Returns RequiredError if Text is empty. 153 | func (cfg ContactCfg) Values() (url.Values, error) { 154 | v, err := cfg.BaseMessage.Values() 155 | if err != nil { 156 | return nil, err 157 | } 158 | missed := []string{} 159 | if cfg.Contact.FirstName == "" { 160 | missed = append(missed, "FirstName") 161 | } 162 | if cfg.Contact.PhoneNumber == "" { 163 | missed = append(missed, "PhoneNumber") 164 | } 165 | if len(missed) > 0 { 166 | return nil, NewRequiredError(missed...) 167 | } 168 | updateValues(v, cfg.Contact.Values()) 169 | return v, nil 170 | } 171 | 172 | // VenueCfg contains information about a SendVenue request. 173 | // Use it to send information about a venue 174 | // Implements Messenger interface. 175 | type VenueCfg struct { 176 | BaseMessage 177 | Venue 178 | } 179 | 180 | // Name returns method name 181 | func (cfg VenueCfg) Name() string { 182 | return sendVenueMethod 183 | } 184 | 185 | // Values returns a url.Values representation of VenueCfg. 186 | // Returns RequiredError if Text is empty. 187 | func (cfg VenueCfg) Values() (url.Values, error) { 188 | v, err := cfg.BaseMessage.Values() 189 | if err != nil { 190 | return nil, err 191 | } 192 | missed := []string{} 193 | if cfg.Venue.Title == "" { 194 | missed = append(missed, "Title") 195 | } 196 | if cfg.Venue.Address == "" { 197 | missed = append(missed, "Address") 198 | } 199 | if len(missed) > 0 { 200 | return nil, NewRequiredError(missed...) 201 | } 202 | updateValues(v, cfg.Venue.Values()) 203 | return v, nil 204 | } 205 | 206 | // ForwardMessageCfg contains information about a ForwardMessage request. 207 | // Use it to forward messages of any kind 208 | // Implements Messenger interface. 209 | type ForwardMessageCfg struct { 210 | BaseChat 211 | // Unique identifier for the chat where the original message was sent 212 | FromChat BaseChat 213 | // Unique message identifier 214 | MessageID int64 215 | // Sends the message silently. 216 | // iOS users will not receive a notification, 217 | // Android users will receive a notification with no sound. 218 | // Other apps coming soon. 219 | DisableNotification bool 220 | } 221 | 222 | // Message returns instance of *Message type. 223 | func (ForwardMessageCfg) Message() *Message { 224 | return &Message{} 225 | } 226 | 227 | // Name returns method name 228 | func (cfg ForwardMessageCfg) Name() string { 229 | return forwardMessageMethod 230 | } 231 | 232 | // Values returns a url.Values representation of MessageCfg. 233 | // Returns RequiredError if Text is empty. 234 | func (cfg ForwardMessageCfg) Values() (url.Values, error) { 235 | v, err := cfg.BaseChat.Values() 236 | if err != nil { 237 | return nil, err 238 | } 239 | from, err := cfg.FromChat.Values() 240 | if err != nil { 241 | return nil, err 242 | } 243 | updateValuesWithPrefix(v, from, "from_") 244 | if cfg.MessageID == 0 { 245 | return nil, NewRequiredError("MessageID") 246 | } 247 | 248 | v.Add("message_id", strconv.FormatInt(cfg.MessageID, 10)) 249 | if cfg.DisableNotification { 250 | v.Add( 251 | "disable_notification", 252 | strconv.FormatBool(cfg.DisableNotification), 253 | ) 254 | } 255 | 256 | return v, nil 257 | } 258 | 259 | type localFile struct { 260 | filename string 261 | reader io.Reader 262 | } 263 | 264 | func (l localFile) Reader() io.Reader { 265 | return l.reader 266 | } 267 | 268 | func (l localFile) Name() string { 269 | return l.filename 270 | } 271 | 272 | // NewInputFile takes Reader object and returns InputFile. 273 | func NewInputFile(filename string, r io.Reader) InputFile { 274 | return localFile{filename, r} 275 | } 276 | 277 | // NewBytesFile takes byte slice and returns InputFile. 278 | func NewBytesFile(filename string, data []byte) InputFile { 279 | return NewInputFile(filename, bytes.NewBuffer(data)) 280 | } 281 | 282 | // BaseFile describes file settings. It's an abstract type. 283 | type BaseFile struct { 284 | BaseMessage 285 | FileID string 286 | MimeType string 287 | InputFile InputFile 288 | } 289 | 290 | // GetFileID returns fileID if it's exist 291 | func (b BaseFile) GetFileID() string { 292 | return b.FileID 293 | } 294 | 295 | // Exist returns true if file exists on telegram servers 296 | func (b BaseFile) Exist() bool { 297 | return b.FileID != "" 298 | } 299 | 300 | // File returns InputFile object that are used to create request 301 | func (b BaseFile) File() InputFile { 302 | return b.InputFile 303 | } 304 | 305 | // Values returns a url.Values representation of BaseFile. 306 | func (b BaseFile) Values() (url.Values, error) { 307 | v, err := b.BaseMessage.Values() 308 | if err != nil { 309 | return nil, err 310 | } 311 | if b.MimeType != "" { 312 | v.Add("mime_type", b.MimeType) 313 | } 314 | return v, nil 315 | } 316 | 317 | // Reset method removes FileID and sets new InputFile 318 | func (b *BaseFile) Reset(i InputFile) { 319 | b.FileID = "" 320 | b.InputFile = i 321 | } 322 | 323 | // PhotoCfg contains information about a SendPhoto request. 324 | // Use it to send information about a venue 325 | // Implements Filer and Messenger interfaces. 326 | type PhotoCfg struct { 327 | BaseFile 328 | Caption string 329 | } 330 | 331 | // Name returns method name 332 | func (cfg PhotoCfg) Name() string { 333 | return sendPhotoMethod 334 | } 335 | 336 | // Values returns a url.Values representation of PhotoCfg. 337 | func (cfg PhotoCfg) Values() (url.Values, error) { 338 | v, err := cfg.BaseFile.Values() 339 | if err != nil { 340 | return nil, err 341 | } 342 | if cfg.BaseFile.FileID != "" { 343 | v.Add(cfg.Field(), cfg.BaseFile.FileID) 344 | } 345 | if cfg.Caption != "" { 346 | v.Add("caption", cfg.Caption) 347 | } 348 | return v, nil 349 | } 350 | 351 | // Field returns name for photo file data 352 | func (cfg PhotoCfg) Field() string { 353 | return photoField 354 | } 355 | 356 | // AudioCfg contains information about a SendAudio request. 357 | // Use it to send information about an audio 358 | // Implements Filer and Messenger interfaces. 359 | type AudioCfg struct { 360 | BaseFile 361 | Duration int 362 | Performer string 363 | Title string 364 | } 365 | 366 | // Name returns method name 367 | func (cfg AudioCfg) Name() string { 368 | return sendAudioMethod 369 | } 370 | 371 | // Values returns a url.Values representation of AudioCfg. 372 | func (cfg AudioCfg) Values() (url.Values, error) { 373 | v, err := cfg.BaseFile.Values() 374 | if err != nil { 375 | return nil, err 376 | } 377 | if cfg.BaseFile.FileID != "" { 378 | v.Add(cfg.Field(), cfg.BaseFile.FileID) 379 | } 380 | if cfg.Duration != 0 { 381 | v.Add("duration", strconv.Itoa(cfg.Duration)) 382 | } 383 | 384 | if cfg.Performer != "" { 385 | v.Add("performer", cfg.Performer) 386 | } 387 | if cfg.Title != "" { 388 | v.Add("title", cfg.Title) 389 | } 390 | return v, nil 391 | } 392 | 393 | // Field returns name for audio file data 394 | func (cfg AudioCfg) Field() string { 395 | return audioField 396 | } 397 | 398 | // VideoCfg contains information about a SendVideo request. 399 | // Use it to send information about a video 400 | // Implements Filer and Messenger interfaces. 401 | type VideoCfg struct { 402 | BaseFile 403 | Duration int 404 | Caption string 405 | } 406 | 407 | // Name returns method name 408 | func (cfg VideoCfg) Name() string { 409 | return sendVideoMethod 410 | } 411 | 412 | // Values returns a url.Values representation of VideoCfg. 413 | func (cfg VideoCfg) Values() (url.Values, error) { 414 | v, err := cfg.BaseFile.Values() 415 | if err != nil { 416 | return nil, err 417 | } 418 | if cfg.BaseFile.FileID != "" { 419 | v.Add(cfg.Field(), cfg.BaseFile.FileID) 420 | } 421 | if cfg.Duration != 0 { 422 | v.Add("duration", strconv.Itoa(cfg.Duration)) 423 | } 424 | 425 | if cfg.Caption != "" { 426 | v.Add("caption", cfg.Caption) 427 | } 428 | return v, nil 429 | } 430 | 431 | // Field returns name for video file data 432 | func (cfg VideoCfg) Field() string { 433 | return videoField 434 | } 435 | 436 | // VoiceCfg contains information about a SendVoice request. 437 | // Use it to send information about a venue 438 | // Implements Filer and Messenger interfaces. 439 | type VoiceCfg struct { 440 | BaseFile 441 | Duration int 442 | } 443 | 444 | // Name returns method name 445 | func (cfg VoiceCfg) Name() string { 446 | return sendVoiceMethod 447 | } 448 | 449 | // Values returns a url.Values representation of VoiceCfg. 450 | func (cfg VoiceCfg) Values() (url.Values, error) { 451 | v, err := cfg.BaseFile.Values() 452 | if err != nil { 453 | return nil, err 454 | } 455 | if cfg.BaseFile.FileID != "" { 456 | v.Add(cfg.Field(), cfg.BaseFile.FileID) 457 | } 458 | if cfg.BaseFile.FileID != "" { 459 | v.Add(cfg.Field(), cfg.BaseFile.FileID) 460 | } 461 | if cfg.Duration != 0 { 462 | v.Add("duration", strconv.Itoa(cfg.Duration)) 463 | } 464 | 465 | return v, nil 466 | } 467 | 468 | // Field returns name for voice file data 469 | func (cfg VoiceCfg) Field() string { 470 | return voiceField 471 | } 472 | 473 | // DocumentCfg contains information about a SendDocument request. 474 | // Use it to send information about a documents 475 | // Implements Filer and Messenger interfaces. 476 | type DocumentCfg struct { 477 | BaseFile 478 | } 479 | 480 | // Name returns method name 481 | func (cfg DocumentCfg) Name() string { 482 | return sendDocumentMethod 483 | } 484 | 485 | // Values returns a url.Values representation of DocumentCfg. 486 | func (cfg DocumentCfg) Values() (url.Values, error) { 487 | v, err := cfg.BaseFile.Values() 488 | if err != nil { 489 | return nil, err 490 | } 491 | if cfg.BaseFile.FileID != "" { 492 | v.Add(cfg.Field(), cfg.BaseFile.FileID) 493 | } 494 | 495 | return v, nil 496 | } 497 | 498 | // Field returns name for document file data 499 | func (cfg DocumentCfg) Field() string { 500 | return documentField 501 | } 502 | 503 | // StickerCfg contains information about a SendSticker request. 504 | // Implements Filer and Messenger interfaces. 505 | type StickerCfg struct { 506 | BaseFile 507 | } 508 | 509 | // Values returns a url.Values representation of StickerCfg. 510 | func (cfg StickerCfg) Values() (url.Values, error) { 511 | v, err := cfg.BaseFile.Values() 512 | if err != nil { 513 | return nil, err 514 | } 515 | if cfg.BaseFile.FileID != "" { 516 | v.Add(cfg.Field(), cfg.BaseFile.FileID) 517 | } 518 | 519 | return v, nil 520 | } 521 | 522 | // Name method returns Telegram API method name for sending Sticker. 523 | func (cfg StickerCfg) Name() string { 524 | return sendStickerMethod 525 | } 526 | 527 | // Field returns name for sticker file data 528 | func (cfg StickerCfg) Field() string { 529 | return stickerField 530 | } 531 | -------------------------------------------------------------------------------- /configs_message_test.go: -------------------------------------------------------------------------------- 1 | package telegram_test 2 | 3 | import ( 4 | "encoding/json" 5 | "net/url" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/bot-api/telegram" 10 | "gopkg.in/stretchr/testify.v1/assert" 11 | ) 12 | 13 | func TestBaseChat_Values(t *testing.T) { 14 | testTable := []cfgTT{ 15 | { 16 | exp: url.Values{ 17 | "chat_id": {"100"}, 18 | }, 19 | cfg: telegram.BaseChat{ 20 | ID: 100, 21 | }, 22 | }, 23 | { 24 | exp: nil, 25 | expErr: telegram.NewRequiredError( 26 | "ID", "ChannelUsername", 27 | ), 28 | cfg: telegram.BaseChat{}, 29 | }, 30 | { 31 | exp: url.Values{ 32 | "chat_id": {"username"}, 33 | }, 34 | cfg: telegram.BaseChat{ 35 | ID: 10, 36 | ChannelUsername: "username", 37 | }, 38 | }, 39 | } 40 | for i, tt := range testTable { 41 | t.Logf("test #%d", i) 42 | values, err := tt.cfg.Values() 43 | assert.Equal(t, tt.expErr, err) 44 | assert.Equal(t, tt.exp, values) 45 | } 46 | } 47 | 48 | func TestBaseMessage_Values(t *testing.T) { 49 | testTable := []cfgTT{ 50 | { 51 | exp: url.Values{ 52 | "chat_id": {"10"}, 53 | }, 54 | cfg: telegram.BaseMessage{ 55 | BaseChat: telegram.BaseChat{ID: 10}, 56 | }, 57 | }, 58 | { 59 | exp: url.Values{ 60 | "chat_id": {"100"}, 61 | "reply_to_message_id": {"100"}, 62 | "disable_notification": {"true"}, 63 | "reply_markup": { 64 | "{\"keyboard\":[[{\"text\":\"1\"}" + 65 | ",{\"text\":\"2\"}]]}", 66 | }, 67 | }, 68 | cfg: telegram.BaseMessage{ 69 | BaseChat: telegram.BaseChat{ 70 | ID: 100, 71 | }, 72 | ReplyToMessageID: 100, 73 | DisableNotification: true, 74 | ReplyMarkup: &telegram.ReplyKeyboardMarkup{ 75 | Keyboard: [][]telegram.KeyboardButton{ 76 | { 77 | { 78 | Text: "1", 79 | }, 80 | { 81 | Text: "2", 82 | }, 83 | }, 84 | }, 85 | }, 86 | }, 87 | }, 88 | { 89 | exp: nil, 90 | cfg: telegram.BaseMessage{ 91 | BaseChat: telegram.BaseChat{ 92 | ID: 100, 93 | }, 94 | ReplyMarkup: replyBadMarkup{}, 95 | }, 96 | expErr: &json.MarshalerError{ 97 | Type: reflect.TypeOf(replyBadMarkup{}), 98 | Err: marshalError, 99 | }, 100 | }, 101 | { 102 | exp: nil, 103 | cfg: telegram.BaseMessage{}, 104 | expErr: telegram.NewRequiredError( 105 | "ID", "ChannelUsername", 106 | ), 107 | }, 108 | } 109 | for i, tt := range testTable { 110 | t.Logf("test #%d", i) 111 | values, err := tt.cfg.Values() 112 | assert.Equal(t, tt.expErr, err) 113 | assert.Equal(t, tt.exp, values) 114 | } 115 | } 116 | 117 | func TestBaseMessage_Message(t *testing.T) { 118 | m := telegram.BaseMessage{} 119 | msg := m.Message() 120 | assert.NotNil(t, msg, "Message shouln't be nil") 121 | } 122 | 123 | func TestMessageCfg_Name(t *testing.T) { 124 | name := "sendMessage" 125 | c := telegram.MessageCfg{} 126 | assert.Equal(t, name, c.Name()) 127 | } 128 | 129 | func TestMessageCfg_Values(t *testing.T) { 130 | testTable := []cfgTT{ 131 | { 132 | exp: url.Values{ 133 | "chat_id": {"10"}, 134 | "text": {"text"}, 135 | }, 136 | cfg: telegram.MessageCfg{ 137 | BaseMessage: telegram.BaseMessage{ 138 | BaseChat: telegram.BaseChat{ID: 10}, 139 | }, 140 | Text: "text", 141 | }, 142 | }, 143 | { 144 | exp: url.Values{ 145 | "chat_id": {"10"}, 146 | "text": {"text2"}, 147 | "disable_web_page_preview": {"true"}, 148 | "parse_mode": {"HTML"}, 149 | }, 150 | cfg: telegram.MessageCfg{ 151 | BaseMessage: telegram.BaseMessage{ 152 | BaseChat: telegram.BaseChat{ID: 10}, 153 | }, 154 | Text: "text2", 155 | DisableWebPagePreview: true, 156 | ParseMode: telegram.HTMLMode, 157 | }, 158 | }, 159 | { 160 | exp: nil, 161 | expErr: telegram.NewRequiredError( 162 | "ID", "ChannelUsername", 163 | ), 164 | cfg: telegram.MessageCfg{}, 165 | }, 166 | { 167 | exp: nil, 168 | expErr: telegram.NewRequiredError( 169 | "Text", 170 | ), 171 | cfg: telegram.MessageCfg{ 172 | BaseMessage: telegram.BaseMessage{ 173 | BaseChat: telegram.BaseChat{ID: 10}, 174 | }, 175 | }, 176 | }, 177 | } 178 | for i, tt := range testTable { 179 | t.Logf("test #%d", i) 180 | values, err := tt.cfg.Values() 181 | assert.Equal(t, tt.expErr, err) 182 | assert.Equal(t, tt.exp, values) 183 | } 184 | } 185 | 186 | func TestForwardMessageCfg_Name(t *testing.T) { 187 | name := "forwardMessage" 188 | c := telegram.ForwardMessageCfg{} 189 | assert.Equal(t, name, c.Name()) 190 | } 191 | 192 | func TestForwardMessageCfg_Message(t *testing.T) { 193 | c := telegram.ForwardMessageCfg{} 194 | assert.Equal(t, c.Message(), &telegram.Message{}) 195 | } 196 | 197 | func TestForwardMessageCfg_Values(t *testing.T) { 198 | testTable := []cfgTT{ 199 | { 200 | exp: url.Values{ 201 | "chat_id": {"10"}, 202 | "from_chat_id": {"20"}, 203 | "message_id": {"30"}, 204 | }, 205 | cfg: telegram.ForwardMessageCfg{ 206 | BaseChat: telegram.BaseChat{ID: 10}, 207 | FromChat: telegram.BaseChat{ID: 20}, 208 | MessageID: 30, 209 | }, 210 | }, 211 | { 212 | exp: url.Values{ 213 | "chat_id": {"10"}, 214 | "from_chat_id": {"20"}, 215 | "message_id": {"30"}, 216 | "disable_notification": {"true"}, 217 | }, 218 | cfg: telegram.ForwardMessageCfg{ 219 | BaseChat: telegram.BaseChat{ID: 10}, 220 | FromChat: telegram.BaseChat{ID: 20}, 221 | MessageID: 30, 222 | DisableNotification: true, 223 | }, 224 | }, 225 | { 226 | exp: nil, 227 | expErr: telegram.NewRequiredError( 228 | "MessageID", 229 | ), 230 | cfg: telegram.ForwardMessageCfg{ 231 | BaseChat: telegram.BaseChat{ID: 10}, 232 | FromChat: telegram.BaseChat{ID: 20}, 233 | }, 234 | }, 235 | { 236 | exp: nil, 237 | expErr: telegram.NewRequiredError( 238 | "ID", "ChannelUsername", 239 | ), 240 | cfg: telegram.ForwardMessageCfg{}, 241 | }, 242 | { 243 | exp: nil, 244 | expErr: telegram.NewRequiredError( 245 | "ID", "ChannelUsername", 246 | ), 247 | cfg: telegram.ForwardMessageCfg{ 248 | BaseChat: telegram.BaseChat{ID: 10}, 249 | }, 250 | }, 251 | } 252 | for i, tt := range testTable { 253 | t.Logf("test #%d", i) 254 | values, err := tt.cfg.Values() 255 | assert.Equal(t, tt.expErr, err) 256 | assert.Equal(t, tt.exp, values) 257 | } 258 | } 259 | 260 | func TestLocationCfg_Name(t *testing.T) { 261 | name := "sendLocation" 262 | c := telegram.LocationCfg{} 263 | assert.Equal(t, name, c.Name()) 264 | } 265 | 266 | func TestLocationCfg_Values(t *testing.T) { 267 | testTable := []cfgTT{ 268 | { 269 | exp: url.Values{ 270 | "chat_id": {"10"}, 271 | "longitude": {"20"}, 272 | "latitude": {"30"}, 273 | }, 274 | cfg: telegram.LocationCfg{ 275 | BaseMessage: telegram.BaseMessage{ 276 | BaseChat: telegram.BaseChat{ID: 10}, 277 | }, 278 | Location: telegram.Location{ 279 | Longitude: 20, 280 | Latitude: 30, 281 | }, 282 | }, 283 | }, 284 | { 285 | exp: url.Values{ 286 | "chat_id": {"10"}, 287 | "longitude": {"0"}, 288 | "latitude": {"0"}, 289 | "reply_to_message_id": {"20"}, 290 | "disable_notification": {"true"}, 291 | }, 292 | cfg: telegram.LocationCfg{ 293 | BaseMessage: telegram.BaseMessage{ 294 | BaseChat: telegram.BaseChat{ID: 10}, 295 | ReplyToMessageID: 20, 296 | DisableNotification: true, 297 | }, 298 | }, 299 | }, 300 | { 301 | exp: nil, 302 | expErr: telegram.NewRequiredError( 303 | "ID", "ChannelUsername", 304 | ), 305 | cfg: telegram.LocationCfg{}, 306 | }, 307 | } 308 | for i, tt := range testTable { 309 | t.Logf("test #%d", i) 310 | values, err := tt.cfg.Values() 311 | assert.Equal(t, tt.expErr, err) 312 | assert.Equal(t, tt.exp, values) 313 | } 314 | } 315 | 316 | func TestContactCfg_Name(t *testing.T) { 317 | name := "sendContact" 318 | c := telegram.ContactCfg{} 319 | assert.Equal(t, name, c.Name()) 320 | } 321 | 322 | func TestContactCfg_Values(t *testing.T) { 323 | testTable := []cfgTT{ 324 | { 325 | exp: url.Values{ 326 | "chat_id": {"10"}, 327 | "user_id": {"30"}, 328 | "phone_number": {"phone_number_value"}, 329 | "first_name": {"first_name_value"}, 330 | "last_name": {"last_name_value"}, 331 | }, 332 | cfg: telegram.ContactCfg{ 333 | BaseMessage: telegram.BaseMessage{ 334 | BaseChat: telegram.BaseChat{ID: 10}, 335 | }, 336 | Contact: telegram.Contact{ 337 | FirstName: "first_name_value", 338 | LastName: "last_name_value", 339 | PhoneNumber: "phone_number_value", 340 | UserID: 30, 341 | }, 342 | }, 343 | }, 344 | { 345 | exp: nil, 346 | expErr: telegram.NewRequiredError( 347 | "FirstName", "PhoneNumber", 348 | ), 349 | cfg: telegram.ContactCfg{ 350 | BaseMessage: telegram.BaseMessage{ 351 | BaseChat: telegram.BaseChat{ID: 10}, 352 | }, 353 | }, 354 | }, 355 | { 356 | exp: nil, 357 | expErr: telegram.NewRequiredError( 358 | "ID", "ChannelUsername", 359 | ), 360 | cfg: telegram.ContactCfg{}, 361 | }, 362 | } 363 | for i, tt := range testTable { 364 | t.Logf("test #%d", i) 365 | values, err := tt.cfg.Values() 366 | assert.Equal(t, tt.expErr, err) 367 | assert.Equal(t, tt.exp, values) 368 | } 369 | } 370 | 371 | func TestVenueCfg_Name(t *testing.T) { 372 | name := "sendVenue" 373 | c := telegram.VenueCfg{} 374 | assert.Equal(t, name, c.Name()) 375 | } 376 | 377 | func TestVenueCfg_Values(t *testing.T) { 378 | testTable := []cfgTT{ 379 | { 380 | exp: url.Values{ 381 | "chat_id": {"10"}, 382 | "longitude": {"20"}, 383 | "latitude": {"30"}, 384 | "title": {"venue title"}, 385 | "address": {"venue address"}, 386 | "foursquare_id": {"foursquare-id"}, 387 | }, 388 | cfg: telegram.VenueCfg{ 389 | BaseMessage: telegram.BaseMessage{ 390 | BaseChat: telegram.BaseChat{ID: 10}, 391 | }, 392 | Venue: telegram.Venue{ 393 | Location: telegram.Location{ 394 | Longitude: 20, 395 | Latitude: 30, 396 | }, 397 | Title: "venue title", 398 | Address: "venue address", 399 | FoursquareID: "foursquare-id", 400 | }, 401 | }, 402 | }, 403 | { 404 | exp: nil, 405 | expErr: telegram.NewRequiredError( 406 | "Title", "Address", 407 | ), 408 | cfg: telegram.VenueCfg{ 409 | BaseMessage: telegram.BaseMessage{ 410 | BaseChat: telegram.BaseChat{ID: 10}, 411 | }, 412 | }, 413 | }, 414 | { 415 | exp: nil, 416 | expErr: telegram.NewRequiredError( 417 | "ID", "ChannelUsername", 418 | ), 419 | cfg: telegram.VenueCfg{}, 420 | }, 421 | } 422 | for i, tt := range testTable { 423 | t.Logf("test #%d", i) 424 | values, err := tt.cfg.Values() 425 | assert.Equal(t, tt.expErr, err) 426 | assert.Equal(t, tt.exp, values) 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /configs_test.go: -------------------------------------------------------------------------------- 1 | // telegram_test package tests only public interface 2 | package telegram_test 3 | 4 | import ( 5 | "encoding/json" 6 | "net/url" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/bot-api/telegram" 11 | "gopkg.in/stretchr/testify.v1/assert" 12 | ) 13 | 14 | func TestMeCfg(t *testing.T) { 15 | name := "getMe" 16 | c := telegram.MeCfg{} 17 | assert.Equal(t, name, c.Name(), "method Name() has wrong value") 18 | values, err := c.Values() 19 | assert.Nil(t, values) 20 | assert.NoError(t, err) 21 | } 22 | 23 | func TestUpdateCfg_Name(t *testing.T) { 24 | name := "getUpdates" 25 | c := telegram.UpdateCfg{} 26 | assert.Equal(t, name, c.Name(), "method Name() has wrong value") 27 | } 28 | 29 | func TestUpdateCfg_Values(t *testing.T) { 30 | testTable := []cfgTT{ 31 | { 32 | exp: url.Values{ 33 | "offset": {"100"}, 34 | "limit": {"10"}, 35 | "timeout": {"30"}, 36 | }, 37 | cfg: telegram.UpdateCfg{ 38 | Offset: 100, 39 | Limit: 10, 40 | Timeout: 30, 41 | }, 42 | }, 43 | { 44 | exp: url.Values{}, 45 | cfg: telegram.UpdateCfg{}, 46 | }, 47 | { 48 | exp: nil, 49 | expErr: telegram.NewValidationError( 50 | "Limit", 51 | "should be between 1 and 100", 52 | ), 53 | cfg: telegram.UpdateCfg{ 54 | Limit: -10, 55 | }, 56 | }, 57 | } 58 | for i, tt := range testTable { 59 | t.Logf("test #%d", i) 60 | values, err := tt.cfg.Values() 61 | assert.Equal(t, tt.expErr, err) 62 | assert.Equal(t, tt.exp, values) 63 | } 64 | } 65 | 66 | func TestChatAction_Name(t *testing.T) { 67 | name := "sendChatAction" 68 | c := telegram.ChatActionCfg{} 69 | if c.Name() != name { 70 | t.Errorf("Expected Name() to be %s, actual %s", name, c.Name()) 71 | } 72 | } 73 | 74 | func TestChatActionCfg_Values(t *testing.T) { 75 | testTable := []cfgTT{ 76 | { 77 | exp: url.Values{ 78 | "chat_id": {"10"}, 79 | "action": {"typing"}, 80 | }, 81 | cfg: telegram.ChatActionCfg{ 82 | BaseChat: telegram.BaseChat{ID: 10}, 83 | Action: telegram.ActionTyping, 84 | }, 85 | }, 86 | { 87 | exp: nil, 88 | expErr: telegram.NewRequiredError( 89 | "Action", 90 | ), 91 | cfg: telegram.ChatActionCfg{ 92 | BaseChat: telegram.BaseChat{ID: 10}, 93 | }, 94 | }, 95 | { 96 | exp: nil, 97 | cfg: telegram.ChatActionCfg{}, 98 | expErr: telegram.NewRequiredError( 99 | "ID", "ChannelUsername", 100 | ), 101 | }, 102 | } 103 | for i, tt := range testTable { 104 | t.Logf("test #%d", i) 105 | values, err := tt.cfg.Values() 106 | assert.Equal(t, tt.expErr, err) 107 | assert.Equal(t, tt.exp, values) 108 | } 109 | } 110 | 111 | func TestUserProfilePhotosCfg_Name(t *testing.T) { 112 | name := "getUserProfilePhotos" 113 | c := telegram.UserProfilePhotosCfg{} 114 | if c.Name() != name { 115 | t.Errorf("Expected Name() to be %s, actual %s", name, c.Name()) 116 | } 117 | } 118 | 119 | func TestUserProfilePhotosCfg_Values(t *testing.T) { 120 | testTable := []cfgTT{ 121 | { 122 | exp: url.Values{ 123 | "user_id": {"10"}, 124 | }, 125 | cfg: telegram.UserProfilePhotosCfg{ 126 | UserID: 10, 127 | }, 128 | }, 129 | { 130 | exp: url.Values{ 131 | "user_id": {"10"}, 132 | "offset": {"100"}, 133 | "limit": {"5"}, 134 | }, 135 | cfg: telegram.UserProfilePhotosCfg{ 136 | UserID: 10, 137 | Offset: 100, 138 | Limit: 5, 139 | }, 140 | }, 141 | { 142 | expErr: telegram.NewValidationError( 143 | "Limit", 144 | "should be between 1 and 100", 145 | ), 146 | cfg: telegram.UserProfilePhotosCfg{ 147 | UserID: 10, 148 | Limit: 1000, 149 | }, 150 | }, 151 | { 152 | cfg: telegram.UserProfilePhotosCfg{}, 153 | expErr: telegram.NewRequiredError( 154 | "UserID", 155 | ), 156 | }, 157 | } 158 | for i, tt := range testTable { 159 | t.Logf("test #%d", i) 160 | values, err := tt.cfg.Values() 161 | assert.Equal(t, tt.expErr, err) 162 | assert.Equal(t, tt.exp, values) 163 | } 164 | } 165 | 166 | func TestAnswerInlineQueryCfg_Name(t *testing.T) { 167 | name := "answerInlineQuery" 168 | c := telegram.AnswerInlineQueryCfg{} 169 | if c.Name() != name { 170 | t.Errorf("Expected Name() to be %s, actual %s", name, c.Name()) 171 | } 172 | } 173 | 174 | func TestAnswerInlineQueryCfg_Values(t *testing.T) { 175 | results := []telegram.InlineQueryResult{ 176 | &telegram.InlineQueryResultArticle{ 177 | BaseInlineQueryResult: telegram.BaseInlineQueryResult{ 178 | ID: "id", 179 | Type: "type", 180 | }, 181 | Title: "title", 182 | }, 183 | } 184 | resultsEncoded := "[{\"type\":\"type\",\"id\":\"id\",\"title\":\"title\"}]" 185 | testTable := []cfgTT{ 186 | { 187 | exp: url.Values{ 188 | "results": {resultsEncoded}, 189 | "inline_query_id": {"10"}, 190 | }, 191 | cfg: telegram.AnswerInlineQueryCfg{ 192 | InlineQueryID: "10", 193 | Results: results, 194 | }, 195 | }, 196 | { 197 | exp: url.Values{ 198 | "results": {resultsEncoded}, 199 | "inline_query_id": {"10"}, 200 | "cache_time": {"60"}, 201 | "is_personal": {"true"}, 202 | "next_offset": {"offset"}, 203 | "switch_pm_text": {"switch_pm_text"}, 204 | "switch_pm_parameter": {"switch_pm_parameter"}, 205 | }, 206 | cfg: telegram.AnswerInlineQueryCfg{ 207 | InlineQueryID: "10", 208 | Results: results, 209 | CacheTime: 60, 210 | IsPersonal: true, 211 | NextOffset: "offset", 212 | SwitchPMText: "switch_pm_text", 213 | SwitchPMParameter: "switch_pm_parameter", 214 | }, 215 | }, 216 | { 217 | cfg: telegram.AnswerInlineQueryCfg{ 218 | InlineQueryID: "10", 219 | }, 220 | expErr: telegram.NewRequiredError( 221 | "Results", 222 | ), 223 | }, 224 | { 225 | cfg: telegram.AnswerInlineQueryCfg{ 226 | InlineQueryID: "10", 227 | Results: []telegram.InlineQueryResult{}, 228 | }, 229 | expErr: telegram.NewRequiredError( 230 | "Results", 231 | ), 232 | }, 233 | { 234 | cfg: telegram.AnswerInlineQueryCfg{ 235 | InlineQueryID: "10", 236 | Results: []telegram.InlineQueryResult{ 237 | badInlineQueryResult{}, 238 | }, 239 | }, 240 | expErr: &json.MarshalerError{ 241 | Type: reflect.TypeOf(badInlineQueryResult{}), 242 | Err: marshalError, 243 | }, 244 | }, 245 | } 246 | for i, tt := range testTable { 247 | t.Logf("test #%d", i) 248 | values, err := tt.cfg.Values() 249 | assert.Equal(t, tt.expErr, err) 250 | assert.Equal(t, tt.exp, values) 251 | } 252 | } 253 | 254 | func TestGetChat_Name(t *testing.T) { 255 | name := "getChat" 256 | c := telegram.GetChatCfg{} 257 | if c.Name() != name { 258 | t.Errorf("Expected Name() to be %s, actual %s", name, c.Name()) 259 | } 260 | } 261 | 262 | func TestGetChatCfg_Values(t *testing.T) { 263 | testTable := []cfgTT{ 264 | { 265 | exp: url.Values{ 266 | "chat_id": {"10"}, 267 | }, 268 | cfg: telegram.GetChatCfg{ 269 | BaseChat: telegram.BaseChat{ID: 10}, 270 | }, 271 | }, 272 | { 273 | exp: nil, 274 | cfg: telegram.GetChatCfg{}, 275 | expErr: telegram.NewRequiredError( 276 | "ID", "ChannelUsername", 277 | ), 278 | }, 279 | } 280 | for i, tt := range testTable { 281 | t.Logf("test #%d", i) 282 | values, err := tt.cfg.Values() 283 | assert.Equal(t, tt.expErr, err) 284 | assert.Equal(t, tt.exp, values) 285 | } 286 | } 287 | 288 | func TestGetChatAdministrators_Name(t *testing.T) { 289 | name := "getChatAdministrators" 290 | c := telegram.GetChatAdministratorsCfg{} 291 | if c.Name() != name { 292 | t.Errorf("Expected Name() to be %s, actual %s", name, c.Name()) 293 | } 294 | } 295 | 296 | func TestGetChatAdministratorsCfg_Values(t *testing.T) { 297 | testTable := []cfgTT{ 298 | { 299 | exp: url.Values{ 300 | "chat_id": {"10"}, 301 | }, 302 | cfg: telegram.GetChatAdministratorsCfg{ 303 | BaseChat: telegram.BaseChat{ID: 10}, 304 | }, 305 | }, 306 | { 307 | exp: nil, 308 | cfg: telegram.GetChatAdministratorsCfg{}, 309 | expErr: telegram.NewRequiredError( 310 | "ID", "ChannelUsername", 311 | ), 312 | }, 313 | } 314 | for i, tt := range testTable { 315 | t.Logf("test #%d", i) 316 | values, err := tt.cfg.Values() 317 | assert.Equal(t, tt.expErr, err) 318 | assert.Equal(t, tt.exp, values) 319 | } 320 | } 321 | 322 | func TestGetChatMembersCount_Name(t *testing.T) { 323 | name := "getChatMembersCount" 324 | c := telegram.GetChatMembersCountCfg{} 325 | if c.Name() != name { 326 | t.Errorf("Expected Name() to be %s, actual %s", name, c.Name()) 327 | } 328 | } 329 | 330 | func TestGetChatMembersCountCfg_Values(t *testing.T) { 331 | testTable := []cfgTT{ 332 | { 333 | exp: url.Values{ 334 | "chat_id": {"10"}, 335 | }, 336 | cfg: telegram.GetChatMembersCountCfg{ 337 | BaseChat: telegram.BaseChat{ID: 10}, 338 | }, 339 | }, 340 | { 341 | exp: nil, 342 | cfg: telegram.GetChatMembersCountCfg{}, 343 | expErr: telegram.NewRequiredError( 344 | "ID", "ChannelUsername", 345 | ), 346 | }, 347 | } 348 | for i, tt := range testTable { 349 | t.Logf("test #%d", i) 350 | values, err := tt.cfg.Values() 351 | assert.Equal(t, tt.expErr, err) 352 | assert.Equal(t, tt.exp, values) 353 | } 354 | } 355 | 356 | func TestGetChatMember_Name(t *testing.T) { 357 | name := "getChatMember" 358 | c := telegram.GetChatMemberCfg{} 359 | if c.Name() != name { 360 | t.Errorf("Expected Name() to be %s, actual %s", name, c.Name()) 361 | } 362 | } 363 | 364 | func TestGetChatMemberCfg_Values(t *testing.T) { 365 | testTable := []cfgTT{ 366 | { 367 | exp: url.Values{ 368 | "chat_id": {"10"}, 369 | "user_id": {"11"}, 370 | }, 371 | cfg: telegram.GetChatMemberCfg{ 372 | BaseChat: telegram.BaseChat{ID: 10}, 373 | UserID: 11, 374 | }, 375 | }, 376 | { 377 | exp: nil, 378 | expErr: telegram.NewRequiredError( 379 | "UserID", 380 | ), 381 | cfg: telegram.GetChatMemberCfg{ 382 | BaseChat: telegram.BaseChat{ID: 10}, 383 | }, 384 | }, 385 | { 386 | exp: nil, 387 | cfg: telegram.GetChatMemberCfg{}, 388 | expErr: telegram.NewRequiredError( 389 | "ID", "ChannelUsername", 390 | ), 391 | }, 392 | } 393 | for i, tt := range testTable { 394 | t.Logf("test #%d", i) 395 | values, err := tt.cfg.Values() 396 | assert.Equal(t, tt.expErr, err) 397 | assert.Equal(t, tt.exp, values) 398 | } 399 | } 400 | 401 | func TestKickChatMember_Name(t *testing.T) { 402 | name := "kickChatMember" 403 | c := telegram.KickChatMemberCfg{} 404 | if c.Name() != name { 405 | t.Errorf("Expected Name() to be %s, actual %s", name, c.Name()) 406 | } 407 | } 408 | 409 | func TestKickChatMemberCfg_Values(t *testing.T) { 410 | testTable := []cfgTT{ 411 | { 412 | exp: url.Values{ 413 | "chat_id": {"10"}, 414 | "user_id": {"11"}, 415 | }, 416 | cfg: telegram.KickChatMemberCfg{ 417 | BaseChat: telegram.BaseChat{ID: 10}, 418 | UserID: 11, 419 | }, 420 | }, 421 | { 422 | exp: nil, 423 | expErr: telegram.NewRequiredError( 424 | "UserID", 425 | ), 426 | cfg: telegram.KickChatMemberCfg{ 427 | BaseChat: telegram.BaseChat{ID: 10}, 428 | }, 429 | }, 430 | { 431 | exp: nil, 432 | cfg: telegram.KickChatMemberCfg{}, 433 | expErr: telegram.NewRequiredError( 434 | "ID", "ChannelUsername", 435 | ), 436 | }, 437 | } 438 | for i, tt := range testTable { 439 | t.Logf("test #%d", i) 440 | values, err := tt.cfg.Values() 441 | assert.Equal(t, tt.expErr, err) 442 | assert.Equal(t, tt.exp, values) 443 | } 444 | } 445 | 446 | func TestUnbanChatMember_Name(t *testing.T) { 447 | name := "unbanChatMember" 448 | c := telegram.UnbanChatMemberCfg{} 449 | if c.Name() != name { 450 | t.Errorf("Expected Name() to be %s, actual %s", name, c.Name()) 451 | } 452 | } 453 | 454 | func TestUnbanChatMemberCfg_Values(t *testing.T) { 455 | testTable := []cfgTT{ 456 | { 457 | exp: url.Values{ 458 | "chat_id": {"10"}, 459 | "user_id": {"11"}, 460 | }, 461 | cfg: telegram.UnbanChatMemberCfg{ 462 | BaseChat: telegram.BaseChat{ID: 10}, 463 | UserID: 11, 464 | }, 465 | }, 466 | { 467 | exp: nil, 468 | expErr: telegram.NewRequiredError( 469 | "UserID", 470 | ), 471 | cfg: telegram.UnbanChatMemberCfg{ 472 | BaseChat: telegram.BaseChat{ID: 10}, 473 | }, 474 | }, 475 | { 476 | exp: nil, 477 | cfg: telegram.UnbanChatMemberCfg{}, 478 | expErr: telegram.NewRequiredError( 479 | "ID", "ChannelUsername", 480 | ), 481 | }, 482 | } 483 | for i, tt := range testTable { 484 | t.Logf("test #%d", i) 485 | values, err := tt.cfg.Values() 486 | assert.Equal(t, tt.expErr, err) 487 | assert.Equal(t, tt.exp, values) 488 | } 489 | } 490 | 491 | func TestLeaveChat_Name(t *testing.T) { 492 | name := "leaveChat" 493 | c := telegram.LeaveChatCfg{} 494 | if c.Name() != name { 495 | t.Errorf("Expected Name() to be %s, actual %s", name, c.Name()) 496 | } 497 | } 498 | 499 | func TestLeaveChatCfg_Values(t *testing.T) { 500 | testTable := []cfgTT{ 501 | { 502 | exp: url.Values{ 503 | "chat_id": {"10"}, 504 | }, 505 | cfg: telegram.LeaveChatCfg{ 506 | BaseChat: telegram.BaseChat{ID: 10}, 507 | }, 508 | }, 509 | { 510 | exp: nil, 511 | cfg: telegram.LeaveChatCfg{}, 512 | expErr: telegram.NewRequiredError( 513 | "ID", "ChannelUsername", 514 | ), 515 | }, 516 | } 517 | for i, tt := range testTable { 518 | t.Logf("test #%d", i) 519 | values, err := tt.cfg.Values() 520 | assert.Equal(t, tt.expErr, err) 521 | assert.Equal(t, tt.exp, values) 522 | } 523 | } 524 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | // Type of chat 4 | const ( 5 | PrivateChatType = "private" 6 | GroupChatType = "group" 7 | SuperGroupChatType = "supergroup" 8 | ChannelChatType = "channel" 9 | ) 10 | 11 | // Type of action to broadcast. 12 | // 13 | // Choose one, depending on what the user is about to receive: 14 | // typing for text messages 15 | // upload_photo for photos 16 | // record_video or upload_video for videos 17 | // record_audio or upload_audio for audio files 18 | // upload_document for general files 19 | // find_location for location data 20 | const ( 21 | ActionTyping = "typing" 22 | ActionUploadPhoto = "upload_photo" 23 | ActionRecordVideo = "record_video" 24 | ActionUploadVideo = "upload_video" 25 | ActionRecordAudio = "record_audio" 26 | ActionUploadAudio = "upload_audio" 27 | ActionUploadDocument = "upload_document" 28 | ActionFindLocation = "find_location" 29 | ) 30 | 31 | // internal constants for method names 32 | const ( 33 | getMeMethod = "getMe" 34 | getUpdatesMethod = "getUpdates" 35 | getUserProfilePhotosMethod = "getUserProfilePhotos" 36 | getChatMethod = "getChat" 37 | getChatAdministratorsMethod = "getChatAdministrators" 38 | getChatMembersCountMethod = "getChatMembersCount" 39 | getChatMemberMethod = "getChatMember" 40 | kickChatMemberMethod = "kickChatMember" 41 | unbanChatMemberMethod = "unbanChatMember" 42 | leaveChatMethod = "leaveChat" 43 | 44 | sendChatActionMethod = "sendChatAction" 45 | sendMessageMethod = "sendMessage" 46 | sendVenueMethod = "sendVenue" 47 | sendPhotoMethod = "sendPhoto" 48 | sendAudioMethod = "sendAudio" 49 | sendVideoMethod = "sendVideo" 50 | sendVoiceMethod = "sendVoice" 51 | sendDocumentMethod = "sendDocument" 52 | sendContactMethod = "sendContact" 53 | sendLocationMethod = "sendLocation" 54 | sendStickerMethod = "sendSticker" 55 | forwardMessageMethod = "forwardMessage" 56 | 57 | answerCallbackQueryMethod = "answerCallbackQuery" 58 | setWebhookMethod = "setWebhook" 59 | getFileMethod = "getFile" 60 | answerInlineQueryMethod = "answerInlineQuery" 61 | 62 | editMessageTextMethod = "editMessageText" 63 | editMessageCaptionMethod = "editMessageCaption" 64 | editMessageReplyMarkupMethod = "editMessageReplyMarkup" 65 | ) 66 | 67 | // constants for field names for file-like messages 68 | const ( 69 | photoField = "photo" 70 | documentField = "document" 71 | audioField = "audio" 72 | stickerField = "sticker" 73 | videoField = "video" 74 | voiceField = "voice" 75 | ) 76 | 77 | // Constant values for ParseMode in MessageCfg. 78 | const ( 79 | MarkdownMode = "Markdown" 80 | HTMLMode = "HTML" 81 | ) 82 | 83 | // EntityType constants helps to set type of entity in MessageEntity object 84 | const ( 85 | // @username 86 | MentionEntityType = "mention" 87 | HashTagEntityType = "hashtag" 88 | BotCommandEntityType = "bot_command" 89 | URLEntityType = "url" 90 | EmailEntityType = "email" 91 | BoldEntityType = "bold" // bold text 92 | ItalicEntityType = "italic" // italic text 93 | CodeEntityType = "code" // monowidth string 94 | PreEntityType = "pre" // monowidth block 95 | TextLinkEntityType = "text_link" // for clickable text URLs 96 | TextMentionEntityType = "text_mention" // for users without usernames 97 | ) 98 | 99 | // ChatMember possible statuses 100 | const ( 101 | MemberStatus = "member" 102 | CreatorMemberStatus = "creator" 103 | AdministratorMemberStatus = "administrator" 104 | LeftMemberStatus = "left" 105 | KickedMemberStatus = "kicked" 106 | ) 107 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | var errUnauthorized = fmt.Errorf("unauthorized") 9 | 10 | // IsUnauthorizedError checks if error is unauthorized 11 | func IsUnauthorizedError(err error) bool { 12 | return err == errUnauthorized 13 | } 14 | 15 | var errForbidden = fmt.Errorf("forbidden") 16 | 17 | // IsForbiddenError checks if error is forbidden 18 | func IsForbiddenError(err error) bool { 19 | return err == errForbidden 20 | } 21 | 22 | // IsAPIError checks if error is ApiError 23 | func IsAPIError(err error) bool { 24 | _, ok := err.(*APIError) 25 | return ok 26 | } 27 | 28 | // IsRequiredError checks if error is RequiredError 29 | func IsRequiredError(err error) bool { 30 | _, ok := err.(*RequiredError) 31 | return ok 32 | } 33 | 34 | // IsValidationError checks if error is ValidationError 35 | func IsValidationError(err error) bool { 36 | _, ok := err.(*ValidationError) 37 | return ok 38 | } 39 | 40 | // APIError contains error information from response 41 | type APIError struct { 42 | Description string `json:"description"` 43 | // ErrorCode contents are subject to change in the future. 44 | ErrorCode int `json:"error_code"` 45 | } 46 | 47 | // Error returns string representation for ApiError 48 | func (e *APIError) Error() string { 49 | return fmt.Sprintf("apiError: %s", e.Description) 50 | } 51 | 52 | // RequiredError tells if fields are required but were not filled 53 | type RequiredError struct { 54 | Fields []string 55 | } 56 | 57 | // Error returns string representation for RequiredError 58 | func (e *RequiredError) Error() string { 59 | return fmt.Sprintf("%s required", strings.Join(e.Fields, " or ")) 60 | } 61 | 62 | // NewRequiredError creates RequireError 63 | func NewRequiredError(fields ...string) *RequiredError { 64 | return &RequiredError{Fields: fields} 65 | } 66 | 67 | // NewValidationError creates ValidationError 68 | func NewValidationError(field string, description string) *ValidationError { 69 | return &ValidationError{ 70 | Field: field, 71 | Description: description, 72 | } 73 | } 74 | 75 | // ValidationError tells if field has wrong value 76 | type ValidationError struct { 77 | // Field name 78 | Field string `json:"field"` 79 | Description string `json:"description"` 80 | } 81 | 82 | // Error returns string representation for ValidationError 83 | func (e *ValidationError) Error() string { 84 | return fmt.Sprintf( 85 | "field %s is invalid: %s", 86 | e.Field, 87 | e.Description) 88 | } 89 | -------------------------------------------------------------------------------- /errors_internal_test.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "testing" 5 | 6 | "gopkg.in/stretchr/testify.v1/assert" 7 | ) 8 | 9 | func TestIsUnauthorizedError(t *testing.T) { 10 | assert.True(t, IsUnauthorizedError(errUnauthorized)) 11 | assert.False(t, IsUnauthorizedError(errForbidden)) 12 | } 13 | 14 | func TestIsForbiddenError(t *testing.T) { 15 | assert.True(t, IsForbiddenError(errForbidden)) 16 | assert.False(t, IsForbiddenError(errUnauthorized)) 17 | } 18 | 19 | func TestIsAPIError(t *testing.T) { 20 | assert.True(t, IsAPIError(&APIError{})) 21 | assert.False(t, IsAPIError(errUnauthorized)) 22 | } 23 | 24 | func TestIsRequiredError(t *testing.T) { 25 | assert.True(t, IsRequiredError(&RequiredError{})) 26 | assert.False(t, IsRequiredError(errUnauthorized)) 27 | } 28 | 29 | func TestIsValidationError(t *testing.T) { 30 | assert.True(t, IsValidationError(&ValidationError{})) 31 | assert.False(t, IsValidationError(errUnauthorized)) 32 | } 33 | -------------------------------------------------------------------------------- /examples/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | 7 | "github.com/bot-api/telegram" 8 | "golang.org/x/net/context" 9 | ) 10 | 11 | func main() { 12 | token := flag.String("token", "", "telegram bot token") 13 | debug := flag.Bool("debug", false, "show debug information") 14 | flag.Parse() 15 | 16 | if *token == "" { 17 | log.Fatal("token flag required") 18 | } 19 | 20 | api := telegram.New(*token) 21 | api.Debug(*debug) 22 | 23 | ctx, cancel := context.WithCancel(context.Background()) 24 | defer cancel() 25 | 26 | if user, err := api.GetMe(ctx); err != nil { 27 | log.Panic(err) 28 | } else { 29 | log.Printf("bot info: %#v", user) 30 | } 31 | 32 | updatesCh := make(chan telegram.Update) 33 | 34 | go telegram.GetUpdates(ctx, api, telegram.UpdateCfg{ 35 | Timeout: 10, // Timeout in seconds for long polling. 36 | Offset: 0, // Start with the oldest update 37 | }, updatesCh) 38 | 39 | for update := range updatesCh { 40 | log.Printf("got update from %s", update.Message.From.Username) 41 | if update.Message == nil { 42 | continue 43 | } 44 | msg := telegram.CloneMessage(update.Message, nil) 45 | // echo with the same message 46 | if _, err := api.Send(ctx, msg); err != nil { 47 | log.Printf("send error: %v", err) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/callback/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Callback example shows how to use callback query and how to edit bot message 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | "strings" 10 | 11 | "github.com/bot-api/telegram" 12 | "github.com/bot-api/telegram/telebot" 13 | "golang.org/x/net/context" 14 | ) 15 | 16 | func main() { 17 | token := flag.String("token", "", "telegram bot token") 18 | debug := flag.Bool("debug", false, "show debug information") 19 | flag.Parse() 20 | 21 | if *token == "" { 22 | log.Fatal("token flag is required") 23 | 24 | } 25 | 26 | api := telegram.New(*token) 27 | api.Debug(*debug) 28 | bot := telebot.NewWithAPI(api) 29 | bot.Use(telebot.Recover()) // recover if handler panics 30 | 31 | netCtx, cancel := context.WithCancel(context.Background()) 32 | defer cancel() 33 | 34 | bot.HandleFunc(func(ctx context.Context) error { 35 | update := telebot.GetUpdate(ctx) // take update from context 36 | api := telebot.GetAPI(ctx) // take api from context 37 | 38 | if update.CallbackQuery != nil { 39 | data := update.CallbackQuery.Data 40 | if strings.HasPrefix(data, "sex:") { 41 | cfg := telegram.NewEditMessageText( 42 | update.Chat().ID, 43 | update.CallbackQuery.Message.MessageID, 44 | fmt.Sprintf("You sex: %s", data[4:]), 45 | ) 46 | _, err := api.AnswerCallbackQuery( 47 | ctx, 48 | telegram.NewAnswerCallback( 49 | update.CallbackQuery.ID, 50 | "Your configs changed", 51 | ), 52 | ) 53 | if err != nil { 54 | return err 55 | } 56 | _, err = api.EditMessageText(ctx, cfg) 57 | return err 58 | } 59 | } 60 | 61 | msg := telegram.NewMessage(update.Chat().ID, 62 | "Your sex:") 63 | msg.ReplyMarkup = telegram.InlineKeyboardMarkup{ 64 | InlineKeyboard: telegram.NewVInlineKeyboard( 65 | "sex:", 66 | []string{"Female", "Male"}, 67 | []string{"female", "male"}, 68 | ), 69 | } 70 | _, err := api.SendMessage(ctx, msg) 71 | return err 72 | 73 | }) 74 | 75 | err := bot.Serve(netCtx) 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/commands/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Command example shows how to handle commands 4 | 5 | import ( 6 | "flag" 7 | "log" 8 | 9 | "github.com/bot-api/telegram" 10 | "github.com/bot-api/telegram/telebot" 11 | "golang.org/x/net/context" 12 | ) 13 | 14 | func main() { 15 | token := flag.String("token", "", "telegram bot token") 16 | debug := flag.Bool("debug", false, "show debug information") 17 | flag.Parse() 18 | 19 | if *token == "" { 20 | log.Fatal("token flag is required") 21 | 22 | } 23 | 24 | api := telegram.New(*token) 25 | api.Debug(*debug) 26 | bot := telebot.NewWithAPI(api) 27 | bot.Use(telebot.Recover()) // recover if handler panics 28 | 29 | netCtx, cancel := context.WithCancel(context.Background()) 30 | defer cancel() 31 | 32 | // Use command middleware, that helps to work with commands 33 | bot.Use(telebot.Commands(map[string]telebot.Commander{ 34 | "start": telebot.CommandFunc( 35 | func(ctx context.Context, arg string) error { 36 | 37 | api := telebot.GetAPI(ctx) 38 | update := telebot.GetUpdate(ctx) 39 | _, err := api.SendMessage(ctx, 40 | telegram.NewMessagef(update.Chat().ID, 41 | "received start with arg %s", arg, 42 | )) 43 | return err 44 | }), 45 | "": telebot.CommandFunc( 46 | func(ctx context.Context, arg string) error { 47 | 48 | api := telebot.GetAPI(ctx) 49 | update := telebot.GetUpdate(ctx) 50 | command, arg := update.Message.Command() 51 | _, err := api.SendMessage(ctx, 52 | telegram.NewMessagef(update.Chat().ID, 53 | "received unrecognized"+ 54 | " command %s with arg %s", 55 | command, arg, 56 | )) 57 | return err 58 | }), 59 | })) 60 | 61 | err := bot.Serve(netCtx) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/echo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Simple echo bot, that responses with the same message 4 | 5 | import ( 6 | "flag" 7 | "log" 8 | 9 | "github.com/bot-api/telegram" 10 | "github.com/bot-api/telegram/telebot" 11 | "golang.org/x/net/context" 12 | ) 13 | 14 | func main() { 15 | token := flag.String("token", "", "telegram bot token") 16 | debug := flag.Bool("debug", false, "show debug information") 17 | flag.Parse() 18 | 19 | if *token == "" { 20 | log.Fatal("token flag is required") 21 | } 22 | 23 | api := telegram.New(*token) 24 | api.Debug(*debug) 25 | bot := telebot.NewWithAPI(api) 26 | bot.Use(telebot.Recover()) // recover if handler panic 27 | 28 | netCtx, cancel := context.WithCancel(context.Background()) 29 | defer cancel() 30 | 31 | bot.HandleFunc(func(ctx context.Context) error { 32 | update := telebot.GetUpdate(ctx) // take update from context 33 | if update.Message == nil { 34 | return nil 35 | } 36 | api := telebot.GetAPI(ctx) // take api from context 37 | msg := telegram.CloneMessage(update.Message, nil) 38 | _, err := api.Send(ctx, msg) 39 | return err 40 | 41 | }) 42 | err := bot.Serve(netCtx) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/inline/main.go: -------------------------------------------------------------------------------- 1 | // Inline example shows how to use inline bots 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/bot-api/telegram" 10 | "github.com/bot-api/telegram/telebot" 11 | "golang.org/x/net/context" 12 | ) 13 | 14 | func main() { 15 | token := flag.String("token", "", "telegram bot token") 16 | debug := flag.Bool("debug", false, "show debug information") 17 | flag.Parse() 18 | 19 | if *token == "" { 20 | log.Fatal("token flag is required") 21 | } 22 | 23 | api := telegram.New(*token) 24 | api.Debug(*debug) 25 | bot := telebot.NewWithAPI(api) 26 | bot.Use(telebot.Recover()) // recover if handler panic 27 | 28 | netCtx, cancel := context.WithCancel(context.Background()) 29 | defer cancel() 30 | 31 | bot.HandleFunc(func(ctx context.Context) error { 32 | update := telebot.GetUpdate(ctx) // take update from context 33 | if update.InlineQuery == nil { 34 | return nil 35 | } 36 | query := update.InlineQuery 37 | api := telebot.GetAPI(ctx) // take api from context 38 | 39 | api.AnswerInlineQuery(ctx, telegram.AnswerInlineQueryCfg{ 40 | InlineQueryID: query.ID, 41 | Results: []telegram.InlineQueryResult{ 42 | telegram.NewInlineQueryResultArticle( 43 | "10", 44 | fmt.Sprintf("one for %s", query.Query), 45 | fmt.Sprintf("result1: %s", query.Query), 46 | ), 47 | telegram.NewInlineQueryResultArticle( 48 | "11", 49 | fmt.Sprintf("two for %s", query.Query), 50 | fmt.Sprintf("result2: %s", query.Query), 51 | ), 52 | }, 53 | CacheTime: 10, // cached for 10 seconds 54 | 55 | }) 56 | return nil 57 | 58 | }) 59 | err := bot.Serve(netCtx) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Simple proxy bot, that proxy messages from one user to another 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | 10 | "github.com/bot-api/telegram" 11 | "github.com/bot-api/telegram/telebot" 12 | "golang.org/x/net/context" 13 | ) 14 | 15 | func main() { 16 | token := flag.String("token", "", "telegram bot token") 17 | user1 := flag.Int("user1", 0, "first user") 18 | user2 := flag.Int("user2", 0, "second user") 19 | debug := flag.Bool("debug", false, "show debug information") 20 | flag.Parse() 21 | 22 | if *token == "" { 23 | log.Fatal("token flag is required") 24 | } 25 | if !telegram.IsValidToken(*token) { 26 | log.Fatal("token has wrong format") 27 | } 28 | 29 | api := telegram.New(*token) 30 | api.Debug(*debug) 31 | bot := telebot.NewWithAPI(api) 32 | 33 | netCtx, cancel := context.WithCancel(context.Background()) 34 | defer cancel() 35 | 36 | bot.Use(telebot.Recover()) 37 | 38 | bot.HandleFunc(func(ctx context.Context) error { 39 | update := telebot.GetUpdate(ctx) 40 | if update.Message == nil { 41 | return nil 42 | } 43 | api := telebot.GetAPI(ctx) 44 | 45 | userTo := *user1 46 | if update.Message.Chat.ID == int64(userTo) { 47 | userTo = *user2 48 | } 49 | msg := telegram.CloneMessage( 50 | update.Message, 51 | &telegram.BaseMessage{ 52 | BaseChat: telegram.BaseChat{ 53 | ID: int64(userTo), 54 | }, 55 | ReplyToMessageID: 0, 56 | }, 57 | ) 58 | if msg == nil { 59 | return fmt.Errorf("can't clone message") 60 | } 61 | _, err := api.Send(ctx, msg) 62 | return err 63 | 64 | }) 65 | 66 | err := bot.Serve(netCtx) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "golang.org/x/net/context" 8 | ) 9 | 10 | func newBM(chatID int64) BaseMessage { 11 | return BaseMessage{ 12 | BaseChat: BaseChat{ 13 | ID: chatID, 14 | }, 15 | } 16 | } 17 | 18 | // NewMessage creates a new Message. 19 | // 20 | // chatID is where to send it, text is the message text. 21 | func NewMessage(chatID int64, text string) MessageCfg { 22 | return MessageCfg{ 23 | BaseMessage: newBM(chatID), 24 | Text: text, 25 | DisableWebPagePreview: false, 26 | } 27 | } 28 | 29 | // NewMessagef creates a new Message with formatting. 30 | // 31 | // chatID is where to send it, text is the message text 32 | func NewMessagef(chatID int64, text string, args ...interface{}) MessageCfg { 33 | return MessageCfg{ 34 | BaseMessage: newBM(chatID), 35 | Text: fmt.Sprintf(text, args...), 36 | DisableWebPagePreview: false, 37 | } 38 | } 39 | 40 | // NewKeyboard creates keyboard by matrix i*j. 41 | func NewKeyboard(buttons [][]string) [][]KeyboardButton { 42 | rows := make([][]KeyboardButton, len(buttons)) 43 | for i, colButtons := range buttons { 44 | cols := make([]KeyboardButton, len(colButtons)) 45 | for j, button := range colButtons { 46 | cols[j].Text = button 47 | } 48 | rows[i] = cols 49 | } 50 | return rows 51 | } 52 | 53 | // NewHKeyboard creates keyboard with horizontal buttons only. 54 | // [ first ] [ second ] [ third ] 55 | func NewHKeyboard(buttons ...string) [][]KeyboardButton { 56 | row := make([]KeyboardButton, len(buttons)) 57 | for i, button := range buttons { 58 | row[i].Text = button 59 | } 60 | return [][]KeyboardButton{row} 61 | } 62 | 63 | // NewVKeyboard creates keyboard with vertical buttons only 64 | // [ first ] 65 | // [ second ] 66 | // [ third ] 67 | func NewVKeyboard(buttons ...string) [][]KeyboardButton { 68 | r := make([][]KeyboardButton, len(buttons)) 69 | for i, button := range buttons { 70 | r[i] = []KeyboardButton{{Text: button}} 71 | } 72 | return r 73 | } 74 | 75 | // NewHInlineKeyboard creates inline keyboard with horizontal buttons only. 76 | // [ first ] [ second ] [ third ] 77 | func NewHInlineKeyboard(prefix string, text []string, data []string) [][]InlineKeyboardButton { 78 | row := make([]InlineKeyboardButton, len(text)) 79 | 80 | for i, t := range text { 81 | row[i].Text = t 82 | row[i].CallbackData = prefix + data[i] 83 | } 84 | return [][]InlineKeyboardButton{row} 85 | } 86 | 87 | // NewVInlineKeyboard creates inline keyboard with vertical buttons only 88 | // [ first ] 89 | // [ second ] 90 | // [ third ] 91 | func NewVInlineKeyboard(prefix string, text []string, data []string) [][]InlineKeyboardButton { 92 | r := make([][]InlineKeyboardButton, len(text)) 93 | for i, button := range text { 94 | r[i] = []InlineKeyboardButton{ 95 | {Text: button, CallbackData: prefix + data[i]}, 96 | } 97 | } 98 | return r 99 | } 100 | 101 | // NewForwardMessage creates a new Message. 102 | // 103 | // chatID is where to send it, text is the message text. 104 | func NewForwardMessage(chatID, fromChatID, messageID int64) ForwardMessageCfg { 105 | return ForwardMessageCfg{ 106 | BaseChat: BaseChat{ 107 | ID: chatID, 108 | }, 109 | FromChat: BaseChat{ 110 | ID: fromChatID, 111 | }, 112 | MessageID: messageID, 113 | } 114 | } 115 | 116 | // NewUserProfilePhotos gets user profile photos. 117 | // 118 | // userID is the ID of the user you wish to get profile photos from. 119 | func NewUserProfilePhotos(userID int64) UserProfilePhotosCfg { 120 | return UserProfilePhotosCfg{ 121 | UserID: userID, 122 | Offset: 0, 123 | Limit: 0, 124 | } 125 | } 126 | 127 | // NewUpdate gets updates since the last Offset with Timeout 30 seconds 128 | // 129 | // offset is the last Update ID to include. 130 | // You likely want to set this to the last Update ID plus 1. 131 | // The negative offset can be specified to retrieve updates starting 132 | // from -offset update from the end of the updates queue. 133 | // All previous updates will forgotten. 134 | func NewUpdate(offset int64) UpdateCfg { 135 | return UpdateCfg{ 136 | Offset: offset, 137 | Limit: 0, 138 | Timeout: 30, 139 | } 140 | } 141 | 142 | // NewChatAction sets a chat action. 143 | // Actions last for 5 seconds, or until your next action. 144 | // 145 | // chatID is where to send it, action should be set via Action constants. 146 | func NewChatAction(chatID int64, action string) ChatActionCfg { 147 | return ChatActionCfg{ 148 | BaseChat: BaseChat{ID: chatID}, 149 | Action: action, 150 | } 151 | } 152 | 153 | // NewLocation shares your location. 154 | // 155 | // chatID is where to send it, latitude and longitude are coordinates. 156 | func NewLocation(chatID int64, lat float64, lon float64) LocationCfg { 157 | return LocationCfg{ 158 | BaseMessage: newBM(chatID), 159 | Location: Location{ 160 | Latitude: lat, 161 | Longitude: lon, 162 | }, 163 | } 164 | } 165 | 166 | // NewPhotoUpload creates a new photo uploader. 167 | // 168 | // chatID is where to send it, inputFile is a file representation. 169 | func NewPhotoUpload(chatID int64, inputFile InputFile) PhotoCfg { 170 | return PhotoCfg{ 171 | BaseFile: BaseFile{ 172 | BaseMessage: newBM(chatID), 173 | InputFile: inputFile, 174 | }, 175 | } 176 | } 177 | 178 | // NewPhotoShare creates a new photo uploader. 179 | // 180 | // chatID is where to send it 181 | func NewPhotoShare(chatID int64, fileID string) PhotoCfg { 182 | return PhotoCfg{ 183 | BaseFile: BaseFile{ 184 | BaseMessage: newBM(chatID), 185 | FileID: fileID, 186 | }, 187 | } 188 | } 189 | 190 | // NewAnswerCallback creates a new callback message. 191 | func NewAnswerCallback(id, text string) AnswerCallbackCfg { 192 | return AnswerCallbackCfg{ 193 | CallbackQueryID: id, 194 | Text: text, 195 | ShowAlert: false, 196 | } 197 | } 198 | 199 | // NewAnswerCallbackWithAlert creates a new callback message that alerts 200 | // the user. 201 | func NewAnswerCallbackWithAlert(id, text string) AnswerCallbackCfg { 202 | return AnswerCallbackCfg{ 203 | CallbackQueryID: id, 204 | Text: text, 205 | ShowAlert: true, 206 | } 207 | } 208 | 209 | // NewEditMessageText allows you to edit the text of a message. 210 | func NewEditMessageText(chatID, messageID int64, text string) EditMessageTextCfg { 211 | return EditMessageTextCfg{ 212 | BaseEdit: BaseEdit{ 213 | ChatID: chatID, 214 | MessageID: messageID, 215 | }, 216 | Text: text, 217 | } 218 | } 219 | 220 | // NewEditMessageCaption allows you to edit the caption of a message. 221 | func NewEditMessageCaption(chatID, messageID int64, caption string) EditMessageCaptionCfg { 222 | return EditMessageCaptionCfg{ 223 | BaseEdit: BaseEdit{ 224 | ChatID: chatID, 225 | MessageID: messageID, 226 | }, 227 | Caption: caption, 228 | } 229 | } 230 | 231 | // NewEditMessageReplyMarkup allows you to edit the inline 232 | // keyboard markup. 233 | func NewEditMessageReplyMarkup(chatID, messageID int64, replyMarkup *InlineKeyboardMarkup) EditMessageReplyMarkupCfg { 234 | return EditMessageReplyMarkupCfg{ 235 | BaseEdit: BaseEdit{ 236 | ChatID: chatID, 237 | MessageID: messageID, 238 | ReplyMarkup: replyMarkup, 239 | }, 240 | } 241 | } 242 | 243 | // NewWebhook creates a new webhook. 244 | // 245 | // link is the url parsable link you wish to get the updates. 246 | func NewWebhook(link string) WebhookCfg { 247 | u, _ := url.Parse(link) 248 | 249 | return WebhookCfg{ 250 | URL: u.String(), 251 | } 252 | } 253 | 254 | // NewWebhookWithCert creates a new webhook with a certificate. 255 | // 256 | // link is the url you wish to get webhooks, 257 | // file contains a string to a file, FileReader, or FileBytes. 258 | func NewWebhookWithCert(link string, file InputFile) WebhookCfg { 259 | u, _ := url.Parse(link) 260 | 261 | return WebhookCfg{ 262 | URL: u.String(), 263 | Certificate: file, 264 | } 265 | } 266 | 267 | // CloneMessage convert message to Messenger type to send it to another chat. 268 | // It supports only data message: Text, Sticker, Audio, Photo, Location, 269 | // Contact, Audio, Voice, Document. 270 | func CloneMessage(msg *Message, baseMessage *BaseMessage) Messenger { 271 | var base BaseMessage 272 | if baseMessage == nil { 273 | base = newBM(msg.Chat.ID) 274 | 275 | } else { 276 | base = *baseMessage 277 | } 278 | if msg.Text != "" { 279 | return &MessageCfg{ 280 | BaseMessage: base, 281 | Text: msg.Text, 282 | } 283 | } 284 | if msg.Sticker != nil { 285 | return &StickerCfg{ 286 | BaseFile: BaseFile{ 287 | BaseMessage: base, 288 | FileID: msg.Sticker.FileID, 289 | }, 290 | } 291 | } 292 | if msg.Photo != nil && len(msg.Photo) > 0 { 293 | return &PhotoCfg{ 294 | BaseFile: BaseFile{ 295 | BaseMessage: base, 296 | FileID: msg.Photo[len(msg.Photo)-1].FileID, 297 | }, 298 | Caption: msg.Caption, 299 | } 300 | } 301 | if msg.Location != nil { 302 | return &LocationCfg{ 303 | BaseMessage: base, 304 | Location: *msg.Location, 305 | } 306 | } 307 | if msg.Contact != nil { 308 | return &ContactCfg{ 309 | BaseMessage: base, 310 | Contact: *msg.Contact, 311 | } 312 | } 313 | if msg.Audio != nil { 314 | return &AudioCfg{ 315 | BaseFile: BaseFile{ 316 | BaseMessage: base, 317 | FileID: msg.Audio.FileID, 318 | }, 319 | Duration: msg.Audio.Duration, 320 | Performer: msg.Audio.Performer, 321 | Title: msg.Audio.Title, 322 | } 323 | } 324 | if msg.Voice != nil { 325 | return &VoiceCfg{ 326 | BaseFile: BaseFile{ 327 | BaseMessage: base, 328 | FileID: msg.Voice.FileID, 329 | }, 330 | Duration: msg.Voice.Duration, 331 | } 332 | } 333 | if msg.Document != nil { 334 | return &DocumentCfg{ 335 | BaseFile: BaseFile{ 336 | BaseMessage: base, 337 | FileID: msg.Document.FileID, 338 | }, 339 | } 340 | } 341 | return nil 342 | } 343 | 344 | // GetUpdates runs loop and requests updates from telegram. 345 | // It breaks loop, close out channel and returns error 346 | // if something happened during update cycle. 347 | func GetUpdates( 348 | ctx context.Context, 349 | api *API, 350 | cfg UpdateCfg, 351 | out chan<- Update) error { 352 | 353 | var rErr error 354 | defer close(out) 355 | 356 | loop: 357 | for { 358 | updates, err := api.GetUpdates( 359 | ctx, 360 | cfg, 361 | ) 362 | if err != nil { 363 | rErr = err 364 | break loop 365 | } 366 | for _, update := range updates { 367 | if update.UpdateID >= cfg.Offset { 368 | cfg.Offset = update.UpdateID + 1 369 | select { 370 | case <-ctx.Done(): 371 | rErr = ctx.Err() 372 | break loop 373 | case out <- update: 374 | } 375 | } 376 | } 377 | } 378 | return rErr 379 | } 380 | 381 | // InlineQuery helpers 382 | 383 | // NewInlineQueryResultArticle creates a new inline query article. 384 | func NewInlineQueryResultArticle(id, title, messageText string) *InlineQueryResultArticle { 385 | return &InlineQueryResultArticle{ 386 | BaseInlineQueryResult: BaseInlineQueryResult{ 387 | Type: "article", 388 | ID: id, 389 | InputMessageContent: InputTextMessageContent{ 390 | MessageText: messageText, 391 | }, 392 | }, 393 | Title: title, 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /interfaces.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "io" 5 | "net/url" 6 | ) 7 | 8 | // Method describes interface for Telegram API request 9 | // 10 | // Every method is https://api.telegram.org/bot/METHOD_NAME 11 | // Values are passed as application/x-www-form-urlencoded for usual request 12 | // and multipart/form-data when files are uploaded. 13 | type Method interface { 14 | // method name 15 | Name() string 16 | // method params 17 | Values() (url.Values, error) 18 | } 19 | 20 | // Messenger is a virtual interface to distinct methods 21 | // that return Message from others.BaseMessage 22 | type Messenger interface { 23 | Method 24 | Message() *Message 25 | } 26 | 27 | // Filer is any config type that can be sent that includes a file. 28 | type Filer interface { 29 | // Field name for file data 30 | Field() string 31 | // File data 32 | File() InputFile 33 | // Exist returns true if file exists on telegram servers 34 | Exist() bool 35 | // Reset removes FileID and sets new InputFile 36 | // Reset(InputFile) 37 | // GetFileID returns fileID if it's exist 38 | GetFileID() string 39 | } 40 | 41 | // ReplyMarkup describes interface for reply_markup keyboards. 42 | type ReplyMarkup interface { 43 | // ReplyMarkup is a fake method that helps to identify implementations 44 | ReplyMarkup() 45 | } 46 | 47 | // InputFile describes interface for input files. 48 | type InputFile interface { 49 | Reader() io.Reader 50 | Name() string 51 | } 52 | 53 | // InlineQueryResult interface represents one result of an inline query. 54 | // Telegram clients currently support results of the following 19 types: 55 | // 56 | // - InlineQueryResultCachedAudio 57 | // - InlineQueryResultCachedDocument 58 | // - InlineQueryResultCachedGif 59 | // - InlineQueryResultCachedMpeg4Gif 60 | // - InlineQueryResultCachedPhoto 61 | // - InlineQueryResultCachedSticker 62 | // - InlineQueryResultCachedVideo 63 | // - InlineQueryResultCachedVoice 64 | // - InlineQueryResultArticle 65 | // - InlineQueryResultAudio 66 | // - InlineQueryResultContact 67 | // - InlineQueryResultDocument 68 | // - InlineQueryResultGif 69 | // - InlineQueryResultLocation 70 | // - InlineQueryResultMpeg4Gif 71 | // - InlineQueryResultPhoto 72 | // - InlineQueryResultVenue 73 | // - InlineQueryResultVideo 74 | // - InlineQueryResultVoice 75 | // 76 | type InlineQueryResult interface { 77 | // InlineQueryResult is a fake method that helps to identify implementations 78 | InlineQueryResult() 79 | } 80 | 81 | // InputMessageContent interface represents the content of a message 82 | // to be sent as a result of an inline query. 83 | // Telegram clients currently support the following 4 types: 84 | // - InputTextMessageContent 85 | // - InputLocationMessageContent 86 | // - InputVenueMessageContent 87 | // - InputContactMessageContent 88 | type InputMessageContent interface { 89 | // MessageContent is a fake method that helps to identify implementations 90 | InputMessageContent() 91 | } 92 | -------------------------------------------------------------------------------- /scripts/checkfmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | test_fmt() { 5 | DIR="$1" 6 | hash goimports 2>&- || { echo >&2 "goimports not in PATH."; exit 1; } 7 | 8 | for file in $(find -L $DIR -type f -name "*.go" -not -path "./Godeps/*") 9 | do 10 | output=`cat $file | goimports -l 2>&1` 11 | if test $? -ne 0 12 | then 13 | output=`echo "$output" | sed "s,,$file,"` 14 | syntaxerrors="${list}${output}\n" 15 | elif test -n "$output" 16 | then 17 | list="${list}${file}\n" 18 | fi 19 | done 20 | exitcode=0 21 | if test -n "$syntaxerrors" 22 | then 23 | echo >&2 "goimports found syntax errors:" 24 | printf "$syntaxerrors" 25 | exitcode=1 26 | fi 27 | if test -n "$list" 28 | then 29 | echo >&2 "goimports needs to format these files (run make fmt and git add):" 30 | printf "$list" 31 | printf "\n" 32 | exitcode=1 33 | fi 34 | exit $exitcode 35 | } 36 | 37 | main() { 38 | test_fmt "$@" 39 | } 40 | 41 | main "$@" 42 | 43 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | 6 | main() { 7 | _cd_into_top_level 8 | _generate_coverage_files 9 | _combine_coverage_reports 10 | } 11 | 12 | _cd_into_top_level() { 13 | cd "$(git rev-parse --show-toplevel)" 14 | } 15 | 16 | _generate_coverage_files() { 17 | for dir in $(find . -maxdepth 10 -not -path './.git*' -not -path '*/_*' -type d); do 18 | if ls $dir/*.go &>/dev/null ; then 19 | go test -covermode=count -coverprofile=$dir/profile.coverprofile $dir || fail=1 20 | fi 21 | done 22 | } 23 | 24 | 25 | _combine_coverage_reports() { 26 | gover 27 | } 28 | 29 | main "$@" -------------------------------------------------------------------------------- /scripts/coverage_i.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | 6 | main() { 7 | _cd_into_top_level 8 | _generate_coverage_files 9 | } 10 | 11 | _cd_into_top_level() { 12 | cd "$(git rev-parse --show-toplevel)" 13 | } 14 | 15 | _generate_coverage_files() { 16 | for dir in $(find . -maxdepth 10 -not -path './.git*' -not -path '*/_*' -type d); do 17 | if ls $dir/*.go &>/dev/null ; then 18 | go test -tags integration -run TestI_* -covermode=count -coverprofile=$dir/profile_i.coverprofile $dir || fail=1 19 | fi 20 | done 21 | } 22 | 23 | main "$@" -------------------------------------------------------------------------------- /telebot/bot.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/bot-api/telegram" 10 | "golang.org/x/net/context" 11 | ) 12 | 13 | type apiKey struct{} 14 | type updateKey struct{} 15 | type webhookKey struct{} 16 | 17 | // GetAPI takes telegram API from context. 18 | // Raises panic if context doesn't have API instance. 19 | func GetAPI(ctx context.Context) *telegram.API { 20 | return ctx.Value(apiKey{}).(*telegram.API) 21 | } 22 | 23 | // GetUpdate takes telegram Update from context. 24 | // Raises panic if context doesn't have Update instance. 25 | func GetUpdate(ctx context.Context) *telegram.Update { 26 | return ctx.Value(updateKey{}).(*telegram.Update) 27 | } 28 | 29 | // WithUpdate returns context with telegram Update inside. 30 | // Use GetUpdate to take Update from context. 31 | func WithUpdate(ctx context.Context, u *telegram.Update) context.Context { 32 | return context.WithValue(ctx, updateKey{}, u) 33 | } 34 | 35 | // WithAPI returns context with telegram api inside. 36 | // Use GetAPI to take api from context. 37 | func WithAPI(ctx context.Context, api *telegram.API) context.Context { 38 | return context.WithValue(ctx, apiKey{}, api) 39 | } 40 | 41 | // IsWebhook returns true if update received by webhook 42 | func IsWebhook(ctx context.Context) bool { 43 | return ctx.Value(webhookKey{}) != nil 44 | } 45 | 46 | // A Bot object helps to work with telegram bot api using handlers. 47 | // 48 | // Bot initialization is not thread safe. 49 | type Bot struct { 50 | api *telegram.API 51 | me *telegram.User 52 | 53 | handler Handler 54 | middleware []MiddlewareFunc 55 | errFunc ErrorFunc 56 | } 57 | 58 | // NewWithAPI returns bot with custom API client 59 | func NewWithAPI(api *telegram.API) *Bot { 60 | return &Bot{ 61 | api: api, 62 | middleware: []MiddlewareFunc{}, 63 | errFunc: func(ctx context.Context, err error) { 64 | log.Printf("update error: %s", err.Error()) 65 | }, 66 | } 67 | } 68 | 69 | // New returns bot with default api client 70 | func New(token string) *Bot { 71 | return NewWithAPI(telegram.New(token)) 72 | } 73 | 74 | // Use adds middleware to a middleware chain. 75 | func (b *Bot) Use(middleware ...MiddlewareFunc) { 76 | b.middleware = append(b.middleware, middleware...) 77 | } 78 | 79 | // Handle setups handler to handle telegram updates. 80 | func (b *Bot) Handle(handler Handler) { 81 | b.handler = handler 82 | } 83 | 84 | // HandleFunc takes HandlerFunc and sets handler. 85 | func (b *Bot) HandleFunc(handler HandlerFunc) { 86 | b.handler = handler 87 | } 88 | 89 | // ErrorFunc set a ErrorFunc, that handles error returned 90 | // from handlers/middlewares. 91 | func (b *Bot) ErrorFunc(errFunc ErrorFunc) { 92 | b.errFunc = errFunc 93 | } 94 | 95 | // ServeWithConfig runs update cycle with custom update config. 96 | func (b *Bot) ServeWithConfig(ctx context.Context, cfg telegram.UpdateCfg) error { 97 | if err := b.updateMe(ctx); err != nil { 98 | return err 99 | } 100 | 101 | var rErr error 102 | errCh := make(chan error, 1) 103 | updatesCh := make(chan telegram.Update) 104 | go func() { 105 | errCh <- telegram.GetUpdates( 106 | ctx, 107 | b.api, 108 | cfg, 109 | updatesCh) 110 | }() 111 | loop: 112 | for { 113 | select { 114 | case rErr = <-errCh: 115 | break loop 116 | case update, ok := <-updatesCh: 117 | if !ok { 118 | // update channel was closed, wait for error 119 | select { 120 | case rErr = <-errCh: 121 | break loop 122 | } 123 | } 124 | b.handleUpdate(ctx, &update) 125 | 126 | } 127 | } 128 | return rErr 129 | } 130 | 131 | // Serve runs update cycle with default update config. 132 | // Offset is zero and timeout is 30 seconds. 133 | func (b *Bot) Serve(ctx context.Context) error { 134 | cfg := telegram.NewUpdate(0) 135 | return b.ServeWithConfig(ctx, cfg) 136 | } 137 | 138 | // ServeByWebhook returns webhook handler, 139 | // that can handle incoming telegram webhook messages. 140 | // 141 | // Use IsWebhook function to identify webhook updates. 142 | func (b *Bot) ServeByWebhook(ctx context.Context) (http.HandlerFunc, error) { 143 | if err := b.updateMe(ctx); err != nil { 144 | return nil, err 145 | } 146 | 147 | updatesCh := make(chan telegram.Update) 148 | go func() { 149 | loop: 150 | for { 151 | select { 152 | case <-ctx.Done(): 153 | break loop 154 | case update := <-updatesCh: 155 | b.handleUpdate( 156 | context.WithValue( 157 | ctx, 158 | webhookKey{}, 159 | struct{}{}), 160 | &update, 161 | ) 162 | } 163 | } 164 | }() 165 | return b.getWebhookHandler(ctx, updatesCh), nil 166 | } 167 | 168 | // ============== Internal ================================================== // 169 | 170 | func (b *Bot) updateMe(ctx context.Context) (err error) { 171 | b.me, err = b.api.GetMe(ctx) 172 | return err 173 | } 174 | 175 | func (b *Bot) handleUpdate(ctx context.Context, update *telegram.Update) { 176 | ctx = WithAPI(ctx, b.api) 177 | ctx = WithUpdate(ctx, update) 178 | ctx = context.WithValue(ctx, "update.id", update.UpdateID) 179 | h := b.handler 180 | if h == nil { 181 | h = EmptyHandler() 182 | } 183 | for i := len(b.middleware) - 1; i >= 0; i-- { 184 | h = b.middleware[i](h) 185 | } 186 | 187 | err := h.Handle(ctx) 188 | if err != nil && b.errFunc != nil { 189 | eh := b.errFunc 190 | eh(ctx, err) 191 | } 192 | } 193 | 194 | func (b *Bot) getWebhookHandler( 195 | ctx context.Context, 196 | out chan<- telegram.Update) http.HandlerFunc { 197 | 198 | return func(w http.ResponseWriter, r *http.Request) { 199 | bytes, err := ioutil.ReadAll(r.Body) 200 | if err != nil { 201 | log.Println(err) 202 | return 203 | } 204 | 205 | var update telegram.Update 206 | err = json.Unmarshal(bytes, &update) 207 | if err != nil { 208 | log.Println(err) 209 | return 210 | } 211 | select { 212 | case out <- update: 213 | case <-ctx.Done(): 214 | return 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /telebot/bot_internal_test.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | "time" 11 | 12 | "github.com/bot-api/telegram" 13 | "github.com/m0sth8/httpmock" 14 | "golang.org/x/net/context" 15 | "gopkg.in/stretchr/testify.v1/assert" 16 | "gopkg.in/stretchr/testify.v1/require" 17 | ) 18 | 19 | func TestNewWithApi(t *testing.T) { 20 | api := &telegram.API{} 21 | b := NewWithAPI(api) 22 | assert.Equal(t, api, b.api) 23 | assert.Equal(t, 0, len(b.middleware)) 24 | assert.NotNil(t, b.errFunc) 25 | assert.Nil(t, b.handler) 26 | } 27 | 28 | func TestBot_Use(t *testing.T) { 29 | b := New("") 30 | b.Use(nil, nil) 31 | assert.Equal(t, 2, len(b.middleware)) 32 | } 33 | 34 | func TestBot_Handle(t *testing.T) { 35 | b := New("") 36 | h := HandlerFunc(func(context.Context) error { return nil }) 37 | b.Handle(h) 38 | assert.NotNil(t, b.handler) 39 | assert.Equal(t, fmt.Sprintf("%#v", h), fmt.Sprintf("%#v", b.handler)) 40 | } 41 | 42 | func TestBot_HandleFunc(t *testing.T) { 43 | b := New("") 44 | h := HandlerFunc(func(context.Context) error { return nil }) 45 | b.HandleFunc(h) 46 | assert.NotNil(t, b.handler) 47 | assert.Equal(t, fmt.Sprintf("%#v", h), fmt.Sprintf("%#v", b.handler)) 48 | } 49 | 50 | func TestBot_ErrorFunc(t *testing.T) { 51 | b := New("") 52 | h := ErrorFunc(func(context.Context, error) {}) 53 | b.ErrorFunc(h) 54 | assert.NotNil(t, b.errFunc) 55 | assert.Equal(t, fmt.Sprintf("%#v", h), fmt.Sprintf("%#v", b.errFunc)) 56 | } 57 | 58 | func TestBot_handleUpdate(t *testing.T) { 59 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 60 | defer cancel() 61 | expErr := fmt.Errorf("expected error") 62 | 63 | api := telegram.New("token") 64 | b := NewWithAPI(api) 65 | u := &telegram.Update{ 66 | UpdateID: 10, 67 | Message: &telegram.Message{ 68 | Text: "message", 69 | }, 70 | } 71 | handlerInvoked := false 72 | errFuncInvoked := false 73 | middlewareInvoked := false 74 | b.ErrorFunc(func(_ context.Context, err error) { 75 | errFuncInvoked = true 76 | assert.Equal(t, expErr, err) 77 | }) 78 | b.Use(func(next Handler) Handler { 79 | return HandlerFunc(func(ctx context.Context) error { 80 | middlewareInvoked = true 81 | return next.Handle(ctx) 82 | }) 83 | }) 84 | b.HandleFunc(func(ctx context.Context) error { 85 | handlerInvoked = true 86 | assert.Equal(t, api, GetAPI(ctx)) 87 | assert.Equal(t, u, GetUpdate(ctx)) 88 | assert.Equal(t, u.UpdateID, ctx.Value("update.id")) 89 | return expErr 90 | }) 91 | b.handleUpdate(ctx, u) 92 | 93 | assert.True(t, handlerInvoked, "handler wasn't invoked") 94 | assert.True(t, errFuncInvoked, "errFunc wasn't invoked") 95 | assert.True(t, middlewareInvoked, "middleware wasn't invoked") 96 | 97 | // test EmptyHandler 98 | b = NewWithAPI(api) 99 | b.Use(func(next Handler) Handler { 100 | return HandlerFunc(func(ctx context.Context) error { 101 | assert.Equal(t, 102 | fmt.Sprintf("%#v", EmptyHandler()), 103 | fmt.Sprintf("%#v", next)) 104 | return next.Handle(ctx) 105 | }) 106 | }) 107 | b.handleUpdate(ctx, u) 108 | 109 | } 110 | 111 | func TestBot_getWebhookHandler(t *testing.T) { 112 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 113 | defer cancel() 114 | 115 | api := telegram.New("token") 116 | b := NewWithAPI(api) 117 | expUpd := telegram.Update{ 118 | UpdateID: 10, 119 | Message: &telegram.Message{ 120 | Text: "message", 121 | }, 122 | } 123 | ch := make(chan telegram.Update, 1) 124 | whHandler := b.getWebhookHandler(ctx, ch) 125 | 126 | { 127 | w := httptest.NewRecorder() 128 | // prepare request 129 | buf := &bytes.Buffer{} 130 | err := json.NewEncoder(buf).Encode(expUpd) 131 | require.NoError(t, err) 132 | req, err := http.NewRequest("POST", "", buf) 133 | require.NoError(t, err) 134 | 135 | whHandler(w, req) 136 | 137 | select { 138 | case <-ctx.Done(): 139 | require.NoError(t, ctx.Err()) 140 | case upd := <-ch: 141 | assert.EqualValues(t, expUpd, upd) 142 | } 143 | } 144 | 145 | // TODO (m0sth8): what to do with errors during webhook handling?? 146 | // right now it's just logging 147 | //{ 148 | // w := httptest.NewRecorder() 149 | // // prepare request 150 | // req, err := http.NewRequest("POST", "", 151 | // bytes.NewBufferString("bad json")) 152 | // require.NoError(t, err) 153 | // 154 | // whHandler(w, req) 155 | // 156 | // select { 157 | // case <- ctx.Done(): 158 | // require.NoError(t, ctx.Err()) 159 | // case upd := <- ch: 160 | // assert.EqualValues(t, expUpd, upd) 161 | // } 162 | //} 163 | 164 | } 165 | 166 | func NewAPIResponder(status int, result interface{}) httpmock.Responder { 167 | data, err := json.Marshal(result) 168 | if err != nil { 169 | panic(err) 170 | } 171 | raw := json.RawMessage(data) 172 | apiResponse := telegram.APIResponse{ 173 | Ok: true, 174 | Result: &raw, 175 | } 176 | responder, err := httpmock.NewJsonResponder(status, apiResponse) 177 | if err != nil { 178 | panic(err) 179 | } 180 | return responder 181 | } 182 | 183 | func TestBot_updateMe(t *testing.T) { 184 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 185 | defer cancel() 186 | 187 | client := &http.Client{} 188 | api := telegram.NewWithClient("_token", client) 189 | b := NewWithAPI(api) 190 | expMe := telegram.User{ 191 | ID: 10, 192 | Username: "test_bot", 193 | } 194 | 195 | httpmock.ActivateNonDefault(client) 196 | defer httpmock.DeactivateAndReset() 197 | 198 | httpmock.RegisterResponder( 199 | "POST", 200 | "https://api.telegram.org/bot_token/getMe", 201 | NewAPIResponder(200, expMe), 202 | ) 203 | err := b.updateMe(ctx) 204 | require.NoError(t, err) 205 | assert.Equal(t, expMe, *b.me) 206 | } 207 | 208 | func TestBot_Serve(t *testing.T) { 209 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 210 | defer cancel() 211 | 212 | client := &http.Client{} 213 | api := telegram.NewWithClient("_token", client) 214 | 215 | expUpd := []telegram.Update{ 216 | { 217 | UpdateID: 10, 218 | Message: &telegram.Message{ 219 | Text: "message", 220 | }, 221 | }, 222 | { 223 | UpdateID: 11, 224 | Message: &telegram.Message{ 225 | Text: "message", 226 | }, 227 | }, 228 | } 229 | expMe := telegram.User{ 230 | ID: 10, 231 | Username: "test_bot", 232 | } 233 | httpmock.ActivateNonDefault(client) 234 | defer httpmock.DeactivateAndReset() 235 | 236 | httpmock.RegisterResponder( 237 | "POST", 238 | "https://api.telegram.org/bot_token/getMe", 239 | NewAPIResponder(200, expMe), 240 | ) 241 | httpmock.RegisterResponder( 242 | "POST", 243 | "https://api.telegram.org/bot_token/getUpdates", 244 | NewAPIResponder(200, expUpd), 245 | ) 246 | 247 | b := NewWithAPI(api) 248 | handleCh := make(chan context.Context, 1) 249 | b.HandleFunc(func(ctx context.Context) error { 250 | select { 251 | case handleCh <- ctx: 252 | case <-ctx.Done(): 253 | } 254 | return nil 255 | }) 256 | 257 | errCh := make(chan error, 1) 258 | go func() { 259 | errCh <- b.Serve(ctx) 260 | }() 261 | 262 | // got first update 263 | var update1 *telegram.Update 264 | select { 265 | case err := <-errCh: 266 | require.NoError(t, err) 267 | case handleCtx1 := <-handleCh: 268 | update1 = GetUpdate(handleCtx1) 269 | } 270 | 271 | assert.Equal(t, expMe, *b.me) 272 | assert.Equal(t, expUpd[0], *update1) 273 | 274 | // got second update 275 | var update2 *telegram.Update 276 | select { 277 | case err := <-errCh: 278 | require.NoError(t, err) 279 | case handleCtx1 := <-handleCh: 280 | update2 = GetUpdate(handleCtx1) 281 | } 282 | 283 | assert.Equal(t, expUpd[1], *update2) 284 | 285 | cancel() 286 | select { 287 | case err := <-errCh: 288 | require.Equal(t, err, context.Canceled, "exp %v", err.Error()) 289 | case <-time.After(time.Second * 5): 290 | t.Fatal("Server should be cancelled") 291 | } 292 | } 293 | 294 | func TestBot_ServeByWebhook(t *testing.T) { 295 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 296 | defer cancel() 297 | httpmock.Activate() 298 | defer httpmock.DeactivateAndReset() 299 | 300 | api := telegram.New("_token") 301 | 302 | expUpd := []telegram.Update{ 303 | { 304 | UpdateID: 10, 305 | Message: &telegram.Message{ 306 | Text: "message", 307 | }, 308 | }, 309 | { 310 | UpdateID: 11, 311 | Message: &telegram.Message{ 312 | Text: "message", 313 | }, 314 | }, 315 | } 316 | expMe := telegram.User{ 317 | ID: 10, 318 | Username: "test_bot", 319 | } 320 | 321 | httpmock.RegisterResponder( 322 | "POST", 323 | "https://api.telegram.org/bot_token/getMe", 324 | NewAPIResponder(200, expMe), 325 | ) 326 | 327 | b := NewWithAPI(api) 328 | 329 | handleCh := make(chan context.Context, 1) 330 | b.HandleFunc(func(ctx context.Context) error { 331 | select { 332 | case handleCh <- ctx: 333 | case <-ctx.Done(): 334 | } 335 | return nil 336 | }) 337 | 338 | whHandler, err := b.ServeByWebhook(ctx) 339 | require.NoError(t, err) 340 | 341 | { 342 | w := httptest.NewRecorder() 343 | // prepare request 344 | buf := &bytes.Buffer{} 345 | err := json.NewEncoder(buf).Encode(expUpd[0]) 346 | require.NoError(t, err) 347 | req, err := http.NewRequest("POST", "", buf) 348 | require.NoError(t, err) 349 | 350 | go whHandler(w, req) 351 | 352 | } 353 | 354 | // got first update 355 | var update1 *telegram.Update 356 | select { 357 | case handleCtx1 := <-handleCh: 358 | update1 = GetUpdate(handleCtx1) 359 | case <-ctx.Done(): 360 | require.NoError(t, ctx.Err()) 361 | } 362 | 363 | assert.Equal(t, expMe, *b.me) 364 | assert.Equal(t, expUpd[0], *update1) 365 | 366 | { 367 | w := httptest.NewRecorder() 368 | // prepare request 369 | buf := &bytes.Buffer{} 370 | err := json.NewEncoder(buf).Encode(expUpd[1]) 371 | require.NoError(t, err) 372 | req, err := http.NewRequest("POST", "", buf) 373 | require.NoError(t, err) 374 | 375 | go whHandler(w, req) 376 | 377 | } 378 | 379 | // got second update 380 | var update2 *telegram.Update 381 | select { 382 | case handleCtx1 := <-handleCh: 383 | update2 = GetUpdate(handleCtx1) 384 | case <-ctx.Done(): 385 | require.NoError(t, ctx.Err()) 386 | } 387 | 388 | assert.Equal(t, expUpd[1], *update2) 389 | 390 | } 391 | -------------------------------------------------------------------------------- /telebot/bot_test.go: -------------------------------------------------------------------------------- 1 | package telebot_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bot-api/telegram" 7 | "github.com/bot-api/telegram/telebot" 8 | "golang.org/x/net/context" 9 | "gopkg.in/stretchr/testify.v1/assert" 10 | ) 11 | 12 | func TestGetAPI(t *testing.T) { 13 | bg := context.Background() 14 | api := telegram.New("") 15 | 16 | ctx := telebot.WithAPI(bg, api) 17 | assert.NotEqual(t, bg, ctx) 18 | assert.Equal(t, api, telebot.GetAPI(ctx)) 19 | 20 | assert.Panics(t, func() { 21 | telebot.GetAPI(bg) 22 | }) 23 | } 24 | 25 | func TestGetUpdate(t *testing.T) { 26 | bg := context.Background() 27 | update := &telegram.Update{} 28 | 29 | ctx := telebot.WithUpdate(bg, update) 30 | assert.NotEqual(t, bg, ctx) 31 | assert.Equal(t, update, telebot.GetUpdate(ctx)) 32 | 33 | assert.Panics(t, func() { 34 | telebot.GetUpdate(bg) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /telebot/handlers.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "github.com/bot-api/telegram" 5 | "golang.org/x/net/context" 6 | ) 7 | 8 | // Empty does nothing. 9 | func Empty(context.Context) error { 10 | return nil 11 | } 12 | 13 | // EmptyHandler returns a handler that does nothing. 14 | func EmptyHandler() Handler { return HandlerFunc(Empty) } 15 | 16 | // StringHandler sends user a text 17 | func StringHandler(text string) Handler { 18 | return HandlerFunc(func(ctx context.Context) error { 19 | update := GetUpdate(ctx) 20 | api := GetAPI(ctx) 21 | chat := update.Chat() 22 | if chat == nil { 23 | return nil 24 | } 25 | _, err := api.SendMessage(ctx, telegram.NewMessage(chat.ID, text)) 26 | return err 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /telebot/handlers_test.go: -------------------------------------------------------------------------------- 1 | package telebot_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bot-api/telegram/telebot" 7 | "golang.org/x/net/context" 8 | "gopkg.in/stretchr/testify.v1/assert" 9 | ) 10 | 11 | func TestEmpty(t *testing.T) { 12 | ctx := context.Background() 13 | assert.Nil(t, telebot.Empty(ctx)) 14 | } 15 | 16 | func TestEmptyHandler(t *testing.T) { 17 | ctx := context.Background() 18 | assert.Nil(t, telebot.EmptyHandler().Handle(ctx)) 19 | } 20 | -------------------------------------------------------------------------------- /telebot/middleware.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "strings" 5 | 6 | "golang.org/x/net/context" 7 | ) 8 | 9 | // A Handler takes update message. 10 | type Handler interface { 11 | Handle(context.Context) error 12 | } 13 | 14 | // A Commander takes command message. 15 | type Commander interface { 16 | Command(ctx context.Context, arg string) error 17 | } 18 | 19 | // InlineCallback interface describes inline callback function. 20 | type InlineCallback interface { 21 | Callback(ctx context.Context, data string) error 22 | } 23 | 24 | type ( 25 | 26 | // MiddlewareFunc defines a function to process middleware. 27 | MiddlewareFunc func(next Handler) Handler 28 | 29 | // HandlerFunc defines a function to serve on update message. 30 | // Implements Handler interface. 31 | HandlerFunc func(context.Context) error 32 | 33 | //MessageFunc func(context.Context, *telegram.Message) error 34 | 35 | // ErrorFunc handles error, if 36 | ErrorFunc func(ctx context.Context, err error) 37 | 38 | // CommandFunc defines a function to handle commands. 39 | // Implements Commander interface. 40 | CommandFunc func(ctx context.Context, arg string) error 41 | 42 | // CallbackFunc defines a function to handle callbacks. 43 | // Implements InlineCallback interface. 44 | CallbackFunc func(ctx context.Context, data string) error 45 | ) 46 | 47 | // Handle method handles message update. 48 | func (h HandlerFunc) Handle(c context.Context) error { 49 | return h(c) 50 | } 51 | 52 | // Command method handles command on message update. 53 | func (c CommandFunc) Command(ctx context.Context, arg string) error { 54 | return c(ctx, arg) 55 | } 56 | 57 | // Callback method handles command on message update. 58 | func (c CallbackFunc) Callback(ctx context.Context, data string) error { 59 | return c(ctx, data) 60 | } 61 | 62 | // Commands middleware takes map of commands. 63 | // It runs associated Commander if update messages has a command message. 64 | // Empty command (e.x. "": Commander) used as a default Commander. 65 | // Nil command (e.x. "cmd": nil) used as an EmptyHandler 66 | // Take a look on examples/commands/main.go to know more. 67 | func Commands(commands map[string]Commander) MiddlewareFunc { 68 | return func(next Handler) Handler { 69 | return HandlerFunc(func(ctx context.Context) error { 70 | update := GetUpdate(ctx) 71 | if update.Message == nil { 72 | return next.Handle(ctx) 73 | } 74 | command, arg := update.Message.Command() 75 | if command == "" { 76 | return next.Handle(ctx) 77 | } 78 | cmd, ok := commands[command] 79 | if !ok { 80 | if cmd = commands[""]; cmd == nil { 81 | return next.Handle(ctx) 82 | } 83 | } 84 | if cmd == nil { 85 | return next.Handle(ctx) 86 | } 87 | return cmd.Command(ctx, arg) 88 | }) 89 | } 90 | } 91 | 92 | // Callbacks middleware takes map of callbacks. 93 | // It runs associated InlineCallback if update messages has a callback query. 94 | // Callback path is divided by ":". 95 | // Empty callback (e.x. "": InlineCallback) used as a default callback handler. 96 | // Nil callback (e.x. "smth": nil) used as an EmptyHandler 97 | // Take a look on examples/callbacks/main.go to know more. 98 | func Callbacks(callbacks map[string]InlineCallback) MiddlewareFunc { 99 | return func(next Handler) Handler { 100 | return HandlerFunc(func(ctx context.Context) error { 101 | update := GetUpdate(ctx) 102 | if update.CallbackQuery == nil { 103 | return next.Handle(ctx) 104 | } 105 | queryData := strings.SplitN(update.CallbackQuery.Data, ":", 2) 106 | var prefix, data string 107 | switch len(queryData) { 108 | case 2: 109 | data = queryData[1] 110 | fallthrough 111 | case 1: 112 | prefix = queryData[0] 113 | default: 114 | return next.Handle(ctx) 115 | } 116 | callback, ok := callbacks[prefix] 117 | if !ok { 118 | if callback = callbacks[""]; callback == nil { 119 | return next.Handle(ctx) 120 | } 121 | } 122 | if callback == nil { 123 | return next.Handle(ctx) 124 | } 125 | return callback.Callback(ctx, data) 126 | }) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /telebot/middleware_test.go: -------------------------------------------------------------------------------- 1 | package telebot_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/bot-api/telegram" 8 | "github.com/bot-api/telegram/telebot" 9 | "golang.org/x/net/context" 10 | "gopkg.in/stretchr/testify.v1/assert" 11 | ) 12 | 13 | func TestHandlerFunc_Handle(t *testing.T) { 14 | ctx := context.Background() 15 | err := fmt.Errorf("expected error") 16 | f := telebot.HandlerFunc(func(ctx2 context.Context) error { 17 | assert.Equal(t, ctx, ctx2) 18 | return err 19 | }) 20 | assert.Equal(t, err, f.Handle(ctx)) 21 | } 22 | 23 | func TestCommandFunc_Command(t *testing.T) { 24 | ctx := context.Background() 25 | err := fmt.Errorf("expected error") 26 | f := telebot.CommandFunc(func(ctx2 context.Context, arg string) error { 27 | assert.Equal(t, ctx, ctx2) 28 | assert.Equal(t, "arg", arg) 29 | return err 30 | }) 31 | assert.Equal(t, err, f.Command(ctx, "arg")) 32 | } 33 | 34 | func TestCommands(t *testing.T) { 35 | err := fmt.Errorf("expected error") 36 | hErr := fmt.Errorf("handler error") 37 | defErr := fmt.Errorf("default error") 38 | 39 | f := telebot.HandlerFunc(func(context.Context) error { 40 | return hErr 41 | }) 42 | cmdOne := false 43 | cmdTwo := false 44 | cmdDef := false 45 | 46 | c := telebot.Commands(map[string]telebot.Commander{ 47 | "one": telebot.CommandFunc( 48 | func(ctx context.Context, arg string) error { 49 | cmdOne = true 50 | assert.Equal(t, "oneArg", arg) 51 | return err 52 | }), 53 | "two": telebot.CommandFunc( 54 | func(ctx context.Context, arg string) error { 55 | cmdTwo = true 56 | assert.Equal(t, "", arg) 57 | return nil 58 | }), 59 | // this command pass execution to a handler 60 | "three": nil, 61 | "": telebot.CommandFunc( 62 | func(ctx context.Context, arg string) error { 63 | cmdDef = true 64 | assert.Equal(t, "def", arg) 65 | return defErr 66 | }), 67 | }) 68 | { 69 | ctx := telebot.WithUpdate(context.Background(), &telegram.Update{ 70 | Message: &telegram.Message{ 71 | Text: "/one oneArg", 72 | }, 73 | }) 74 | assert.Equal(t, c(f).Handle(ctx), err) 75 | } 76 | { 77 | ctx := telebot.WithUpdate(context.Background(), &telegram.Update{ 78 | Message: &telegram.Message{ 79 | Text: "/two", 80 | }, 81 | }) 82 | assert.Equal(t, c(f).Handle(ctx), nil) 83 | } 84 | { 85 | ctx := telebot.WithUpdate(context.Background(), &telegram.Update{ 86 | Message: &telegram.Message{ 87 | Text: "/three", 88 | }, 89 | }) 90 | assert.Equal(t, c(f).Handle(ctx), hErr) 91 | } 92 | { 93 | ctx := telebot.WithUpdate(context.Background(), &telegram.Update{ 94 | Message: &telegram.Message{ 95 | Text: "not a command", 96 | }, 97 | }) 98 | // non commands passed directly to a handler 99 | assert.Equal(t, c(f).Handle(ctx), hErr) 100 | } 101 | { 102 | ctx := telebot.WithUpdate(context.Background(), &telegram.Update{}) 103 | // non message update passed directly to a handler 104 | assert.Equal(t, c(f).Handle(ctx), hErr) 105 | } 106 | { 107 | ctx := telebot.WithUpdate(context.Background(), &telegram.Update{ 108 | Message: &telegram.Message{ 109 | Text: "/four def", 110 | }, 111 | }) 112 | // unknown commands passed to an empty commander 113 | assert.Equal(t, c(f).Handle(ctx), defErr) 114 | } 115 | assert.True(t, cmdOne, "command one hasn't been executed") 116 | assert.True(t, cmdTwo, "command two hasn't been executed") 117 | assert.True(t, cmdDef, "default command hasn't been executed") 118 | 119 | c = telebot.Commands(map[string]telebot.Commander{ 120 | "one": telebot.CommandFunc( 121 | func(ctx context.Context, arg string) error { 122 | assert.Equal(t, "oneArg twoArg", arg) 123 | return err 124 | }), 125 | }) 126 | { 127 | ctx := telebot.WithUpdate(context.Background(), &telegram.Update{ 128 | Message: &telegram.Message{ 129 | Text: "/one oneArg twoArg", 130 | }, 131 | }) 132 | assert.Equal(t, c(f).Handle(ctx), err) 133 | } 134 | { 135 | ctx := telebot.WithUpdate(context.Background(), &telegram.Update{ 136 | Message: &telegram.Message{ 137 | Text: "/two oneArg twoArg", 138 | }, 139 | }) 140 | // two commands passed directly to a handler 141 | assert.Equal(t, c(f).Handle(ctx), hErr) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /telebot/recover_middleware.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "os" 8 | "runtime" 9 | "sync" 10 | 11 | "golang.org/x/net/context" 12 | ) 13 | 14 | type ( 15 | // RecoverCfg defines the config for recover middleware. 16 | RecoverCfg struct { 17 | // StackSize is the stack size to be printed. 18 | // Optional, with default value as 4 KB. 19 | StackSize int 20 | 21 | // EnableStackAll enables formatting stack traces of all 22 | // other goroutines into buffer after the trace 23 | // for the current goroutine. 24 | // Optional, with default value as false. 25 | EnableStackAll bool 26 | 27 | // DisablePrintStack disables printing stack trace. 28 | // Optional, with default value as false. 29 | DisablePrintStack bool 30 | 31 | // LogFunc uses to write recover data to your own logger. 32 | // Please do not use stack arg after function execution, 33 | // because it will be freed. 34 | LogFunc func(ctx context.Context, cause error, stack []byte) 35 | } 36 | ) 37 | 38 | var ( 39 | // DefaultRecoverConfig is the default recover middleware config. 40 | DefaultRecoverConfig = RecoverCfg{ 41 | StackSize: 4 << 10, // 4 KB 42 | DisablePrintStack: false, 43 | } 44 | // DefaultRecoverLogger is used to print recover information 45 | // if RecoverCfg.LogFunc is not set 46 | DefaultRecoverLogger = log.New(os.Stderr, "", log.LstdFlags) 47 | ) 48 | 49 | // Recover returns a middleware which recovers from panics anywhere in the chain 50 | // and returns nil error. 51 | // It prints recovery information to standard log. 52 | func Recover() MiddlewareFunc { 53 | return RecoverWithConfig(DefaultRecoverConfig) 54 | } 55 | 56 | // RecoverWithConfig returns a middleware which recovers from panics anywhere 57 | // in the chain and returns nil error. 58 | // It takes RecoverCfg to configure itself. 59 | func RecoverWithConfig(cfg RecoverCfg) MiddlewareFunc { 60 | // Defaults 61 | if cfg.StackSize == 0 { 62 | cfg.StackSize = DefaultRecoverConfig.StackSize 63 | } 64 | 65 | var logFunc func(ctx context.Context, cause error, stack []byte) 66 | if cfg.LogFunc != nil { 67 | logFunc = cfg.LogFunc 68 | } else { 69 | logFunc = defaultLogFunc 70 | } 71 | 72 | var stackPool = &sync.Pool{ 73 | New: func() interface{} { 74 | return make([]byte, cfg.StackSize) 75 | }, 76 | } 77 | 78 | // getBuffer returns a buffer from the pool. 79 | getBuffer := func() (buf []byte) { 80 | return stackPool.Get().([]byte) 81 | } 82 | 83 | // putBuffer returns a buffer to the pool. 84 | // The buffer is reset before it is put back into circulation. 85 | putBuffer := func(buf []byte) { 86 | stackPool.Put(buf) 87 | } 88 | 89 | return func(next Handler) Handler { 90 | return HandlerFunc(func(ctx context.Context) error { 91 | defer func() { 92 | r := recover() 93 | if r == nil { 94 | return 95 | } 96 | var err error 97 | switch r := r.(type) { 98 | case error: 99 | err = r 100 | default: 101 | err = fmt.Errorf("%v", r) 102 | } 103 | var stackBuf []byte 104 | if !cfg.DisablePrintStack { 105 | stackBuf = getBuffer() 106 | length := runtime.Stack( 107 | stackBuf, cfg.EnableStackAll) 108 | 109 | stackBuf = stackBuf[:length] 110 | } 111 | logFunc(ctx, err, stackBuf) 112 | if stackBuf != nil { 113 | putBuffer(stackBuf) 114 | } 115 | 116 | }() 117 | return next.Handle(ctx) 118 | }) 119 | } 120 | } 121 | 122 | func defaultLogFunc(ctx context.Context, cause error, stack []byte) { 123 | buf := bytes.NewBufferString("PANIC RECOVER") 124 | buf.WriteString("\ncause:") 125 | buf.WriteString(cause.Error()) 126 | if stack != nil { 127 | buf.WriteString("\nstack:") 128 | buf.Write(stack) 129 | } 130 | DefaultRecoverLogger.Print(buf.String()) 131 | } 132 | -------------------------------------------------------------------------------- /telebot/recover_middleware_test.go: -------------------------------------------------------------------------------- 1 | package telebot_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/bot-api/telegram/telebot" 9 | "golang.org/x/net/context" 10 | "gopkg.in/stretchr/testify.v1/assert" 11 | ) 12 | 13 | func TestRecoverWithConfig(t *testing.T) { 14 | var ( 15 | actCause error 16 | actStack string 17 | ) 18 | ctx := context.Background() 19 | expErr := fmt.Errorf("expected error") 20 | 21 | buf := &bytes.Buffer{} 22 | telebot.DefaultRecoverLogger.SetOutput(buf) 23 | 24 | errHandler := telebot.HandlerFunc(func(ctx context.Context) error { 25 | return expErr 26 | }) 27 | 28 | logFunc := func(ctx context.Context, cause error, stack []byte) { 29 | actCause = cause 30 | actStack = string(stack) // we must copy stack 31 | } 32 | 33 | m := telebot.Recover() 34 | { 35 | err := m(errHandler).Handle(ctx) 36 | // no panic just passed 37 | assert.Equal(t, expErr, err) 38 | assert.Equal(t, 0, buf.Len()) 39 | } 40 | { 41 | err := m(telebot.HandlerFunc(func(ctx context.Context) error { 42 | panic("whatever") 43 | })).Handle(ctx) 44 | // no panic just passed, recovery info printed to default logger 45 | assert.Nil(t, err) 46 | assert.NotEqual(t, 0, buf.Len()) 47 | stack := buf.String() 48 | assert.Contains(t, stack, "whatever") 49 | assert.Contains(t, stack, "bot.RecoverWithConfig") 50 | assert.Contains(t, stack, "recover_middleware.go") 51 | } 52 | 53 | m = telebot.RecoverWithConfig(telebot.RecoverCfg{ 54 | LogFunc: logFunc, 55 | }) 56 | { 57 | err := m(telebot.HandlerFunc(func(ctx context.Context) error { 58 | panic(expErr) 59 | })).Handle(ctx) 60 | // no panic just passed 61 | assert.Nil(t, err) 62 | assert.Equal(t, expErr, actCause) 63 | assert.Contains(t, actStack, "bot.RecoverWithConfig") 64 | assert.Contains(t, actStack, "recover_middleware.go") 65 | 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /telebot/session_middleware.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "bytes" 5 | 6 | "encoding/json" 7 | 8 | "golang.org/x/net/context" 9 | ) 10 | 11 | type sessionKey struct{} 12 | 13 | // SessionData describes key:value data 14 | type SessionData map[string]interface{} 15 | 16 | // UpdateFunc describes a func to update session data 17 | type UpdateFunc func(data []byte) error 18 | 19 | // SessionConfig helps to configure Session Middleware 20 | type SessionConfig struct { 21 | // Encode takes SessionData and return encoded version 22 | Encode func(interface{}) ([]byte, error) 23 | // Decode takes encoded session data and fill dst struct 24 | Decode func(data []byte, dst interface{}) error 25 | // GetSession function should receive request context and return 26 | // []bytes of current session and UpdateFunc 27 | // that is invoked if session is modified 28 | GetSession func(context.Context) ([]byte, UpdateFunc, error) 29 | } 30 | 31 | // GetSession returns SessionData or nil for current context 32 | func GetSession(ctx context.Context) SessionData { 33 | if item, ok := ctx.Value(sessionKey{}).(SessionData); ok { 34 | return item 35 | } 36 | return nil 37 | } 38 | 39 | // WithSession returns a new context with SessionData inside. 40 | func WithSession(ctx context.Context, item SessionData) context.Context { 41 | return context.WithValue(ctx, sessionKey{}, item) 42 | } 43 | 44 | // Session is a default middleware to work with sessions. 45 | // getSession function should receive request context and return 46 | // []bytes of current session, UpdateFunc that is invoked if session is modified 47 | // error if something goes wrong. 48 | func Session(getSession func(context.Context) ([]byte, UpdateFunc, error)) MiddlewareFunc { 49 | return SessionWithConfig(SessionConfig{ 50 | GetSession: getSession, 51 | }) 52 | } 53 | 54 | // SessionWithConfig takes SessionConfig and returns SessionMiddleware 55 | func SessionWithConfig(cfg SessionConfig) MiddlewareFunc { 56 | encode := cfg.Encode 57 | if encode == nil { 58 | encode = json.Marshal 59 | } 60 | decode := cfg.Decode 61 | if decode == nil { 62 | decode = json.Unmarshal 63 | } 64 | 65 | return func(next Handler) Handler { 66 | return HandlerFunc(func(ctx context.Context) error { 67 | sessionBytes, update, err := cfg.GetSession(ctx) 68 | if err != nil { 69 | return err 70 | } 71 | sessionData := SessionData{} 72 | if sessionBytes != nil && len(sessionBytes) > 0 { 73 | err = decode(sessionBytes, &sessionData) 74 | if err != nil { 75 | return err 76 | } 77 | } 78 | 79 | ctx = WithSession(ctx, sessionData) 80 | defer func() { 81 | data, err := encode(sessionData) 82 | if err != nil { 83 | return 84 | } 85 | if !bytes.Equal(data, sessionBytes) { 86 | err := update(data) 87 | if err != nil { 88 | return 89 | } 90 | } 91 | }() 92 | err = next.Handle(ctx) 93 | return err 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /testdata/files/AgADAQADracxG87oCAwifY52FDYYdyoW2ykABBedWvJj470nY0wBAAEC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bot-api/telegram/b7abf87c449e690eb3563f53987186c95702c49f/testdata/files/AgADAQADracxG87oCAwifY52FDYYdyoW2ykABBedWvJj470nY0wBAAEC.jpg -------------------------------------------------------------------------------- /testdata/files/AgADAQADracxG87oCAwifY52FDYYdyoW2ykABEFzMZ03BS96ZUwBAAEC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bot-api/telegram/b7abf87c449e690eb3563f53987186c95702c49f/testdata/files/AgADAQADracxG87oCAwifY52FDYYdyoW2ykABEFzMZ03BS96ZUwBAAEC.jpg -------------------------------------------------------------------------------- /testdata/files/AgADAQADracxG87oCAwifY52FDYYdyoW2ykABL6tT4iSzvK7ZEwBAAEC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bot-api/telegram/b7abf87c449e690eb3563f53987186c95702c49f/testdata/files/AgADAQADracxG87oCAwifY52FDYYdyoW2ykABL6tT4iSzvK7ZEwBAAEC.jpg -------------------------------------------------------------------------------- /testdata/files/AgADAQADracxG87oCAwifY52FDYYdyoW2ykABPxYFPTyioocYkwBAAEC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bot-api/telegram/b7abf87c449e690eb3563f53987186c95702c49f/testdata/files/AgADAQADracxG87oCAwifY52FDYYdyoW2ykABPxYFPTyioocYkwBAAEC.jpg -------------------------------------------------------------------------------- /testdata/integration_user_profile_photos.json: -------------------------------------------------------------------------------- 1 | { 2 | "total_count": 1, 3 | "photos": [ 4 | [ 5 | { 6 | "file_id": "AgADAQADracxG87oCAwifY52FDYYdyoW2ykABPxYFPTyioocYkwBAAEC", 7 | "file_size": 7920, 8 | "width": 160, 9 | "height": 160 10 | }, 11 | { 12 | "file_id": "AgADAQADracxG87oCAwifY52FDYYdyoW2ykABBedWvJj470nY0wBAAEC", 13 | "file_size": 18175, 14 | "width": 320, 15 | "height": 320 16 | }, 17 | { 18 | "file_id": "AgADAQADracxG87oCAwifY52FDYYdyoW2ykABL6tT4iSzvK7ZEwBAAEC", 19 | "file_size": 45515, 20 | "width": 640, 21 | "height": 640 22 | }, 23 | { 24 | "file_id": "AgADAQADracxG87oCAwifY52FDYYdyoW2ykABEFzMZ03BS96ZUwBAAEC", 25 | "file_size": 48267, 26 | "width": 900, 27 | "height": 900 28 | } 29 | ] 30 | ] 31 | } -------------------------------------------------------------------------------- /testutils/mocks.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/url" 7 | 8 | "github.com/bot-api/telegram" 9 | "github.com/m0sth8/httpmock" 10 | "golang.org/x/net/context" 11 | ) 12 | 13 | // UpdatesResponder provides a useful responder that returns updates from ch 14 | func UpdatesResponder(ctx context.Context, ch <-chan []telegram.Update) httpmock.Responder { 15 | return func(req *http.Request) (*http.Response, error) { 16 | select { 17 | case <-ctx.Done(): 18 | return nil, ctx.Err() 19 | case update := <-ch: 20 | data, err := json.Marshal(update) 21 | if err != nil { 22 | return httpmock.NewJsonResponse(200, 23 | telegram.APIResponse{ 24 | Ok: false, 25 | Description: err.Error(), 26 | }, 27 | ) 28 | } 29 | raw := json.RawMessage(data) 30 | return httpmock.NewJsonResponse(200, 31 | telegram.APIResponse{ 32 | Ok: true, 33 | Result: &raw, 34 | }, 35 | ) 36 | } 37 | } 38 | } 39 | 40 | // ReceivedMessage contains information about message that are sent by api 41 | type ReceivedMessage struct { 42 | Values url.Values 43 | URL url.URL 44 | } 45 | 46 | // SendResponder helps to test telegram api. 47 | // Returns received message to out channel, 48 | // Waits for result channel with object to return in API. 49 | // If result channel is nil, then returns empty telegram.Message. 50 | // If result channel has error, then returns APIResponse with Ok False and Description. 51 | func SendResponder(ctx context.Context, 52 | result <-chan interface{}) (httpmock.Responder, <-chan ReceivedMessage) { 53 | out := make(chan ReceivedMessage) 54 | return func(req *http.Request) (*http.Response, error) { 55 | req.ParseForm() 56 | select { 57 | case out <- ReceivedMessage{ 58 | Values: req.Form, 59 | URL: *req.URL, 60 | }: 61 | case <-ctx.Done(): 62 | return nil, ctx.Err() 63 | } 64 | select { 65 | case data := <-result: 66 | if err, casted := data.(error); casted { 67 | return httpmock.NewJsonResponse(200, 68 | telegram.APIResponse{ 69 | Ok: false, 70 | Description: err.Error(), 71 | }, 72 | ) 73 | } 74 | rawData, err := json.Marshal(data) 75 | if err != nil { 76 | return nil, err 77 | } 78 | raw := json.RawMessage(rawData) 79 | return httpmock.NewJsonResponse(200, 80 | telegram.APIResponse{ 81 | Ok: true, 82 | Result: &raw, 83 | }, 84 | ) 85 | case <-ctx.Done(): 86 | return nil, ctx.Err() 87 | } 88 | }, out 89 | } 90 | -------------------------------------------------------------------------------- /types_inline.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | // InlineQuery is an incoming inline query. When the user sends 4 | // an empty query, your bot could return some default or 5 | // trending results. 6 | type InlineQuery struct { 7 | // ID is a unique identifier for this query. 8 | ID string `json:"id"` 9 | // From is a sender. 10 | From User `json:"from"` 11 | // Sender location, only for bots that request user location. 12 | // Optional. 13 | Location *Location `json:"location,omitempty"` 14 | // Query is a text of the query. 15 | Query string `json:"query"` 16 | // Offset of the results to be returned, can be controlled by the bot. 17 | Offset string `json:"offset"` 18 | } 19 | 20 | // ChosenInlineResult represents a result of an inline query 21 | // that was chosen by the user and sent to their chat partner 22 | type ChosenInlineResult struct { 23 | // ResultID is a unique identifier for the result that was chosen. 24 | ResultID string `json:"result_id"` 25 | // From is a user that chose the result. 26 | From User `json:"from"` 27 | // Query is used to obtain the result. 28 | Query string `json:"query"` 29 | } 30 | 31 | // A MarkInlineQueryResult implements InlineQueryResult interface. 32 | // You can mark your structures with this object. 33 | type MarkInlineQueryResult struct{} 34 | 35 | // InlineQueryResult is a fake method that helps to identify implementations 36 | func (MarkInlineQueryResult) InlineQueryResult() {} 37 | 38 | // BaseInlineQueryResult is a base class for InlineQueryResult 39 | type BaseInlineQueryResult struct { 40 | Type string `json:"type"` // required 41 | ID string `json:"id"` // required 42 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 43 | // ReplyMarkup supports only InlineKeyboardMarkup for InlineQueryResult 44 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 45 | } 46 | 47 | // InlineThumb struct helps to describe thumbnail. 48 | type InlineThumb struct { 49 | ThumbURL string `json:"thumb_url,omitempty"` 50 | ThumbWidth int `json:"thumb_width,omitempty"` 51 | ThumbHeight int `json:"thumb_height,omitempty"` 52 | } 53 | 54 | // InlineQueryResultArticle is an inline query response article. 55 | type InlineQueryResultArticle struct { 56 | MarkInlineQueryResult 57 | BaseInlineQueryResult 58 | InlineThumb 59 | 60 | Title string `json:"title"` // required 61 | URL string `json:"url,omitempty"` 62 | // Optional. Pass True, if you don't want the URL 63 | // to be shown in the message 64 | HideURL bool `json:"hide_url,omitempty"` 65 | Description string `json:"description,omitempty"` 66 | } 67 | 68 | // InlineQueryResultPhoto is an inline query response photo. 69 | type InlineQueryResultPhoto struct { 70 | MarkInlineQueryResult 71 | BaseInlineQueryResult 72 | InlineThumb 73 | 74 | PhotoURL string `json:"photo_url"` // required 75 | PhotoWidth int `json:"photo_width,omitempty"` 76 | PhotoHeight int `json:"photo_height,omitempty"` 77 | MimeType string `json:"mime_type,omitempty"` 78 | Title string `json:"title,omitempty"` 79 | Description string `json:"description,omitempty"` 80 | Caption string `json:"caption,omitempty"` 81 | } 82 | 83 | // InlineQueryResultGIF is an inline query response GIF. 84 | type InlineQueryResultGIF struct { 85 | MarkInlineQueryResult 86 | BaseInlineQueryResult 87 | InlineThumb 88 | 89 | // A valid URL for the GIF file. File size must not exceed 1MB 90 | GifURL string `json:"gif_url"` // required 91 | GifWidth int `json:"gif_width,omitempty"` 92 | GifHeight int `json:"gif_height,omitempty"` 93 | Title string `json:"title,omitempty"` 94 | Caption string `json:"caption,omitempty"` 95 | } 96 | 97 | // InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF. 98 | type InlineQueryResultMPEG4GIF struct { 99 | MarkInlineQueryResult 100 | BaseInlineQueryResult 101 | 102 | MPEG4URL string `json:"mpeg4_url"` // required 103 | MPEG4Width int `json:"mpeg4_width,omitempty"` 104 | MPEG4Height int `json:"mpeg4_height,omitempty"` 105 | Title string `json:"title,omitempty"` 106 | Caption string `json:"caption,omitempty"` 107 | } 108 | 109 | // InlineQueryResultVideo is an inline query response video. 110 | type InlineQueryResultVideo struct { 111 | MarkInlineQueryResult 112 | BaseInlineQueryResult 113 | InlineThumb 114 | 115 | VideoURL string `json:"video_url"` // required 116 | MimeType string `json:"mime_type"` // required 117 | Title string `json:"title,omitempty"` 118 | Caption string `json:"caption,omitempty"` 119 | VideoWidth int `json:"video_width,omitempty"` 120 | VideoHeight int `json:"video_height,omitempty"` 121 | VideoDuration int `json:"video_duration,omitempty"` 122 | Description string `json:"description,omitempty"` 123 | } 124 | 125 | // InlineQueryResultAudio is an inline query response audio. 126 | type InlineQueryResultAudio struct { 127 | MarkInlineQueryResult 128 | BaseInlineQueryResult 129 | 130 | AudioURL string `json:"audio_url"` // required 131 | Title string `json:"title"` // required 132 | Performer string `json:"performer"` 133 | AudioDuration int `json:"audio_duration"` 134 | } 135 | 136 | // InlineQueryResultVoice is an inline query response voice. 137 | type InlineQueryResultVoice struct { 138 | MarkInlineQueryResult 139 | BaseInlineQueryResult 140 | 141 | VoiceURL string `json:"voice_url"` // required 142 | Title string `json:"title"` // required 143 | Duration int `json:"voice_duration,omitempty"` 144 | } 145 | 146 | // InlineQueryResultDocument is an inline query response document. 147 | type InlineQueryResultDocument struct { 148 | MarkInlineQueryResult 149 | BaseInlineQueryResult 150 | InlineThumb 151 | 152 | DocumentURL string `json:"document_url"` // required 153 | // Mime type of the content of the file, 154 | // either “application/pdf” or “application/zip” 155 | MimeType string `json:"mime_type"` // required 156 | // Title for the result 157 | Title string `json:"title"` // required 158 | // Optional. Caption of the document to be sent, 0-200 characters 159 | Caption string `json:"caption,omitempty"` 160 | // Optional. Short description of the result 161 | Description string `json:"description,omitempty"` 162 | } 163 | 164 | // InlineQueryResultLocation is an inline query response location. 165 | type InlineQueryResultLocation struct { 166 | MarkInlineQueryResult 167 | BaseInlineQueryResult 168 | InlineThumb 169 | 170 | Latitude float64 `json:"latitude"` // required 171 | Longitude float64 `json:"longitude"` // required 172 | Title string `json:"title"` // required 173 | } 174 | 175 | // InlineQueryResultContact represents a contact with a phone number. 176 | // By default, this contact will be sent by the user. 177 | // Alternatively, you can use input_message_content 178 | // to send a message with the specified content instead of the contact. 179 | type InlineQueryResultContact struct { 180 | MarkInlineQueryResult 181 | BaseInlineQueryResult 182 | InlineThumb 183 | Contact 184 | } 185 | 186 | // InlineQueryResultVenue represents a venue. 187 | // By default, the venue will be sent by the user. 188 | // Alternatively, you can use input_message_content 189 | // to send a message with the specified content instead of the venue. 190 | type InlineQueryResultVenue struct { 191 | MarkInlineQueryResult 192 | BaseInlineQueryResult 193 | InlineThumb 194 | Venue 195 | } 196 | 197 | // A MarkInputMessageContent implements InputMessageContent interface. 198 | // You can mark your structures with this object. 199 | type MarkInputMessageContent struct{} 200 | 201 | // InputMessageContent is a fake method that helps to identify implementations 202 | func (MarkInputMessageContent) InputMessageContent() {} 203 | 204 | // InputTextMessageContent represents the content of a text message 205 | // to be sent as the result of an inline query. 206 | type InputTextMessageContent struct { 207 | MarkInputMessageContent 208 | 209 | // Text of the message to be sent, 1‐4096 characters 210 | MessageText string `json:"message_text"` 211 | // Send Markdown or HTML, if you want Telegram apps to show 212 | // bold, italic, fixed‐width text or inline URLs in your bot's message. 213 | // Use Mode constants. Optional. 214 | ParseMode string `json:"parse_mode,omitempty"` 215 | // Disables link previews for links in this message. 216 | DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` 217 | } 218 | 219 | // InputLocationMessageContent contains a location for displaying 220 | // as an inline query result. 221 | // Implements InputMessageContent 222 | type InputLocationMessageContent struct { 223 | MarkInputMessageContent 224 | Location 225 | } 226 | 227 | // InputVenueMessageContent contains a venue for displaying 228 | // as an inline query result. 229 | // Implements InputMessageContent 230 | type InputVenueMessageContent struct { 231 | MarkInputMessageContent 232 | Venue 233 | } 234 | 235 | // InputContactMessageContent contains a contact for displaying 236 | // as an inline query result. 237 | // Implements InputMessageContent 238 | type InputContactMessageContent struct { 239 | MarkInputMessageContent 240 | Contact 241 | } 242 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "net/url" 5 | "regexp" 6 | ) 7 | 8 | var tokenRegex = regexp.MustCompile(`^[\d]{3,11}:[\w-]{35}$`) 9 | 10 | // IsValidToken returns true if token is a valid telegram bot token 11 | // 12 | // Token format is like: 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsawq 13 | func IsValidToken(token string) bool { 14 | return tokenRegex.MatchString(token) 15 | } 16 | 17 | // ========== Internal functions 18 | 19 | func updateValues(to, from url.Values) { 20 | for key, values := range from { 21 | for _, value := range values { 22 | to.Add(key, value) 23 | } 24 | } 25 | } 26 | 27 | func updateValuesWithPrefix(to, from url.Values, prefix string) { 28 | for key, values := range from { 29 | for _, value := range values { 30 | to.Add(prefix+key, value) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package telegram_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bot-api/telegram" 7 | "gopkg.in/stretchr/testify.v1/assert" 8 | ) 9 | 10 | func TestIsValidToken(t *testing.T) { 11 | testTable := []struct { 12 | token string 13 | result bool 14 | }{ 15 | {"110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsawq", true}, 16 | {"110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaq", false}, 17 | {"113:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsawq", true}, 18 | {"12345678901:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsawq", true}, 19 | {"1234567890123:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsawq", false}, 20 | {"110201543:AAHdqTcvCH1vGWJxf-eofSAs0K5PALDsawq", true}, 21 | } 22 | for i, tt := range testTable { 23 | t.Logf("test #%d", i) 24 | assert.Equal(t, tt.result, telegram.IsValidToken(tt.token)) 25 | } 26 | } 27 | --------------------------------------------------------------------------------