all repos — telegram-bot-api @ a45216f441cd4adc8bda63ea430a24ffa778ab6f

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