all repos — telegram-bot-api @ b163052f82f70d259ba53759bd3470286201b479

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