mirror of
https://github.com/tucnak/telebot
synced 2024-11-05 06:00:58 +00:00
608 lines
15 KiB
Go
608 lines
15 KiB
Go
package telebot
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"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
|
|
}
|
|
|
|
bot := &Bot{
|
|
Token: pref.Token,
|
|
Updates: make(chan Update, pref.Updates),
|
|
Poller: pref.Poller,
|
|
|
|
handlers: make(map[string]interface{}),
|
|
}
|
|
|
|
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
|
|
Updates chan Update
|
|
Poller Poller
|
|
Errors chan error
|
|
|
|
handlers map[string]interface{}
|
|
}
|
|
|
|
// Settings represents a utility struct for passing certain
|
|
// properties of a bot around and is required to make bots.
|
|
type Settings struct {
|
|
// Telegram token
|
|
Token string
|
|
|
|
// Updates channel capacity
|
|
Updates int // Default: 100
|
|
|
|
// Poller is the provider of Updates.
|
|
Poller Poller
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// Handle lets you set the handler for some command name or
|
|
// one of the supported endpoints.
|
|
//
|
|
// Example:
|
|
//
|
|
// tb.Handle("/help", func (m *tb.Message) {})
|
|
// tb.Handle(tb.OnEditedMessage, func (m *tb.Message) {})
|
|
// tb.Handle(tb.OnQuery, func (q *tb.Query) {})
|
|
//
|
|
func (b *Bot) Handle(endpoint string, handler interface{}) {
|
|
b.handlers[endpoint] = handler
|
|
}
|
|
|
|
var cmdRx = regexp.MustCompile(`^(\/\w+)(@(\w+))?(\s|$)`)
|
|
|
|
func (b *Bot) handleCommand(m *Message, cmdName, cmdBot string) bool {
|
|
// Group-syntax: "/cmd@bot"
|
|
if cmdBot != "" && !strings.EqualFold(b.Me.Username, cmdBot) {
|
|
return false
|
|
}
|
|
|
|
if handler, ok := b.handlers[cmdName]; ok {
|
|
if handler, ok := handler.(func(*Message)); ok {
|
|
go handler(m)
|
|
return true
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
go b.Poller.Poll(b, b.Updates)
|
|
|
|
for upd := range b.Updates {
|
|
if upd.Message != nil {
|
|
m := upd.Message
|
|
|
|
// Text message
|
|
if m.Text != "" {
|
|
if m.Text[0] == '\a' {
|
|
continue
|
|
}
|
|
|
|
match := cmdRx.FindAllStringSubmatch(m.Text, -1)
|
|
|
|
// Command found
|
|
if match != nil {
|
|
if b.handleCommand(m, match[0][1], match[0][3]) {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// OnMessage
|
|
if handler, ok := b.handlers[string(OnMessage)]; ok {
|
|
if handler, ok := handler.(func(*Message)); ok {
|
|
go handler(m)
|
|
continue
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if upd.EditedMessage != nil {
|
|
if handler, ok := b.handlers[OnEditedMessage]; ok {
|
|
if handler, ok := handler.(func(*Message)); ok {
|
|
handler(upd.EditedMessage)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if upd.ChannelPost != nil {
|
|
if handler, ok := b.handlers[OnChannelPost]; ok {
|
|
if handler, ok := handler.(func(*Message)); ok {
|
|
handler(upd.ChannelPost)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if upd.EditedChannelPost != nil {
|
|
if handler, ok := b.handlers[OnEditedChannelPost]; ok {
|
|
if handler, ok := handler.(func(*Message)); ok {
|
|
handler(upd.EditedChannelPost)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if upd.Callback != nil {
|
|
if handler, ok := b.handlers[OnCallback]; ok {
|
|
if handler, ok := handler.(func(*Callback)); ok {
|
|
handler(upd.Callback)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if upd.Query != nil {
|
|
if handler, ok := b.handlers[OnQuery]; ok {
|
|
if handler, ok := handler.(func(*Query)); ok {
|
|
handler(upd.Query)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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{}, make sure to pass
|
|
// them by-pointer, NOT by-value, which will result in a panic.
|
|
//
|
|
// 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:
|
|
panic(fmt.Sprintf("telebot: object %v is not Sendable", object))
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
if sendOpts == nil {
|
|
sendOpts = &SendOptions{}
|
|
}
|
|
embedSendOptions(params, sendOpts)
|
|
|
|
respJSON, err := b.sendCommand("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)
|
|
//
|
|
func (b *Bot) Edit(originalMsg Editable, text string, options ...interface{}) (*Message, error) {
|
|
messageID, chatID := originalMsg.MessageSig()
|
|
// TODO: add support for inline messages (chatID = 0)
|
|
|
|
params := map[string]string{"text": text}
|
|
|
|
// if inline message
|
|
if chatID == 0 {
|
|
params["inline_message_id"] = strconv.Itoa(messageID)
|
|
} else {
|
|
params["chat_id"] = strconv.FormatInt(chatID, 10)
|
|
params["message_id"] = strconv.Itoa(messageID)
|
|
}
|
|
|
|
sendOpts := extractOptions(options)
|
|
embedSendOptions(params, sendOpts)
|
|
|
|
respJSON, err := b.sendCommand("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"] = strconv.Itoa(messageID)
|
|
} else {
|
|
params["chat_id"] = strconv.FormatInt(chatID, 10)
|
|
params["message_id"] = strconv.Itoa(messageID)
|
|
}
|
|
|
|
respJSON, err := b.sendCommand("editMessageCaption", 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": strconv.Itoa(messageID),
|
|
}
|
|
|
|
respJSON, err := b.sendCommand("deleteMessage", params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return extractOkResponse(respJSON)
|
|
}
|
|
|
|
// SendChatAction updates a 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) SendChatAction(recipient Recipient, action ChatAction) error {
|
|
params := map[string]string{
|
|
"chat_id": recipient.Recipient(),
|
|
"action": string(action),
|
|
}
|
|
|
|
respJSON, err := b.sendCommand("sendChatAction", params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return extractOkResponse(respJSON)
|
|
}
|
|
|
|
// AnswerInlineQuery 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) AnswerInlineQuery(query *Query, response *QueryResponse) error {
|
|
response.QueryID = query.ID
|
|
|
|
respJSON, err := b.sendCommand("answerInlineQuery", response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return extractOkResponse(respJSON)
|
|
}
|
|
|
|
// AnswerCallbackQuery 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.
|
|
func (b *Bot) AnswerCallbackQuery(callback *Callback, response *CallbackResponse) error {
|
|
response.CallbackID = callback.ID
|
|
|
|
respJSON, err := b.sendCommand("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.sendCommand("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
|
|
}
|
|
|
|
// LeaveChat makes bot leave a group, supergroup or channel.
|
|
func (b *Bot) LeaveChat(recipient Recipient) error {
|
|
params := map[string]string{
|
|
"chat_id": recipient.Recipient(),
|
|
}
|
|
|
|
respJSON, err := b.sendCommand("leaveChat", 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.sendCommand("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)
|
|
}
|
|
|
|
return resp.Result, nil
|
|
}
|
|
|
|
// AdminsOf return a member list of chat admins.
|
|
//
|
|
// On success, returns an Array of ChatMember objects that
|
|
// contains information about all chat administrators except other bots.
|
|
// If the chat is a group or a supergroup and
|
|
// no administrators were appointed, only the creator will be returned.
|
|
func (b *Bot) AdminsOf(chat *Chat) ([]ChatMember, error) {
|
|
params := map[string]string{
|
|
"chat_id": chat.Recipient(),
|
|
}
|
|
|
|
respJSON, err := b.sendCommand("getChatAdministrators", 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
|
|
}
|
|
|
|
// Len return the number of members in a chat.
|
|
func (b *Bot) Len(chat *Chat) (int, error) {
|
|
params := map[string]string{
|
|
"chat_id": chat.Recipient(),
|
|
}
|
|
|
|
respJSON, err := b.sendCommand("getChatMembersCount", params)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var resp struct {
|
|
Ok bool
|
|
Result int
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
err = json.Unmarshal(respJSON, &resp)
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "bad response json")
|
|
}
|
|
|
|
if !resp.Ok {
|
|
return 0, errors.Errorf("api error: %s", resp.Description)
|
|
}
|
|
|
|
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.sendCommand("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.sendCommand("getChatMember", params)
|
|
if err != nil {
|
|
return ChatMember{}, err
|
|
}
|
|
|
|
var resp struct {
|
|
Ok bool
|
|
Result ChatMember
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
err = json.Unmarshal(respJSON, &resp)
|
|
if err != nil {
|
|
return ChatMember{}, errors.Wrap(err, "bad response json")
|
|
}
|
|
|
|
if !resp.Ok {
|
|
return ChatMember{}, 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 "https://api.telegram.org/file/bot" + b.Token + "/" + f.FilePath, nil
|
|
}
|