telebot/bot.go
2019-03-29 16:25:48 +02:00

1360 lines
31 KiB
Go

package telebot
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
)
// NewBot does try to build a Bot with token `token`, which
// is a secret API key assigned to particular bot.
func NewBot(pref Settings) (*Bot, error) {
if pref.Updates == 0 {
pref.Updates = 100
}
client := pref.Client
if client == nil {
client = http.DefaultClient
}
if pref.URL == "" {
pref.URL = DefaultApiURL
}
bot := &Bot{
Token: pref.Token,
URL: pref.URL,
Updates: make(chan Update, pref.Updates),
Poller: pref.Poller,
handlers: make(map[string]interface{}),
stop: make(chan struct{}),
reporter: pref.Reporter,
client: client,
}
user, err := bot.getMe()
if err != nil {
return nil, err
}
bot.Me = user
return bot, nil
}
// Bot represents a separate Telegram bot instance.
type Bot struct {
Me *User
Token string
URL string
Updates chan Update
Poller Poller
handlers map[string]interface{}
reporter func(error)
stop chan struct{}
client *http.Client
}
// Settings represents a utility struct for passing certain
// properties of a bot around and is required to make bots.
type Settings struct {
// Telegram API Url
URL string
// Telegram token
Token string
// Updates channel capacity
Updates int // Default: 100
// Poller is the provider of Updates.
Poller Poller
// Reporter is a callback function that will get called
// on any panics recovered from endpoint handlers.
Reporter func(error)
// HTTP Client used to make requests to telegram api
Client *http.Client
}
// Update object represents an incoming update.
type Update struct {
ID int `json:"update_id"`
Message *Message `json:"message,omitempty"`
EditedMessage *Message `json:"edited_message,omitempty"`
ChannelPost *Message `json:"channel_post,omitempty"`
EditedChannelPost *Message `json:"edited_channel_post,omitempty"`
Callback *Callback `json:"callback_query,omitempty"`
Query *Query `json:"inline_query,omitempty"`
ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"`
PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"`
}
// ChosenInlineResult represents a result of an inline query that was chosen
// by the user and sent to their chat partner.
type ChosenInlineResult struct {
From User `json:"from"`
Location *Location `json:"location,omitempty"`
ResultID string `json:"result_id"`
Query string `json:"query"`
// Inline messages only!
MessageID string `json:"inline_message_id"`
}
type PreCheckoutQuery struct {
Sender *User `json:"from"`
ID string `json:"id"`
Currency string `json:"currency"`
Payload string `json:"invoice_payload"`
Total int `json:"total_amount"`
}
// Handle lets you set the handler for some command name or
// one of the supported endpoints.
//
// Example:
//
// b.handle("/help", func (m *tb.Message) {})
// b.handle(tb.OnEdited, func (m *tb.Message) {})
// b.handle(tb.OnQuery, func (q *tb.Query) {})
//
// // make a hook for one of your preserved (by-pointer)
// // inline buttons.
// b.handle(&inlineButton, func (c *tb.Callback) {})
//
func (b *Bot) Handle(endpoint interface{}, handler interface{}) {
switch end := endpoint.(type) {
case string:
b.handlers[end] = handler
case CallbackEndpoint:
b.handlers[end.CallbackUnique()] = handler
default:
panic("telebot: unsupported endpoint")
}
}
var (
cmdRx = regexp.MustCompile(`^(\/\w+)(@(\w+))?(\s|$)(.+)?`)
cbackRx = regexp.MustCompile(`^\f(\w+)(\|(.+))?$`)
)
func (b *Bot) handleCommand(m *Message, cmdName, cmdBot string) bool {
return false
}
// Start brings bot into motion by consuming incoming
// updates (see Bot.Updates channel).
func (b *Bot) Start() {
if b.Poller == nil {
panic("telebot: can't start without a poller")
}
stopPoller := make(chan struct{})
go b.Poller.Poll(b, b.Updates, stopPoller)
for {
select {
// handle incoming updates
case upd := <-b.Updates:
b.incomingUpdate(&upd)
// call to stop polling
case <-b.stop:
stopPoller <- struct{}{}
// polling has stopped
case <-stopPoller:
return
}
}
}
func (b *Bot) incomingUpdate(upd *Update) {
if upd.Message != nil {
m := upd.Message
if m.PinnedMessage != nil {
b.handle(OnPinned, m)
return
}
// Commands
if m.Text != "" {
// Filtering malicious messsages
if m.Text[0] == '\a' {
return
}
match := cmdRx.FindAllStringSubmatch(m.Text, -1)
// Command found - handle and return
if match != nil {
// Syntax: "</command>@<bot> <payload>"
command, botName := match[0][1], match[0][3]
m.Payload = match[0][5]
if botName != "" && !strings.EqualFold(b.Me.Username, botName) {
return
}
if b.handle(command, m) {
return
}
}
// 1:1 satisfaction
if b.handle(m.Text, m) {
return
}
// OnText
b.handle(OnText, m)
return
}
// on media
if b.handleMedia(m) {
return
}
// OnAddedToGroup
wasAdded := (m.UserJoined != nil && m.UserJoined.ID == b.Me.ID) ||
(m.UsersJoined != nil && isUserInList(b.Me, m.UsersJoined))
if m.GroupCreated || m.SuperGroupCreated || wasAdded {
b.handle(OnAddedToGroup, m)
return
}
if m.UserJoined != nil {
b.handle(OnUserJoined, m)
return
}
if m.UsersJoined != nil {
for _, user := range m.UsersJoined {
m.UserJoined = &user
b.handle(OnUserJoined, m)
}
return
}
if m.UserLeft != nil {
b.handle(OnUserLeft, m)
return
}
if m.NewGroupTitle != "" {
b.handle(OnNewGroupTitle, m)
return
}
if m.NewGroupPhoto != nil {
b.handle(OnNewGroupPhoto, m)
return
}
if m.GroupPhotoDeleted {
b.handle(OnGroupPhotoDeleted, m)
return
}
if m.MigrateTo != 0 {
if handler, ok := b.handlers[OnMigration]; ok {
if handler, ok := handler.(func(int64, int64)); ok {
// i'm not 100% sure that any of the values
// won't be cached, so I pass them all in:
go func(b *Bot, handler func(int64, int64), from, to int64) {
if b.reporter == nil {
defer b.deferDebug()
}
handler(from, to)
}(b, handler, m.MigrateFrom, m.MigrateTo)
} else {
panic("telebot: migration handler is bad")
}
}
return
}
return
}
if upd.EditedMessage != nil {
b.handle(OnEdited, upd.EditedMessage)
return
}
if upd.ChannelPost != nil {
b.handle(OnChannelPost, upd.ChannelPost)
return
}
if upd.EditedChannelPost != nil {
b.handle(OnEditedChannelPost, upd.EditedChannelPost)
return
}
if upd.Callback != nil {
if upd.Callback.Data != "" {
data := upd.Callback.Data
if data[0] == '\f' {
match := cbackRx.FindAllStringSubmatch(data, -1)
if match != nil {
unique, payload := match[0][1], match[0][3]
if handler, ok := b.handlers["\f"+unique]; ok {
if handler, ok := handler.(func(*Callback)); ok {
upd.Callback.Data = payload
// i'm not 100% sure that any of the values
// won't be cached, so I pass them all in:
go func(b *Bot, handler func(*Callback), c *Callback) {
if b.reporter == nil {
defer b.deferDebug()
}
handler(c)
}(b, handler, upd.Callback)
return
}
}
}
}
}
if handler, ok := b.handlers[OnCallback]; ok {
if handler, ok := handler.(func(*Callback)); ok {
// i'm not 100% sure that any of the values
// won't be cached, so I pass them all in:
go func(b *Bot, handler func(*Callback), c *Callback) {
if b.reporter == nil {
defer b.deferDebug()
}
handler(c)
}(b, handler, upd.Callback)
} else {
panic("telebot: callback handler is bad")
}
}
return
}
if upd.Query != nil {
if handler, ok := b.handlers[OnQuery]; ok {
if handler, ok := handler.(func(*Query)); ok {
// i'm not 100% sure that any of the values
// won't be cached, so I pass them all in:
go func(b *Bot, handler func(*Query), q *Query) {
if b.reporter == nil {
defer b.deferDebug()
}
handler(q)
}(b, handler, upd.Query)
} else {
panic("telebot: query handler is bad")
}
}
return
}
if upd.ChosenInlineResult != nil {
if handler, ok := b.handlers[OnChosenInlineResult]; ok {
if handler, ok := handler.(func(*ChosenInlineResult)); ok {
// i'm not 100% sure that any of the values
// won't be cached, so I pass them all in:
go func(b *Bot, handler func(*ChosenInlineResult),
r *ChosenInlineResult) {
if b.reporter == nil {
defer b.deferDebug()
}
handler(r)
}(b, handler, upd.ChosenInlineResult)
} else {
panic("telebot: chosen inline result handler is bad")
}
}
return
}
if upd.PreCheckoutQuery != nil {
if handler, ok := b.handlers[OnCheckout]; ok {
if handler, ok := handler.(func(*PreCheckoutQuery)); ok {
// i'm not 100% sure that any of the values
// won't be cached, so I pass them all in:
go func(b *Bot, handler func(*PreCheckoutQuery),
r *PreCheckoutQuery) {
if b.reporter == nil {
defer b.deferDebug()
}
handler(r)
}(b, handler, upd.PreCheckoutQuery)
} else {
panic("telebot: checkout handler is bad")
}
}
return
}
}
func (b *Bot) handle(end string, m *Message) bool {
handler, ok := b.handlers[end]
if !ok {
return false
}
if handler, ok := handler.(func(*Message)); ok {
// i'm not 100% sure that any of the values
// won't be cached, so I pass them all in:
go func(b *Bot, handler func(*Message), m *Message) {
if b.reporter == nil {
defer b.deferDebug()
}
handler(m)
}(b, handler, m)
return true
}
return false
}
func (b *Bot) handleMedia(m *Message) bool {
if m.Photo != nil {
b.handle(OnPhoto, m)
return true
}
if m.Voice != nil {
b.handle(OnVoice, m)
return true
}
if m.Audio != nil {
b.handle(OnAudio, m)
return true
}
if m.Document != nil {
b.handle(OnDocument, m)
return true
}
if m.Sticker != nil {
b.handle(OnSticker, m)
return true
}
if m.Video != nil {
b.handle(OnVideo, m)
return true
}
if m.VideoNote != nil {
b.handle(OnVideoNote, m)
return true
}
if m.Contact != nil {
b.handle(OnContact, m)
return true
}
if m.Location != nil {
b.handle(OnLocation, m)
return true
}
if m.Venue != nil {
b.handle(OnVenue, m)
return true
}
return false
}
// Stop gracefully shuts the poller down.
func (b *Bot) Stop() {
b.stop <- struct{}{}
}
// Send accepts 2+ arguments, starting with destination chat, followed by
// some Sendable (or string!) and optional send options.
//
// Note: since most arguments are of type interface{}, but have pointer
// method recievers, make sure to pass them by-pointer, NOT by-value.
//
// What is a send option exactly? It can be one of the following types:
//
// - *SendOptions (the actual object accepted by Telegram API)
// - *ReplyMarkup (a component of SendOptions)
// - Option (a shorcut flag for popular options)
// - ParseMode (HTML, Markdown, etc)
//
// This function will panic upon unsupported payloads and options!
func (b *Bot) Send(to Recipient, what interface{}, options ...interface{}) (*Message, error) {
sendOpts := extractOptions(options)
switch object := what.(type) {
case string:
return b.sendText(to, object, sendOpts)
case Sendable:
return object.Send(b, to, sendOpts)
default:
return nil, errors.New("telebot: unsupported sendable")
}
}
// SendAlbum is used when sending multiple instances of media as a single
// message (so-called album).
//
// From all existing options, it only supports telebot.Silent.
func (b *Bot) SendAlbum(to Recipient, a Album, options ...interface{}) ([]Message, error) {
media := make([]string, len(a))
files := make(map[string]File)
for i, x := range a {
var mediaRepr string
var jsonRepr []byte
f := x.MediaFile()
if f.InCloud() {
mediaRepr = f.FileID
} else if f.FileURL != "" {
mediaRepr = f.FileURL
} else if f.OnDisk() || f.FileReader != nil {
mediaRepr = fmt.Sprintf("attach://%d", i)
files[strconv.Itoa(i)] = *f
} else {
return nil, errors.Errorf(
"telebot: album entry #%d doesn't exist anywhere", i)
}
switch y := x.(type) {
case *Photo:
jsonRepr, _ = json.Marshal(struct {
Type string `json:"type"`
Caption string `json:"caption"`
Media string `json:"media"`
}{
"photo",
y.Caption,
mediaRepr,
})
case *Video:
jsonRepr, _ = json.Marshal(struct {
Type string `json:"type"`
Caption string `json:"caption"`
Media string `json:"media"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Duration int `json:"duration,omitempty"`
SupportsStreaming bool `json:"supports_streaming,omitempty"`
}{
"video",
y.Caption,
mediaRepr,
y.Width,
y.Height,
y.Duration,
y.SupportsStreaming,
})
default:
return nil, errors.Errorf("telebot: album entry #%d is not valid", i)
}
media[i] = string(jsonRepr)
}
params := map[string]string{
"chat_id": to.Recipient(),
"media": "[" + strings.Join(media, ",") + "]",
}
sendOpts := extractOptions(options)
embedSendOptions(params, sendOpts)
respJSON, err := b.sendFiles("sendMediaGroup", files, params)
if err != nil {
return nil, err
}
var resp struct {
Ok bool
Result []Message
Description string
}
err = json.Unmarshal(respJSON, &resp)
if err != nil {
return nil, errors.Wrap(err, "bad response json")
}
if !resp.Ok {
return nil, errors.Errorf("api error: %s", resp.Description)
}
for attachName, _ := range files {
i, _ := strconv.Atoi(attachName)
var newID string
if resp.Result[i].Photo != nil {
newID = resp.Result[i].Photo.FileID
} else {
newID = resp.Result[i].Video.FileID
}
a[i].MediaFile().FileID = newID
}
return resp.Result, nil
}
// Reply behaves just like Send() with an exception of "reply-to" indicator.
func (b *Bot) Reply(to *Message, what interface{}, options ...interface{}) (*Message, error) {
// This function will panic upon unsupported payloads and options!
sendOpts := extractOptions(options)
if sendOpts == nil {
sendOpts = &SendOptions{}
}
sendOpts.ReplyTo = to
return b.Send(to.Chat, what, sendOpts)
}
// Forward behaves just like Send() but of all options it
// only supports Silent (see Bots API).
//
// This function will panic upon unsupported payloads and options!
func (b *Bot) Forward(to Recipient, what *Message, options ...interface{}) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
"from_chat_id": what.Chat.Recipient(),
"message_id": strconv.Itoa(what.ID),
}
sendOpts := extractOptions(options)
embedSendOptions(params, sendOpts)
respJSON, err := b.Raw("forwardMessage", params)
if err != nil {
return nil, err
}
return extractMsgResponse(respJSON)
}
// Edit is magic, it lets you change already sent message.
//
// Use cases:
//
// b.Edit(msg, msg.Text, newMarkup)
// b.Edit(msg, "new <b>text</b>", tb.ModeHTML)
//
// // Edit live location:
// b.Edit(liveMsg, tb.Location{42.1337, 69.4242})
//
func (b *Bot) Edit(message Editable, what interface{}, options ...interface{}) (*Message, error) {
messageID, chatID := message.MessageSig()
params := map[string]string{}
switch v := what.(type) {
case string:
params["text"] = v
case Location:
params["latitude"] = fmt.Sprintf("%f", v.Lat)
params["longitude"] = fmt.Sprintf("%f", v.Lng)
default:
panic("telebot: unsupported what argument")
}
// if inline message
if chatID == 0 {
params["inline_message_id"] = messageID
} else {
params["chat_id"] = strconv.FormatInt(chatID, 10)
params["message_id"] = messageID
}
sendOpts := extractOptions(options)
embedSendOptions(params, sendOpts)
respJSON, err := b.Raw("editMessageText", params)
if err != nil {
return nil, err
}
return extractMsgResponse(respJSON)
}
// EditCaption used to edit already sent photo caption with known recepient and message id.
//
// On success, returns edited message object
func (b *Bot) EditCaption(originalMsg Editable, caption string) (*Message, error) {
messageID, chatID := originalMsg.MessageSig()
params := map[string]string{"caption": caption}
// if inline message
if chatID == 0 {
params["inline_message_id"] = messageID
} else {
params["chat_id"] = strconv.FormatInt(chatID, 10)
params["message_id"] = messageID
}
respJSON, err := b.Raw("editMessageCaption", params)
if err != nil {
return nil, err
}
return extractMsgResponse(respJSON)
}
// EditMedia used to edit already sent media with known recepient and message id.
//
// Use cases:
//
// bot.EditMedia(msg, &tb.Photo{File: tb.FromDisk("chicken.jpg")});
// bot.EditMedia(msg, &tb.Video{File: tb.FromURL("http://video.mp4")});
//
func (b *Bot) EditMedia(message Editable, inputMedia InputMedia, options ...interface{}) (*Message, error) {
var mediaRepr string;
var jsonRepr []byte;
var thumb *Photo;
file := make(map[string]File);
f := inputMedia.MediaFile();
if f.InCloud() {
mediaRepr = f.FileID;
} else if f.FileURL != "" {
mediaRepr = f.FileURL;
} else if f.OnDisk() || f.FileReader != nil {
s := f.FileLocal;
if (f.FileReader != nil) {
s = "0";
}
mediaRepr = "attach://" + s;
file[s] = *f;
} else {
return nil, errors.Errorf(
"telebot: can't edit media, it doesn't exist anywhere");
}
type FileJson struct {
// All types.
Type string `json:"type"`
Caption string `json:"caption"`
Media string `json:"media"`
// Video.
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
SupportsStreaming bool `json:"supports_streaming,omitempty"`
// Video and audio.
Duration int `json:"duration,omitempty"`
// Document.
FileName string `json:"file_name"`
// Document, video and audio.
Thumbnail string `json:"thumb,omitempty"`
MIME string `json:"mime_type,omitempty"`
// Audio.
Title string `json:"title,omitempty"`
Performer string `json:"performer,omitempty"`
}
resultMedia := &FileJson {Media: mediaRepr};
switch y := inputMedia.(type) {
case *Photo:
resultMedia.Type = "photo";
resultMedia.Caption = y.Caption;
case *Video:
resultMedia.Type = "video";
resultMedia.Caption = y.Caption;
resultMedia.Width = y.Width;
resultMedia.Height = y.Height;
resultMedia.Duration = y.Duration;
resultMedia.SupportsStreaming = y.SupportsStreaming;
resultMedia.MIME = y.MIME;
thumb = y.Thumbnail;
if thumb != nil {
resultMedia.Thumbnail = "attach://thumb";
}
case *Document:
resultMedia.Type = "document";
resultMedia.Caption = y.Caption;
resultMedia.FileName = y.FileName;
resultMedia.MIME = y.MIME;
thumb = y.Thumbnail;
if thumb != nil {
resultMedia.Thumbnail = "attach://thumb";
}
case *Audio:
resultMedia.Type = "audio";
resultMedia.Caption = y.Caption;
resultMedia.Duration = y.Duration;
resultMedia.MIME = y.MIME;
resultMedia.Title = y.Title;
resultMedia.Performer = y.Performer;
default:
return nil, errors.Errorf("telebot: inputMedia entry is not valid");
}
messageID, chatID := message.MessageSig();
jsonRepr, _ = json.Marshal(resultMedia);
params := map[string]string{};
params["media"] = string(jsonRepr);
// If inline message.
if chatID == 0 {
params["inline_message_id"] = messageID;
} else {
params["chat_id"] = strconv.FormatInt(chatID, 10);
params["message_id"] = messageID;
}
if thumb != nil {
file["thumb"] = *thumb.MediaFile();
}
sendOpts := extractOptions(options);
embedSendOptions(params, sendOpts);
respJSON, err := b.sendFiles("editMessageMedia", file, params);
if err != nil {
return nil, err;
}
return extractMsgResponse(respJSON);
}
// Delete removes the message, including service messages,
// with the following limitations:
//
// * A message can only be deleted if it was sent less than 48 hours ago.
// * Bots can delete outgoing messages in groups and supergroups.
// * Bots granted can_post_messages permissions can delete outgoing
// messages in channels.
// * If the bot is an administrator of a group, it can delete any message there.
// * If the bot has can_delete_messages permission in a supergroup or a
// channel, it can delete any message there.
//
func (b *Bot) Delete(message Editable) error {
messageID, chatID := message.MessageSig()
params := map[string]string{
"chat_id": strconv.FormatInt(chatID, 10),
"message_id": messageID,
}
respJSON, err := b.Raw("deleteMessage", params)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// Notify updates the chat action for recipient.
//
// Chat action is a status message that recipient would see where
// you typically see "Harry is typing" status message. The only
// difference is that bots' chat actions live only for 5 seconds
// and die just once the client recieves a message from the bot.
//
// Currently, Telegram supports only a narrow range of possible
// actions, these are aligned as constants of this package.
func (b *Bot) Notify(recipient Recipient, action ChatAction) error {
params := map[string]string{
"chat_id": recipient.Recipient(),
"action": string(action),
}
respJSON, err := b.Raw("sendChatAction", params)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// Accept finalizes the deal.
func (b *Bot) Accept(query *PreCheckoutQuery, errorMessage ...string) error {
params := map[string]string{
"pre_checkout_query_id": query.ID,
}
if len(errorMessage) == 0 {
params["ok"] = "True"
} else {
params["ok"] = "False"
params["error_message"] = errorMessage[0]
}
respJSON, err := b.Raw("answerPreCheckoutQuery", params)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// Answer sends a response for a given inline query. A query can only
// be responded to once, subsequent attempts to respond to the same query
// will result in an error.
func (b *Bot) Answer(query *Query, response *QueryResponse) error {
response.QueryID = query.ID
for _, result := range response.Results {
result.Process()
}
respJSON, err := b.Raw("answerInlineQuery", response)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// Respond sends a response for a given callback query. A callback can
// only be responded to once, subsequent attempts to respond to the same callback
// will result in an error.
//
// Example:
//
// bot.Respond(c)
// bot.Respond(c, response)
//
func (b *Bot) Respond(callback *Callback, responseOptional ...*CallbackResponse) error {
var response *CallbackResponse
if responseOptional == nil {
response = &CallbackResponse{}
} else {
response = responseOptional[0]
}
response.CallbackID = callback.ID
respJSON, err := b.Raw("answerCallbackQuery", response)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// FileByID returns full file object including File.FilePath, allowing you to
// download the file from the server.
//
// Usually, Telegram-provided File objects miss FilePath so you might need to
// perform an additional request to fetch them.
func (b *Bot) FileByID(fileID string) (File, error) {
params := map[string]string{
"file_id": fileID,
}
respJSON, err := b.Raw("getFile", params)
if err != nil {
return File{}, err
}
var resp struct {
Ok bool
Description string
Result File
}
err = json.Unmarshal(respJSON, &resp)
if err != nil {
return File{}, errors.Wrap(err, "bad response json")
}
if !resp.Ok {
return File{}, errors.Errorf("api error: %s", resp.Description)
}
return resp.Result, nil
}
// Download saves the file from Telegram servers locally.
//
// Maximum file size to download is 20 MB.
func (b *Bot) Download(file *File, localFilename string) error {
reader, err := b.GetFile(file)
if err != nil {
return wrapSystem(err)
}
defer reader.Close()
out, err := os.Create(localFilename)
if err != nil {
return wrapSystem(err)
}
defer out.Close()
_, err = io.Copy(out, reader)
if err != nil {
return wrapSystem(err)
}
file.FileLocal = localFilename
return nil
}
// GetFile from Telegram servers
func (b *Bot) GetFile(file *File) (io.ReadCloser, error) {
f, err := b.FileByID(file.FileID)
if err != nil {
return nil, err
}
url := fmt.Sprintf("%s/file/bot%s/%s",
b.URL, b.Token, f.FilePath)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
// set FilePath
*file = f
return resp.Body, nil
}
// StopLiveLocation should be called to stop broadcasting live message location
// before Location.LivePeriod expires.
//
// It supports telebot.ReplyMarkup.
func (b *Bot) StopLiveLocation(message Editable, options ...interface{}) (*Message, error) {
messageID, chatID := message.MessageSig()
params := map[string]string{
"chat_id": fmt.Sprintf("%d", chatID),
"message_id": messageID,
}
sendOpts := extractOptions(options)
embedSendOptions(params, sendOpts)
respJSON, err := b.Raw("stopMessageLiveLocation", params)
if err != nil {
return nil, err
}
return extractMsgResponse(respJSON)
}
// GetInviteLink should be used to export chat's invite link.
func (b *Bot) GetInviteLink(chat *Chat) (string, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
}
respJSON, err := b.Raw("exportChatInviteLink", params)
if err != nil {
return "", err
}
var resp struct {
Ok bool
Description string
Result string
}
err = json.Unmarshal(respJSON, &resp)
if err != nil {
return "", errors.Wrap(err, "bad response json")
}
if !resp.Ok {
return "", errors.Errorf("api error: %s", resp.Description)
}
return resp.Result, nil
}
// SetChatTitle should be used to update group title.
func (b *Bot) SetGroupTitle(chat *Chat, newTitle string) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"title": newTitle,
}
respJSON, err := b.Raw("setChatTitle", params)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// SetGroupDescription should be used to update group title.
func (b *Bot) SetGroupDescription(chat *Chat, description string) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"description": description,
}
respJSON, err := b.Raw("setChatDescription", params)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// SetGroupPhoto should be used to update group photo.
func (b *Bot) SetGroupPhoto(chat *Chat, p *Photo) error {
params := map[string]string{
"chat_id": chat.Recipient(),
}
respJSON, err := b.sendFiles("setChatPhoto", map[string]File{"photo": p.File}, params)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// SetGroupStickerSet should be used to update group's group sticker set.
func (b *Bot) SetGroupStickerSet(chat *Chat, setName string) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"sticker_set_name": setName,
}
respJSON, err := b.Raw("setChatStickerSet", params)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// DeleteGroupPhoto should be used to just remove group photo.
func (b *Bot) DeleteGroupPhoto(chat *Chat) error {
params := map[string]string{
"chat_id": chat.Recipient(),
}
respJSON, err := b.Raw("deleteGroupPhoto", params)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// DeleteGroupStickerSet should be used to just remove group sticker set.
func (b *Bot) DeleteGroupStickerSet(chat *Chat) error {
params := map[string]string{
"chat_id": chat.Recipient(),
}
respJSON, err := b.Raw("deleteChatStickerSet", params)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// Leave makes bot leave a group, supergroup or channel.
func (b *Bot) Leave(chat *Chat) error {
params := map[string]string{
"chat_id": chat.Recipient(),
}
respJSON, err := b.Raw("leaveChat", params)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// Use this method to pin a message in a supergroup or a channel.
//
// It supports telebot.Silent option.
func (b *Bot) Pin(message Editable, options ...interface{}) error {
messageID, chatID := message.MessageSig()
params := map[string]string{
"chat_id": strconv.FormatInt(chatID, 10),
"message_id": messageID,
}
sendOpts := extractOptions(options)
embedSendOptions(params, sendOpts)
respJSON, err := b.Raw("pinChatMessage", params)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// Use this method to unpin a message in a supergroup or a channel.
//
// It supports telebot.Silent option.
func (b *Bot) Unpin(chat *Chat) error {
params := map[string]string{
"chat_id": chat.Recipient(),
}
respJSON, err := b.Raw("unpinChatMessage", params)
if err != nil {
return err
}
return extractOkResponse(respJSON)
}
// ChatByID fetches chat info of its ID.
//
// Including current name of the user for one-on-one conversations,
// current username of a user, group or channel, etc.
//
// Returns a Chat object on success.
func (b *Bot) ChatByID(id string) (*Chat, error) {
params := map[string]string{
"chat_id": id,
}
respJSON, err := b.Raw("getChat", params)
if err != nil {
return nil, err
}
var resp struct {
Ok bool
Description string
Result *Chat
}
err = json.Unmarshal(respJSON, &resp)
if err != nil {
return nil, errors.Wrap(err, "bad response json")
}
if !resp.Ok {
return nil, errors.Errorf("api error: %s", resp.Description)
}
if resp.Result.Type == ChatChannel && resp.Result.Username == "" {
//Channel is Private
resp.Result.Type = ChatChannelPrivate
}
return resp.Result, nil
}
// ProfilePhotosOf return list of profile pictures for a user.
func (b *Bot) ProfilePhotosOf(user *User) ([]Photo, error) {
params := map[string]string{
"user_id": user.Recipient(),
}
respJSON, err := b.Raw("getUserProfilePhotos", params)
if err != nil {
return nil, err
}
var resp struct {
Ok bool
Result struct {
Count int `json:"total_count"`
Photos []Photo `json:"photos"`
}
Description string `json:"description"`
}
err = json.Unmarshal(respJSON, &resp)
if err != nil {
return nil, errors.Wrap(err, "bad response json")
}
if !resp.Ok {
return nil, errors.Errorf("api error: %s", resp.Description)
}
return resp.Result.Photos, nil
}
// ChatMemberOf return information about a member of a chat.
//
// Returns a ChatMember object on success.
func (b *Bot) ChatMemberOf(chat *Chat, user *User) (*ChatMember, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
"user_id": user.Recipient(),
}
respJSON, err := b.Raw("getChatMember", params)
if err != nil {
return nil, err
}
var resp struct {
Ok bool
Result *ChatMember
Description string `json:"description"`
}
err = json.Unmarshal(respJSON, &resp)
if err != nil {
return nil, errors.Wrap(err, "bad response json")
}
if !resp.Ok {
return nil, errors.Errorf("api error: %s", resp.Description)
}
return resp.Result, nil
}
// FileURLByID returns direct url for files using FileId which you can get from File object
func (b *Bot) FileURLByID(fileID string) (string, error) {
f, err := b.FileByID(fileID)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/file/bot%s/%s", b.URL, b.Token, f.FilePath), nil
}