all repos — telegram-bot-api @ ce4fc988c916518bf64e8d02be6e19d89d745928

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				panic(err)
164			}
165		}
166
167		for _, file := range files {
168			switch f := file.File.(type) {
169			case string:
170				fileHandle, err := os.Open(f)
171				if err != nil {
172					panic(err)
173				}
174				defer fileHandle.Close()
175
176				part, err := m.CreateFormFile(file.Name, fileHandle.Name())
177				if err != nil {
178					panic(err)
179				}
180
181				io.Copy(part, fileHandle)
182			case FileBytes:
183				part, err := m.CreateFormFile(file.Name, f.Name)
184				if err != nil {
185					panic(err)
186				}
187
188				buf := bytes.NewBuffer(f.Bytes)
189				io.Copy(part, buf)
190			case FileReader:
191				part, err := m.CreateFormFile(file.Name, f.Name)
192				if err != nil {
193					panic(err)
194				}
195
196				if f.Size != -1 {
197					io.Copy(part, f.Reader)
198				} else {
199					data, err := ioutil.ReadAll(f.Reader)
200					if err != nil {
201						panic(err)
202					}
203
204					buf := bytes.NewBuffer(data)
205					io.Copy(part, buf)
206				}
207			case FileURL:
208				val := string(f)
209				if err := m.WriteField(file.Name, val); err != nil {
210					panic(err)
211				}
212			case FileID:
213				val := string(f)
214				if err := m.WriteField(file.Name, val); err != nil {
215					panic(err)
216				}
217			default:
218				panic(errors.New(ErrBadFileType))
219			}
220		}
221	}()
222
223	if bot.Debug {
224		log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files))
225	}
226
227	method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
228
229	req, err := http.NewRequest("POST", method, r)
230	if err != nil {
231		return nil, err
232	}
233
234	req.Header.Set("Content-Type", m.FormDataContentType())
235
236	resp, err := bot.Client.Do(req)
237	if err != nil {
238		return nil, err
239	}
240	defer resp.Body.Close()
241
242	var apiResp APIResponse
243	bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
244	if err != nil {
245		return &apiResp, err
246	}
247
248	if bot.Debug {
249		log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
250	}
251
252	if !apiResp.Ok {
253		var parameters ResponseParameters
254
255		if apiResp.Parameters != nil {
256			parameters = *apiResp.Parameters
257		}
258
259		return &apiResp, &Error{
260			Message:            apiResp.Description,
261			ResponseParameters: parameters,
262		}
263	}
264
265	return &apiResp, nil
266}
267
268// GetFileDirectURL returns direct URL to file
269//
270// It requires the FileID.
271func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
272	file, err := bot.GetFile(FileConfig{fileID})
273
274	if err != nil {
275		return "", err
276	}
277
278	return file.Link(bot.Token), nil
279}
280
281// GetMe fetches the currently authenticated bot.
282//
283// This method is called upon creation to validate the token,
284// and so you may get this data from BotAPI.Self without the need for
285// another request.
286func (bot *BotAPI) GetMe() (User, error) {
287	resp, err := bot.MakeRequest("getMe", nil)
288	if err != nil {
289		return User{}, err
290	}
291
292	var user User
293	err = json.Unmarshal(resp.Result, &user)
294
295	return user, err
296}
297
298// IsMessageToMe returns true if message directed to this bot.
299//
300// It requires the Message.
301func (bot *BotAPI) IsMessageToMe(message Message) bool {
302	return strings.Contains(message.Text, "@"+bot.Self.UserName)
303}
304
305func hasFilesNeedingUpload(files []RequestFile) bool {
306	for _, file := range files {
307		switch file.File.(type) {
308		case string, FileBytes, FileReader:
309			return true
310		}
311	}
312
313	return false
314}
315
316// Request sends a Chattable to Telegram, and returns the APIResponse.
317func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) {
318	params, err := c.params()
319	if err != nil {
320		return nil, err
321	}
322
323	if t, ok := c.(Fileable); ok {
324		files := t.files()
325
326		// If we have files that need to be uploaded, we should delegate the
327		// request to UploadFile.
328		if hasFilesNeedingUpload(files) {
329			return bot.UploadFiles(t.method(), params, files)
330		}
331
332		// However, if there are no files to be uploaded, there's likely things
333		// that need to be turned into params instead.
334		for _, file := range files {
335			var s string
336
337			switch f := file.File.(type) {
338			case string:
339				s = f
340			case FileID:
341				s = string(f)
342			case FileURL:
343				s = string(f)
344			default:
345				return nil, errors.New(ErrBadFileType)
346			}
347
348			params[file.Name] = s
349		}
350	}
351
352	return bot.MakeRequest(c.method(), params)
353}
354
355// Send will send a Chattable item to Telegram and provides the
356// returned Message.
357func (bot *BotAPI) Send(c Chattable) (Message, error) {
358	resp, err := bot.Request(c)
359	if err != nil {
360		return Message{}, err
361	}
362
363	var message Message
364	err = json.Unmarshal(resp.Result, &message)
365
366	return message, err
367}
368
369// SendMediaGroup sends a media group and returns the resulting messages.
370func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {
371	filesToUpload := []RequestFile{}
372
373	newMedia := []interface{}{}
374
375	for idx, media := range config.Media {
376		switch m := media.(type) {
377		case InputMediaPhoto:
378			switch f := m.Media.(type) {
379			case string, FileBytes, FileReader:
380				m.Media = fmt.Sprintf("attach://file-%d", idx)
381				newMedia = append(newMedia, m)
382
383				filesToUpload = append(filesToUpload, RequestFile{
384					Name: fmt.Sprintf("file-%d", idx),
385					File: f,
386				})
387			default:
388				newMedia = append(newMedia, m)
389			}
390		case InputMediaVideo:
391			switch f := m.Media.(type) {
392			case string, FileBytes, FileReader:
393				m.Media = fmt.Sprintf("attach://file-%d", idx)
394				newMedia = append(newMedia, m)
395
396				filesToUpload = append(filesToUpload, RequestFile{
397					Name: fmt.Sprintf("file-%d", idx),
398					File: f,
399				})
400			default:
401				newMedia = append(newMedia, m)
402			}
403		default:
404			return nil, errors.New(ErrBadFileType)
405		}
406	}
407
408	params, err := config.params()
409	if err != nil {
410		return nil, err
411	}
412
413	params.AddInterface("media", newMedia)
414
415	resp, err := bot.UploadFiles(config.method(), params, filesToUpload)
416	if err != nil {
417		return nil, err
418	}
419
420	var messages []Message
421	err = json.Unmarshal(resp.Result, &messages)
422
423	return messages, err
424}
425
426// GetUserProfilePhotos gets a user's profile photos.
427//
428// It requires UserID.
429// Offset and Limit are optional.
430func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
431	resp, err := bot.Request(config)
432	if err != nil {
433		return UserProfilePhotos{}, err
434	}
435
436	var profilePhotos UserProfilePhotos
437	err = json.Unmarshal(resp.Result, &profilePhotos)
438
439	return profilePhotos, err
440}
441
442// GetFile returns a File which can download a file from Telegram.
443//
444// Requires FileID.
445func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
446	resp, err := bot.Request(config)
447	if err != nil {
448		return File{}, err
449	}
450
451	var file File
452	err = json.Unmarshal(resp.Result, &file)
453
454	return file, err
455}
456
457// GetUpdates fetches updates.
458// If a WebHook is set, this will not return any data!
459//
460// Offset, Limit, and Timeout are optional.
461// To avoid stale items, set Offset to one higher than the previous item.
462// Set Timeout to a large number to reduce requests so you can get updates
463// instantly instead of having to wait between requests.
464func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
465	resp, err := bot.Request(config)
466	if err != nil {
467		return []Update{}, err
468	}
469
470	var updates []Update
471	err = json.Unmarshal(resp.Result, &updates)
472
473	return updates, err
474}
475
476// GetWebhookInfo allows you to fetch information about a webhook and if
477// one currently is set, along with pending update count and error messages.
478func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
479	resp, err := bot.MakeRequest("getWebhookInfo", nil)
480	if err != nil {
481		return WebhookInfo{}, err
482	}
483
484	var info WebhookInfo
485	err = json.Unmarshal(resp.Result, &info)
486
487	return info, err
488}
489
490// GetUpdatesChan starts and returns a channel for getting updates.
491func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
492	ch := make(chan Update, bot.Buffer)
493
494	go func() {
495		for {
496			select {
497			case <-bot.shutdownChannel:
498				return
499			default:
500			}
501
502			updates, err := bot.GetUpdates(config)
503			if err != nil {
504				log.Println(err)
505				log.Println("Failed to get updates, retrying in 3 seconds...")
506				time.Sleep(time.Second * 3)
507
508				continue
509			}
510
511			for _, update := range updates {
512				if update.UpdateID >= config.Offset {
513					config.Offset = update.UpdateID + 1
514					ch <- update
515				}
516			}
517		}
518	}()
519
520	return ch
521}
522
523// StopReceivingUpdates stops the go routine which receives updates
524func (bot *BotAPI) StopReceivingUpdates() {
525	if bot.Debug {
526		log.Println("Stopping the update receiver routine...")
527	}
528	close(bot.shutdownChannel)
529}
530
531// ListenForWebhook registers a http handler for a webhook.
532func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
533	ch := make(chan Update, bot.Buffer)
534
535	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
536		ch <- bot.HandleUpdate(w, r)
537	})
538
539	return ch
540}
541
542// HandleUpdate parses and returns update received via webhook
543func (bot *BotAPI) HandleUpdate(res http.ResponseWriter, req *http.Request) Update {
544	bytes, _ := ioutil.ReadAll(req.Body)
545	req.Body.Close()
546
547	var update Update
548	json.Unmarshal(bytes, &update)
549
550	return update
551}
552
553// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
554//
555// It doesn't support uploading files.
556//
557// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
558// for details.
559func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
560	params, err := c.params()
561	if err != nil {
562		return err
563	}
564
565	if t, ok := c.(Fileable); ok {
566		if hasFilesNeedingUpload(t.files()) {
567			return errors.New("unable to use http response to upload files")
568		}
569	}
570
571	values := buildParams(params)
572	values.Set("method", c.method())
573
574	w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
575	_, err = w.Write([]byte(values.Encode()))
576	return err
577}
578
579// GetChat gets information about a chat.
580func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
581	resp, err := bot.Request(config)
582	if err != nil {
583		return Chat{}, err
584	}
585
586	var chat Chat
587	err = json.Unmarshal(resp.Result, &chat)
588
589	return chat, err
590}
591
592// GetChatAdministrators gets a list of administrators in the chat.
593//
594// If none have been appointed, only the creator will be returned.
595// Bots are not shown, even if they are an administrator.
596func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
597	resp, err := bot.Request(config)
598	if err != nil {
599		return []ChatMember{}, err
600	}
601
602	var members []ChatMember
603	err = json.Unmarshal(resp.Result, &members)
604
605	return members, err
606}
607
608// GetChatMembersCount gets the number of users in a chat.
609func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
610	resp, err := bot.Request(config)
611	if err != nil {
612		return -1, err
613	}
614
615	var count int
616	err = json.Unmarshal(resp.Result, &count)
617
618	return count, err
619}
620
621// GetChatMember gets a specific chat member.
622func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
623	resp, err := bot.Request(config)
624	if err != nil {
625		return ChatMember{}, err
626	}
627
628	var member ChatMember
629	err = json.Unmarshal(resp.Result, &member)
630
631	return member, err
632}
633
634// GetGameHighScores allows you to get the high scores for a game.
635func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
636	resp, err := bot.Request(config)
637	if err != nil {
638		return []GameHighScore{}, err
639	}
640
641	var highScores []GameHighScore
642	err = json.Unmarshal(resp.Result, &highScores)
643
644	return highScores, err
645}
646
647// GetInviteLink get InviteLink for a chat
648func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
649	resp, err := bot.Request(config)
650	if err != nil {
651		return "", err
652	}
653
654	var inviteLink string
655	err = json.Unmarshal(resp.Result, &inviteLink)
656
657	return inviteLink, err
658}
659
660// GetStickerSet returns a StickerSet.
661func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
662	resp, err := bot.Request(config)
663	if err != nil {
664		return StickerSet{}, err
665	}
666
667	var stickers StickerSet
668	err = json.Unmarshal(resp.Result, &stickers)
669
670	return stickers, err
671}
672
673// StopPoll stops a poll and returns the result.
674func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
675	resp, err := bot.Request(config)
676	if err != nil {
677		return Poll{}, err
678	}
679
680	var poll Poll
681	err = json.Unmarshal(resp.Result, &poll)
682
683	return poll, err
684}
685
686// GetMyCommands gets the currently registered commands.
687func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
688	config := GetMyCommandsConfig{}
689
690	resp, err := bot.Request(config)
691	if err != nil {
692		return nil, err
693	}
694
695	var commands []BotCommand
696	err = json.Unmarshal(resp.Result, &commands)
697
698	return commands, err
699}