all repos — telegram-bot-api @ c6bf64c67d2d1002b7fbec45608fb914d176616c

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	filesToUpload := []RequestFile{}
381
382	newMedia := []interface{}{}
383
384	for idx, media := range config.Media {
385		switch m := media.(type) {
386		case InputMediaPhoto:
387			switch f := m.Media.(type) {
388			case string, FileBytes, FileReader:
389				m.Media = fmt.Sprintf("attach://file-%d", idx)
390				newMedia = append(newMedia, m)
391
392				filesToUpload = append(filesToUpload, RequestFile{
393					Name: fmt.Sprintf("file-%d", idx),
394					File: f,
395				})
396			default:
397				newMedia = append(newMedia, m)
398			}
399		case InputMediaVideo:
400			switch f := m.Media.(type) {
401			case string, FileBytes, FileReader:
402				m.Media = fmt.Sprintf("attach://file-%d", idx)
403				newMedia = append(newMedia, m)
404
405				filesToUpload = append(filesToUpload, RequestFile{
406					Name: fmt.Sprintf("file-%d", idx),
407					File: f,
408				})
409			default:
410				newMedia = append(newMedia, m)
411			}
412		default:
413			return nil, errors.New(ErrBadFileType)
414		}
415	}
416
417	params, err := config.params()
418	if err != nil {
419		return nil, err
420	}
421
422	params.AddInterface("media", newMedia)
423
424	resp, err := bot.UploadFiles(config.method(), params, filesToUpload)
425	if err != nil {
426		return nil, err
427	}
428
429	var messages []Message
430	err = json.Unmarshal(resp.Result, &messages)
431
432	return messages, err
433}
434
435// GetUserProfilePhotos gets a user's profile photos.
436//
437// It requires UserID.
438// Offset and Limit are optional.
439func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
440	resp, err := bot.Request(config)
441	if err != nil {
442		return UserProfilePhotos{}, err
443	}
444
445	var profilePhotos UserProfilePhotos
446	err = json.Unmarshal(resp.Result, &profilePhotos)
447
448	return profilePhotos, err
449}
450
451// GetFile returns a File which can download a file from Telegram.
452//
453// Requires FileID.
454func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
455	resp, err := bot.Request(config)
456	if err != nil {
457		return File{}, err
458	}
459
460	var file File
461	err = json.Unmarshal(resp.Result, &file)
462
463	return file, err
464}
465
466// GetUpdates fetches updates.
467// If a WebHook is set, this will not return any data!
468//
469// Offset, Limit, and Timeout are optional.
470// To avoid stale items, set Offset to one higher than the previous item.
471// Set Timeout to a large number to reduce requests so you can get updates
472// instantly instead of having to wait between requests.
473func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
474	resp, err := bot.Request(config)
475	if err != nil {
476		return []Update{}, err
477	}
478
479	var updates []Update
480	err = json.Unmarshal(resp.Result, &updates)
481
482	return updates, err
483}
484
485// GetWebhookInfo allows you to fetch information about a webhook and if
486// one currently is set, along with pending update count and error messages.
487func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
488	resp, err := bot.MakeRequest("getWebhookInfo", nil)
489	if err != nil {
490		return WebhookInfo{}, err
491	}
492
493	var info WebhookInfo
494	err = json.Unmarshal(resp.Result, &info)
495
496	return info, err
497}
498
499// GetUpdatesChan starts and returns a channel for getting updates.
500func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
501	ch := make(chan Update, bot.Buffer)
502
503	go func() {
504		for {
505			select {
506			case <-bot.shutdownChannel:
507				return
508			default:
509			}
510
511			updates, err := bot.GetUpdates(config)
512			if err != nil {
513				log.Println(err)
514				log.Println("Failed to get updates, retrying in 3 seconds...")
515				time.Sleep(time.Second * 3)
516
517				continue
518			}
519
520			for _, update := range updates {
521				if update.UpdateID >= config.Offset {
522					config.Offset = update.UpdateID + 1
523					ch <- update
524				}
525			}
526		}
527	}()
528
529	return ch
530}
531
532// StopReceivingUpdates stops the go routine which receives updates
533func (bot *BotAPI) StopReceivingUpdates() {
534	if bot.Debug {
535		log.Println("Stopping the update receiver routine...")
536	}
537	close(bot.shutdownChannel)
538}
539
540// ListenForWebhook registers a http handler for a webhook.
541func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
542	ch := make(chan Update, bot.Buffer)
543
544	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
545		ch <- bot.HandleUpdate(w, r)
546	})
547
548	return ch
549}
550
551// HandleUpdate parses and returns update received via webhook
552func (bot *BotAPI) HandleUpdate(res http.ResponseWriter, req *http.Request) Update {
553	bytes, _ := ioutil.ReadAll(req.Body)
554	req.Body.Close()
555
556	var update Update
557	json.Unmarshal(bytes, &update)
558
559	return update
560}
561
562// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
563//
564// It doesn't support uploading files.
565//
566// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
567// for details.
568func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
569	params, err := c.params()
570	if err != nil {
571		return err
572	}
573
574	if t, ok := c.(Fileable); ok {
575		if hasFilesNeedingUpload(t.files()) {
576			return errors.New("unable to use http response to upload files")
577		}
578	}
579
580	values := buildParams(params)
581	values.Set("method", c.method())
582
583	w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
584	_, err = w.Write([]byte(values.Encode()))
585	return err
586}
587
588// GetChat gets information about a chat.
589func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
590	resp, err := bot.Request(config)
591	if err != nil {
592		return Chat{}, err
593	}
594
595	var chat Chat
596	err = json.Unmarshal(resp.Result, &chat)
597
598	return chat, err
599}
600
601// GetChatAdministrators gets a list of administrators in the chat.
602//
603// If none have been appointed, only the creator will be returned.
604// Bots are not shown, even if they are an administrator.
605func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
606	resp, err := bot.Request(config)
607	if err != nil {
608		return []ChatMember{}, err
609	}
610
611	var members []ChatMember
612	err = json.Unmarshal(resp.Result, &members)
613
614	return members, err
615}
616
617// GetChatMembersCount gets the number of users in a chat.
618func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
619	resp, err := bot.Request(config)
620	if err != nil {
621		return -1, err
622	}
623
624	var count int
625	err = json.Unmarshal(resp.Result, &count)
626
627	return count, err
628}
629
630// GetChatMember gets a specific chat member.
631func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
632	resp, err := bot.Request(config)
633	if err != nil {
634		return ChatMember{}, err
635	}
636
637	var member ChatMember
638	err = json.Unmarshal(resp.Result, &member)
639
640	return member, err
641}
642
643// GetGameHighScores allows you to get the high scores for a game.
644func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
645	resp, err := bot.Request(config)
646	if err != nil {
647		return []GameHighScore{}, err
648	}
649
650	var highScores []GameHighScore
651	err = json.Unmarshal(resp.Result, &highScores)
652
653	return highScores, err
654}
655
656// GetInviteLink get InviteLink for a chat
657func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
658	resp, err := bot.Request(config)
659	if err != nil {
660		return "", err
661	}
662
663	var inviteLink string
664	err = json.Unmarshal(resp.Result, &inviteLink)
665
666	return inviteLink, err
667}
668
669// GetStickerSet returns a StickerSet.
670func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
671	resp, err := bot.Request(config)
672	if err != nil {
673		return StickerSet{}, err
674	}
675
676	var stickers StickerSet
677	err = json.Unmarshal(resp.Result, &stickers)
678
679	return stickers, err
680}
681
682// StopPoll stops a poll and returns the result.
683func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
684	resp, err := bot.Request(config)
685	if err != nil {
686		return Poll{}, err
687	}
688
689	var poll Poll
690	err = json.Unmarshal(resp.Result, &poll)
691
692	return poll, err
693}
694
695// GetMyCommands gets the currently registered commands.
696func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
697	config := GetMyCommandsConfig{}
698
699	resp, err := bot.Request(config)
700	if err != nil {
701		return nil, err
702	}
703
704	var commands []BotCommand
705	err = json.Unmarshal(resp.Result, &commands)
706
707	return commands, err
708}