all repos — telegram-bot-api @ b4a22fe527a5ddd33b3b66d8ae556db523ae9c9f

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