all repos — telegram-bot-api @ 24e02f7ba6aa2e045e23e8afbebaf7f249f0e368

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