all repos — telegram-bot-api @ 816532053bbc99773ad9a3684527d6358660b0a4

Golang bindings for the Telegram Bot API

bot.go (view raw)

  1// Package tgbotapi has functions and types used for interacting with
  2// the Telegram Bot API.
  3package tgbotapi
  4
  5import (
  6	"encoding/json"
  7	"errors"
  8	"fmt"
  9	"io"
 10	"io/ioutil"
 11	"mime/multipart"
 12	"net/http"
 13	"net/url"
 14	"strings"
 15	"time"
 16)
 17
 18// HTTPClient is the type needed for the bot to perform HTTP requests.
 19type HTTPClient interface {
 20	Do(req *http.Request) (*http.Response, error)
 21	PostForm(url string, data url.Values) (*http.Response, error)
 22}
 23
 24// BotAPI allows you to interact with the Telegram Bot API.
 25type BotAPI struct {
 26	Token  string `json:"token"`
 27	Debug  bool   `json:"debug"`
 28	Buffer int    `json:"buffer"`
 29
 30	Self            User       `json:"-"`
 31	Client          HTTPClient `json:"-"`
 32	shutdownChannel chan interface{}
 33
 34	apiEndpoint string
 35}
 36
 37// NewBotAPI creates a new BotAPI instance.
 38//
 39// It requires a token, provided by @BotFather on Telegram.
 40func NewBotAPI(token string) (*BotAPI, error) {
 41	return NewBotAPIWithClient(token, APIEndpoint, &http.Client{})
 42}
 43
 44// NewBotAPIWithAPIEndpoint creates a new BotAPI instance
 45// and allows you to pass API endpoint.
 46//
 47// It requires a token, provided by @BotFather on Telegram and API endpoint.
 48func NewBotAPIWithAPIEndpoint(token, apiEndpoint string) (*BotAPI, error) {
 49	return NewBotAPIWithClient(token, apiEndpoint, &http.Client{})
 50}
 51
 52// NewBotAPIWithClient creates a new BotAPI instance
 53// and allows you to pass a http.Client.
 54//
 55// It requires a token, provided by @BotFather on Telegram and API endpoint.
 56func NewBotAPIWithClient(token, apiEndpoint string, client HTTPClient) (*BotAPI, error) {
 57	bot := &BotAPI{
 58		Token:           token,
 59		Client:          client,
 60		Buffer:          100,
 61		shutdownChannel: make(chan interface{}),
 62
 63		apiEndpoint: apiEndpoint,
 64	}
 65
 66	self, err := bot.GetMe()
 67	if err != nil {
 68		return nil, err
 69	}
 70
 71	bot.Self = self
 72
 73	return bot, nil
 74}
 75
 76// SetAPIEndpoint changes the Telegram Bot API endpoint used by the instance.
 77func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) {
 78	bot.apiEndpoint = apiEndpoint
 79}
 80
 81func buildParams(in Params) (out url.Values) {
 82	if in == nil {
 83		return url.Values{}
 84	}
 85
 86	out = url.Values{}
 87
 88	for key, value := range in {
 89		out.Set(key, value)
 90	}
 91
 92	return
 93}
 94
 95// MakeRequest makes a request to a specific endpoint with our token.
 96func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) {
 97	if bot.Debug {
 98		log.Printf("Endpoint: %s, params: %v\n", endpoint, params)
 99	}
100
101	method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
102
103	values := buildParams(params)
104
105	resp, err := bot.Client.PostForm(method, values)
106	if err != nil {
107		return nil, err
108	}
109	defer resp.Body.Close()
110
111	var apiResp APIResponse
112	bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
113	if err != nil {
114		return &apiResp, err
115	}
116
117	if bot.Debug {
118		log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
119	}
120
121	if !apiResp.Ok {
122		var parameters ResponseParameters
123
124		if apiResp.Parameters != nil {
125			parameters = *apiResp.Parameters
126		}
127
128		return &apiResp, &Error{
129			Code:               apiResp.ErrorCode,
130			Message:            apiResp.Description,
131			ResponseParameters: parameters,
132		}
133	}
134
135	return &apiResp, nil
136}
137
138// decodeAPIResponse decode response and return slice of bytes if debug enabled.
139// If debug disabled, just decode http.Response.Body stream to APIResponse struct
140// for efficient memory usage
141func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) (_ []byte, err error) {
142	if !bot.Debug {
143		dec := json.NewDecoder(responseBody)
144		err = dec.Decode(resp)
145		return
146	}
147
148	// if debug, read reponse body
149	data, err := ioutil.ReadAll(responseBody)
150	if err != nil {
151		return
152	}
153
154	err = json.Unmarshal(data, resp)
155	if err != nil {
156		return
157	}
158
159	return data, nil
160}
161
162// UploadFiles makes a request to the API with files.
163func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFile) (*APIResponse, error) {
164	r, w := io.Pipe()
165	m := multipart.NewWriter(w)
166
167	// This code modified from the very helpful @HirbodBehnam
168	// https://github.com/go-telegram-bot-api/telegram-bot-api/issues/354#issuecomment-663856473
169	go func() {
170		defer w.Close()
171		defer m.Close()
172
173		for field, value := range params {
174			if err := m.WriteField(field, value); err != nil {
175				w.CloseWithError(err)
176				return
177			}
178		}
179
180		for _, file := range files {
181			if file.Data.NeedsUpload() {
182				name, reader, err := file.Data.UploadData()
183				if err != nil {
184					w.CloseWithError(err)
185					return
186				}
187
188				part, err := m.CreateFormFile(file.Name, name)
189				if err != nil {
190					w.CloseWithError(err)
191					return
192				}
193
194				if _, err := io.Copy(part, reader); err != nil {
195					w.CloseWithError(err)
196					return
197				}
198
199				if closer, ok := reader.(io.ReadCloser); ok {
200					if err = closer.Close(); err != nil {
201						w.CloseWithError(err)
202						return
203					}
204				}
205			} else {
206				value := file.Data.SendData()
207
208				if err := m.WriteField(file.Name, value); err != nil {
209					w.CloseWithError(err)
210					return
211				}
212			}
213		}
214	}()
215
216	if bot.Debug {
217		log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files))
218	}
219
220	method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
221
222	req, err := http.NewRequest("POST", method, r)
223	if err != nil {
224		return nil, err
225	}
226
227	req.Header.Set("Content-Type", m.FormDataContentType())
228
229	resp, err := bot.Client.Do(req)
230	if err != nil {
231		return nil, err
232	}
233	defer resp.Body.Close()
234
235	var apiResp APIResponse
236	bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
237	if err != nil {
238		return &apiResp, err
239	}
240
241	if bot.Debug {
242		log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
243	}
244
245	if !apiResp.Ok {
246		var parameters ResponseParameters
247
248		if apiResp.Parameters != nil {
249			parameters = *apiResp.Parameters
250		}
251
252		return &apiResp, &Error{
253			Message:            apiResp.Description,
254			ResponseParameters: parameters,
255		}
256	}
257
258	return &apiResp, nil
259}
260
261// GetFileDirectURL returns direct URL to file
262//
263// It requires the FileID.
264func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
265	file, err := bot.GetFile(FileConfig{fileID})
266
267	if err != nil {
268		return "", err
269	}
270
271	return file.Link(bot.Token), nil
272}
273
274// GetMe fetches the currently authenticated bot.
275//
276// This method is called upon creation to validate the token,
277// and so you may get this data from BotAPI.Self without the need for
278// another request.
279func (bot *BotAPI) GetMe() (User, error) {
280	resp, err := bot.MakeRequest("getMe", nil)
281	if err != nil {
282		return User{}, err
283	}
284
285	var user User
286	err = json.Unmarshal(resp.Result, &user)
287
288	return user, err
289}
290
291// IsMessageToMe returns true if message directed to this bot.
292//
293// It requires the Message.
294func (bot *BotAPI) IsMessageToMe(message Message) bool {
295	return strings.Contains(message.Text, "@"+bot.Self.UserName)
296}
297
298func hasFilesNeedingUpload(files []RequestFile) bool {
299	for _, file := range files {
300		if file.Data.NeedsUpload() {
301			return true
302		}
303	}
304
305	return false
306}
307
308// Request sends a Chattable to Telegram, and returns the APIResponse.
309func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) {
310	params, err := c.params()
311	if err != nil {
312		return nil, err
313	}
314
315	if t, ok := c.(Fileable); ok {
316		files := t.files()
317
318		// If we have files that need to be uploaded, we should delegate the
319		// request to UploadFile.
320		if hasFilesNeedingUpload(files) {
321			return bot.UploadFiles(t.method(), params, files)
322		}
323
324		// However, if there are no files to be uploaded, there's likely things
325		// that need to be turned into params instead.
326		for _, file := range files {
327			params[file.Name] = file.Data.SendData()
328		}
329	}
330
331	return bot.MakeRequest(c.method(), params)
332}
333
334// Send will send a Chattable item to Telegram and provides the
335// returned Message.
336func (bot *BotAPI) Send(c Chattable) (Message, error) {
337	resp, err := bot.Request(c)
338	if err != nil {
339		return Message{}, err
340	}
341
342	var message Message
343	err = json.Unmarshal(resp.Result, &message)
344
345	return message, err
346}
347
348// SendMediaGroup sends a media group and returns the resulting messages.
349func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {
350	resp, err := bot.Request(config)
351	if err != nil {
352		return nil, err
353	}
354
355	var messages []Message
356	err = json.Unmarshal(resp.Result, &messages)
357
358	return messages, err
359}
360
361// GetUserProfilePhotos gets a user's profile photos.
362//
363// It requires UserID.
364// Offset and Limit are optional.
365func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
366	resp, err := bot.Request(config)
367	if err != nil {
368		return UserProfilePhotos{}, err
369	}
370
371	var profilePhotos UserProfilePhotos
372	err = json.Unmarshal(resp.Result, &profilePhotos)
373
374	return profilePhotos, err
375}
376
377// GetFile returns a File which can download a file from Telegram.
378//
379// Requires FileID.
380func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
381	resp, err := bot.Request(config)
382	if err != nil {
383		return File{}, err
384	}
385
386	var file File
387	err = json.Unmarshal(resp.Result, &file)
388
389	return file, err
390}
391
392// GetUpdates fetches updates.
393// If a WebHook is set, this will not return any data!
394//
395// Offset, Limit, Timeout, and AllowedUpdates are optional.
396// To avoid stale items, set Offset to one higher than the previous item.
397// Set Timeout to a large number to reduce requests so you can get updates
398// instantly instead of having to wait between requests.
399func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
400	resp, err := bot.Request(config)
401	if err != nil {
402		return []Update{}, err
403	}
404
405	var updates []Update
406	err = json.Unmarshal(resp.Result, &updates)
407
408	return updates, err
409}
410
411// GetWebhookInfo allows you to fetch information about a webhook and if
412// one currently is set, along with pending update count and error messages.
413func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
414	resp, err := bot.MakeRequest("getWebhookInfo", nil)
415	if err != nil {
416		return WebhookInfo{}, err
417	}
418
419	var info WebhookInfo
420	err = json.Unmarshal(resp.Result, &info)
421
422	return info, err
423}
424
425// GetUpdatesChan starts and returns a channel for getting updates.
426func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
427	ch := make(chan Update, bot.Buffer)
428
429	go func() {
430		for {
431			select {
432			case <-bot.shutdownChannel:
433				close(ch)
434				return
435			default:
436			}
437
438			updates, err := bot.GetUpdates(config)
439			if err != nil {
440				log.Println(err)
441				log.Println("Failed to get updates, retrying in 3 seconds...")
442				time.Sleep(time.Second * 3)
443
444				continue
445			}
446
447			for _, update := range updates {
448				if update.UpdateID >= config.Offset {
449					config.Offset = update.UpdateID + 1
450					ch <- update
451				}
452			}
453		}
454	}()
455
456	return ch
457}
458
459// StopReceivingUpdates stops the go routine which receives updates
460func (bot *BotAPI) StopReceivingUpdates() {
461	if bot.Debug {
462		log.Println("Stopping the update receiver routine...")
463	}
464	close(bot.shutdownChannel)
465}
466
467// ListenForWebhook registers a http handler for a webhook.
468func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
469	ch := make(chan Update, bot.Buffer)
470
471	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
472		update, err := bot.HandleUpdate(r)
473		if err != nil {
474			errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
475			w.WriteHeader(http.StatusBadRequest)
476			w.Header().Set("Content-Type", "application/json")
477			_, _ = w.Write(errMsg)
478			return
479		}
480
481		ch <- *update
482	})
483
484	return ch
485}
486
487// HandleUpdate parses and returns update received via webhook
488func (bot *BotAPI) HandleUpdate(r *http.Request) (*Update, error) {
489	if r.Method != http.MethodPost {
490		err := errors.New("wrong HTTP method required POST")
491		return nil, err
492	}
493
494	var update Update
495	err := json.NewDecoder(r.Body).Decode(&update)
496	if err != nil {
497		return nil, err
498	}
499
500	return &update, nil
501}
502
503// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
504//
505// It doesn't support uploading files.
506//
507// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
508// for details.
509func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
510	params, err := c.params()
511	if err != nil {
512		return err
513	}
514
515	if t, ok := c.(Fileable); ok {
516		if hasFilesNeedingUpload(t.files()) {
517			return errors.New("unable to use http response to upload files")
518		}
519	}
520
521	values := buildParams(params)
522	values.Set("method", c.method())
523
524	w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
525	_, err = w.Write([]byte(values.Encode()))
526	return err
527}
528
529// GetChat gets information about a chat.
530func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
531	resp, err := bot.Request(config)
532	if err != nil {
533		return Chat{}, err
534	}
535
536	var chat Chat
537	err = json.Unmarshal(resp.Result, &chat)
538
539	return chat, err
540}
541
542// GetChatAdministrators gets a list of administrators in the chat.
543//
544// If none have been appointed, only the creator will be returned.
545// Bots are not shown, even if they are an administrator.
546func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
547	resp, err := bot.Request(config)
548	if err != nil {
549		return []ChatMember{}, err
550	}
551
552	var members []ChatMember
553	err = json.Unmarshal(resp.Result, &members)
554
555	return members, err
556}
557
558// GetChatMembersCount gets the number of users in a chat.
559func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
560	resp, err := bot.Request(config)
561	if err != nil {
562		return -1, err
563	}
564
565	var count int
566	err = json.Unmarshal(resp.Result, &count)
567
568	return count, err
569}
570
571// GetChatMember gets a specific chat member.
572func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
573	resp, err := bot.Request(config)
574	if err != nil {
575		return ChatMember{}, err
576	}
577
578	var member ChatMember
579	err = json.Unmarshal(resp.Result, &member)
580
581	return member, err
582}
583
584// GetGameHighScores allows you to get the high scores for a game.
585func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
586	resp, err := bot.Request(config)
587	if err != nil {
588		return []GameHighScore{}, err
589	}
590
591	var highScores []GameHighScore
592	err = json.Unmarshal(resp.Result, &highScores)
593
594	return highScores, err
595}
596
597// GetInviteLink get InviteLink for a chat
598func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
599	resp, err := bot.Request(config)
600	if err != nil {
601		return "", err
602	}
603
604	var inviteLink string
605	err = json.Unmarshal(resp.Result, &inviteLink)
606
607	return inviteLink, err
608}
609
610// GetStickerSet returns a StickerSet.
611func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
612	resp, err := bot.Request(config)
613	if err != nil {
614		return StickerSet{}, err
615	}
616
617	var stickers StickerSet
618	err = json.Unmarshal(resp.Result, &stickers)
619
620	return stickers, err
621}
622
623// StopPoll stops a poll and returns the result.
624func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
625	resp, err := bot.Request(config)
626	if err != nil {
627		return Poll{}, err
628	}
629
630	var poll Poll
631	err = json.Unmarshal(resp.Result, &poll)
632
633	return poll, err
634}
635
636// GetMyCommands gets the currently registered commands.
637func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
638	return bot.GetMyCommandsWithConfig(GetMyCommandsConfig{})
639}
640
641// GetMyCommandsWithConfig gets the currently registered commands with a config.
642func (bot *BotAPI) GetMyCommandsWithConfig(config GetMyCommandsConfig) ([]BotCommand, error) {
643	resp, err := bot.Request(config)
644	if err != nil {
645		return nil, err
646	}
647
648	var commands []BotCommand
649	err = json.Unmarshal(resp.Result, &commands)
650
651	return commands, err
652}
653
654// CopyMessage copy messages of any kind. The method is analogous to the method
655// forwardMessage, but the copied message doesn't have a link to the original
656// message. Returns the MessageID of the sent message on success.
657func (bot *BotAPI) CopyMessage(config CopyMessageConfig) (MessageID, error) {
658	params, err := config.params()
659	if err != nil {
660		return MessageID{}, err
661	}
662
663	resp, err := bot.MakeRequest(config.method(), params)
664	if err != nil {
665		return MessageID{}, err
666	}
667
668	var messageID MessageID
669	err = json.Unmarshal(resp.Result, &messageID)
670
671	return messageID, err
672}