You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.
telegram-bot-api/bot.go

618 lines
15 KiB

// Package tgbotapi has functions and types used for interacting with
// the Telegram Bot API.
package tgbotapi
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/technoweenie/multipartstreamer"
)
type Bot interface {
MakeRequest(endpoint string, params url.Values) (APIResponse, error)
UploadFile(endpoint string, params map[string]string, fieldname string, file interface{}) (APIResponse, error)
GetFileDirectURL(fileID string) (string, error)
GetMe() (User, error)
IsMessageToMe(message Message) bool
Send(c Chattable) (Message, error)
GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error)
GetFile(config FileConfig) (File, error)
GetUpdates(config UpdateConfig) ([]Update, error)
RemoveWebhook() (APIResponse, error)
SetWebhook(config WebhookConfig) (APIResponse, error)
GetWebhookInfo() (WebhookInfo, error)
GetUpdatesChan(config UpdateConfig) (UpdatesChannel, error)
StopReceivingUpdates()
ListenForWebhook(pattern string) UpdatesChannel
AnswerInlineQuery(config InlineConfig) (APIResponse, error)
AnswerCallbackQuery(config CallbackConfig) (APIResponse, error)
KickChatMember(config KickChatMemberConfig) (APIResponse, error)
LeaveChat(config ChatConfig) (APIResponse, error)
GetChat(config ChatConfig) (Chat, error)
GetChatAdministrators(config ChatConfig) ([]ChatMember, error)
GetChatMembersCount(config ChatConfig) (int, error)
GetChatMember(config ChatConfigWithUser) (ChatMember, error)
UnbanChatMember(config ChatMemberConfig) (APIResponse, error)
RestrictChatMember(config RestrictChatMemberConfig) (APIResponse, error)
PromoteChatMember(config PromoteChatMemberConfig) (APIResponse, error)
GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error)
AnswerShippingQuery(config ShippingConfig) (APIResponse, error)
AnswerPreCheckoutQuery(config PreCheckoutConfig) (APIResponse, error)
DeleteMessage(config DeleteMessageConfig) (APIResponse, error)
GetInviteLink(config ChatConfig) (string, error)
PinChatMessage(config PinChatMessageConfig) (APIResponse, error)
UnpinChatMessage(config UnpinChatMessageConfig) (APIResponse, error)
SetChatTitle(config SetChatTitleConfig) (APIResponse, error)
SetChatDescription(config SetChatDescriptionConfig) (APIResponse, error)
SetChatPhoto(config SetChatPhotoConfig) (APIResponse, error)
DeleteChatPhoto(config DeleteChatPhotoConfig) (APIResponse, error)
}
// BotAPI allows you to interact with the Telegram Bot API.
type BotAPI struct {
Token string `json:"token"`
Debug bool `json:"debug"`
Buffer int `json:"buffer"`
Self User `json:"-"`
Client *http.Client `json:"-"`
shutdownChannel chan interface{}
}
// NewBotAPI creates a new BotAPI instance.
//
// It requires a token, provided by @BotFather on Telegram.
func NewBotAPI(token string) (*BotAPI, error) {
return NewBotAPIWithClient(token, &http.Client{})
}
// NewBotAPIWithClient creates a new BotAPI instance
// and allows you to pass a http.Client.
//
// It requires a token, provided by @BotFather on Telegram.
func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) {
bot := &BotAPI{
Token: token,
Client: client,
Buffer: 100,
shutdownChannel: make(chan interface{}),
}
self, err := bot.GetMe()
if err != nil {
return nil, err
}
bot.Self = self
return bot, nil
}
func buildParams(in Params) (out url.Values) {
if in == nil {
return url.Values{}
}
out = url.Values{}
for key, value := range in {
out.Set(key, value)
}
return
}
// MakeRequest makes a request to a specific endpoint with our token.
func (bot *BotAPI) MakeRequest(endpoint string, params Params) (APIResponse, error) {
if bot.Debug {
log.Printf("Endpoint: %s, params: %v\n", endpoint, params)
}
method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
values := buildParams(params)
resp, err := bot.Client.PostForm(method, values)
if err != nil {
return APIResponse{}, err
}
defer resp.Body.Close()
var apiResp APIResponse
bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
if err != nil {
return apiResp, err
}
if bot.Debug {
log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
}
if !apiResp.Ok {
var parameters ResponseParameters
if apiResp.Parameters != nil {
parameters = *apiResp.Parameters
}
return apiResp, Error{
Message: apiResp.Description,
ResponseParameters: parameters,
}
}
return apiResp, nil
}
// decodeAPIResponse decode response and return slice of bytes if debug enabled.
// If debug disabled, just decode http.Response.Body stream to APIResponse struct
// for efficient memory usage
func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) (_ []byte, err error) {
if !bot.Debug {
dec := json.NewDecoder(responseBody)
err = dec.Decode(resp)
return
}
// if debug, read reponse body
data, err := ioutil.ReadAll(responseBody)
if err != nil {
return
}
err = json.Unmarshal(data, resp)
if err != nil {
return
}
return data, nil
}
// UploadFile makes a request to the API with a file.
//
// Requires the parameter to hold the file not be in the params.
// File should be a string to a file path, a FileBytes struct,
// a FileReader struct, or a url.URL.
//
// Note that if your FileReader has a size set to -1, it will read
// the file into memory to calculate a size.
func (bot *BotAPI) UploadFile(endpoint string, params Params, fieldname string, file interface{}) (APIResponse, error) {
ms := multipartstreamer.New()
switch f := file.(type) {
case string:
ms.WriteFields(params)
fileHandle, err := os.Open(f)
if err != nil {
return APIResponse{}, err
}
defer fileHandle.Close()
fi, err := os.Stat(f)
if err != nil {
return APIResponse{}, err
}
ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle)
case FileBytes:
ms.WriteFields(params)
buf := bytes.NewBuffer(f.Bytes)
ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf)
case FileReader:
ms.WriteFields(params)
if f.Size != -1 {
ms.WriteReader(fieldname, f.Name, f.Size, f.Reader)
break
}
data, err := ioutil.ReadAll(f.Reader)
if err != nil {
return APIResponse{}, err
}
buf := bytes.NewBuffer(data)
ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
case url.URL:
params[fieldname] = f.String()
ms.WriteFields(params)
default:
return APIResponse{}, errors.New(ErrBadFileType)
}
if bot.Debug {
log.Printf("Endpoint: %s, fieldname: %s, params: %v, file: %T\n", endpoint, fieldname, params, file)
}
method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
req, err := http.NewRequest("POST", method, nil)
if err != nil {
return APIResponse{}, err
}
ms.SetupRequest(req)
res, err := bot.Client.Do(req)
if err != nil {
return APIResponse{}, err
}
defer res.Body.Close()
bytes, err := ioutil.ReadAll(res.Body)
if err != nil {
return APIResponse{}, err
}
if bot.Debug {
log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
}
var apiResp APIResponse
err = json.Unmarshal(bytes, &apiResp)
if err != nil {
return APIResponse{}, err
}
if !apiResp.Ok {
return APIResponse{}, errors.New(apiResp.Description)
}
return apiResp, nil
}
// GetFileDirectURL returns direct URL to file
//
// It requires the FileID.
func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
file, err := bot.GetFile(FileConfig{fileID})
if err != nil {
return "", err
}
return file.Link(bot.Token), nil
}
// GetMe fetches the currently authenticated bot.
//
// This method is called upon creation to validate the token,
// and so you may get this data from BotAPI.Self without the need for
// another request.
func (bot *BotAPI) GetMe() (User, error) {
resp, err := bot.MakeRequest("getMe", nil)
if err != nil {
return User{}, err
}
var user User
err = json.Unmarshal(resp.Result, &user)
return user, err
}
// IsMessageToMe returns true if message directed to this bot.
//
// It requires the Message.
func (bot *BotAPI) IsMessageToMe(message Message) bool {
return strings.Contains(message.Text, "@"+bot.Self.UserName)
}
// Request sends a Chattable to Telegram, and returns the APIResponse.
func (bot *BotAPI) Request(c Chattable) (APIResponse, error) {
params, err := c.params()
if err != nil {
return APIResponse{}, err
}
switch t := c.(type) {
case Fileable:
if t.useExistingFile() {
return bot.MakeRequest(t.method(), params)
}
return bot.UploadFile(t.method(), params, t.name(), t.getFile())
default:
return bot.MakeRequest(c.method(), params)
}
}
// Send will send a Chattable item to Telegram and provides the
// returned Message.
func (bot *BotAPI) Send(c Chattable) (Message, error) {
resp, err := bot.Request(c)
if err != nil {
return Message{}, err
}
var message Message
err = json.Unmarshal(resp.Result, &message)
return message, err
}
// SendMediaGroup sends a media group and returns the resulting messages.
func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {
params, _ := config.params()
resp, err := bot.MakeRequest(config.method(), params)
if err != nil {
return nil, err
}
var messages []Message
err = json.Unmarshal(resp.Result, &messages)
return messages, err
}
// GetUserProfilePhotos gets a user's profile photos.
//
// It requires UserID.
// Offset and Limit are optional.
func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
params, _ := config.params()
resp, err := bot.MakeRequest(config.method(), params)
if err != nil {
return UserProfilePhotos{}, err
}
var profilePhotos UserProfilePhotos
err = json.Unmarshal(resp.Result, &profilePhotos)
return profilePhotos, err
}
// GetFile returns a File which can download a file from Telegram.
//
// Requires FileID.
func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
params, _ := config.params()
resp, err := bot.MakeRequest(config.method(), params)
if err != nil {
return File{}, err
}
var file File
err = json.Unmarshal(resp.Result, &file)
return file, err
}
// GetUpdates fetches updates.
// If a WebHook is set, this will not return any data!
//
// Offset, Limit, and Timeout are optional.
// To avoid stale items, set Offset to one higher than the previous item.
// Set Timeout to a large number to reduce requests so you can get updates
// instantly instead of having to wait between requests.
func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
params, _ := config.params()
resp, err := bot.MakeRequest(config.method(), params)
if err != nil {
return []Update{}, err
}
var updates []Update
err = json.Unmarshal(resp.Result, &updates)
return updates, err
}
// GetWebhookInfo allows you to fetch information about a webhook and if
// one currently is set, along with pending update count and error messages.
func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
resp, err := bot.MakeRequest("getWebhookInfo", nil)
if err != nil {
return WebhookInfo{}, err
}
var info WebhookInfo
err = json.Unmarshal(resp.Result, &info)
return info, err
}
// GetUpdatesChan starts and returns a channel for getting updates.
func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
ch := make(chan Update, bot.Buffer)
go func() {
for {
select {
case <-bot.shutdownChannel:
return
default:
}
updates, err := bot.GetUpdates(config)
if err != nil {
log.Println(err)
log.Println("Failed to get updates, retrying in 3 seconds...")
time.Sleep(time.Second * 3)
continue
}
for _, update := range updates {
if update.UpdateID >= config.Offset {
config.Offset = update.UpdateID + 1
ch <- update
}
}
}
}()
return ch
}
// StopReceivingUpdates stops the go routine which receives updates
func (bot *BotAPI) StopReceivingUpdates() {
if bot.Debug {
log.Println("Stopping the update receiver routine...")
}
close(bot.shutdownChannel)
}
// ListenForWebhook registers a http handler for a webhook.
func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
ch := make(chan Update, bot.Buffer)
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
bytes, _ := ioutil.ReadAll(r.Body)
r.Body.Close()
var update Update
json.Unmarshal(bytes, &update)
ch <- update
})
return ch
}
// GetChat gets information about a chat.
func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
params, _ := config.params()
resp, err := bot.MakeRequest(config.method(), params)
if err != nil {
return Chat{}, err
}
var chat Chat
err = json.Unmarshal(resp.Result, &chat)
return chat, err
}
// GetChatAdministrators gets a list of administrators in the chat.
//
// If none have been appointed, only the creator will be returned.
// Bots are not shown, even if they are an administrator.
func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
params, _ := config.params()
resp, err := bot.MakeRequest(config.method(), params)
if err != nil {
return []ChatMember{}, err
}
var members []ChatMember
err = json.Unmarshal(resp.Result, &members)
return members, err
}
// GetChatMembersCount gets the number of users in a chat.
func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
params, _ := config.params()
resp, err := bot.MakeRequest(config.method(), params)
if err != nil {
return -1, err
}
var count int
err = json.Unmarshal(resp.Result, &count)
return count, err
}
// GetChatMember gets a specific chat member.
func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
params, _ := config.params()
resp, err := bot.MakeRequest(config.method(), params)
if err != nil {
return ChatMember{}, err
}
var member ChatMember
err = json.Unmarshal(resp.Result, &member)
return member, err
}
// GetGameHighScores allows you to get the high scores for a game.
func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
params, _ := config.params()
resp, err := bot.MakeRequest(config.method(), params)
if err != nil {
return []GameHighScore{}, err
}
var highScores []GameHighScore
err = json.Unmarshal(resp.Result, &highScores)
return highScores, err
}
// GetInviteLink get InviteLink for a chat
func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
params, _ := config.params()
resp, err := bot.MakeRequest(config.method(), params)
if err != nil {
return "", err
}
var inviteLink string
err = json.Unmarshal(resp.Result, &inviteLink)
return inviteLink, err
}
// GetStickerSet returns a StickerSet.
func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
params, _ := config.params()
resp, err := bot.MakeRequest(config.method(), params)
if err != nil {
return StickerSet{}, err
}
var stickers StickerSet
err = json.Unmarshal(resp.Result, &stickers)
return stickers, err
}
// StopPoll stops a poll and returns the result.
func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
params, err := config.params()
if err != nil {
return Poll{}, err
}
resp, err := bot.MakeRequest(config.method(), params)
if err != nil {
return Poll{}, err
}
var poll Poll
err = json.Unmarshal(resp.Result, &poll)
return poll, err
}