telebot: restructure and refactor

pull/558/head
Demian 2 years ago
parent bd66ef87a9
commit d90e8974cc

@ -6,71 +6,6 @@ import (
"time"
)
// ChatInviteLink object represents an invite for a chat.
type ChatInviteLink struct {
// The invite link.
InviteLink string `json:"invite_link"`
// Invite link name.
Name string `json:"name"`
// The creator of the link.
Creator *User `json:"creator"`
// If the link is primary.
IsPrimary bool `json:"is_primary"`
// If the link is revoked.
IsRevoked bool `json:"is_revoked"`
// (Optional) Point in time when the link will expire,
// use ExpireDate() to get time.Time.
ExpireUnixtime int64 `json:"expire_date,omitempty"`
// (Optional) Maximum number of users that can be members of
// the chat simultaneously.
MemberLimit int `json:"member_limit,omitempty"`
// (Optional) True, if users joining the chat via the link need to
// be approved by chat administrators. If True, member_limit can't be specified.
JoinRequest bool `json:"creates_join_request"`
// (Optional) Number of pending join requests created using this link.
PendingCount int `json:"pending_join_request_count"`
}
// ExpireDate returns the moment of the link expiration in local time.
func (c *ChatInviteLink) ExpireDate() time.Time {
return time.Unix(c.ExpireUnixtime, 0)
}
// ChatMemberUpdate object represents changes in the status of a chat member.
type ChatMemberUpdate struct {
// Chat where the user belongs to.
Chat *Chat `json:"chat"`
// Sender which user the action was triggered.
Sender *User `json:"from"`
// Unixtime, use Date() to get time.Time.
Unixtime int64 `json:"date"`
// Previous information about the chat member.
OldChatMember *ChatMember `json:"old_chat_member"`
// New information about the chat member.
NewChatMember *ChatMember `json:"new_chat_member"`
// (Optional) InviteLink which was used by the user to
// join the chat; for joining by invite link events only.
InviteLink *ChatInviteLink `json:"invite_link"`
}
// Time returns the moment of the change in local time.
func (c *ChatMemberUpdate) Time() time.Time {
return time.Unix(c.Unixtime, 0)
}
// Rights is a list of privileges available to chat members.
type Rights struct {
// Anonymous is true, if the user's presence in the chat is hidden.
@ -345,3 +280,8 @@ func (b *Bot) SetDefaultRights(rights Rights, forChannels bool) error {
_, err := b.Raw("setMyDefaultAdministratorRights", params)
return err
}
func embedRights(p map[string]interface{}, rights Rights) {
data, _ := json.Marshal(rights)
_ = json.Unmarshal(data, &p)
}

@ -0,0 +1,39 @@
package telebot
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEmbedRights(t *testing.T) {
rights := NoRestrictions()
params := map[string]interface{}{
"chat_id": "1",
"user_id": "2",
}
embedRights(params, rights)
expected := map[string]interface{}{
"is_anonymous": false,
"chat_id": "1",
"user_id": "2",
"can_be_edited": true,
"can_send_messages": true,
"can_send_media_messages": true,
"can_send_polls": true,
"can_send_other_messages": true,
"can_add_web_page_previews": true,
"can_change_info": false,
"can_post_messages": false,
"can_edit_messages": false,
"can_delete_messages": false,
"can_invite_users": false,
"can_restrict_members": false,
"can_pin_messages": false,
"can_promote_members": false,
"can_manage_video_chats": false,
"can_manage_chat": false,
}
assert.Equal(t, expected, params)
}

@ -7,6 +7,7 @@ import (
"fmt"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
@ -237,3 +238,94 @@ func (b *Bot) getUpdates(offset, limit int, timeout time.Duration, allowed []str
}
return resp.Result, nil
}
// extractOk checks given result for error. If result is ok returns nil.
// In other cases it extracts API error. If error is not presented
// in errors.go, it will be prefixed with `unknown` keyword.
func extractOk(data []byte) error {
var e struct {
Ok bool `json:"ok"`
Code int `json:"error_code"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
}
if json.NewDecoder(bytes.NewReader(data)).Decode(&e) != nil {
return nil // FIXME
}
if e.Ok {
return nil
}
err := Err(e.Description)
switch err {
case nil:
case ErrGroupMigrated:
migratedTo, ok := e.Parameters["migrate_to_chat_id"]
if !ok {
return NewError(e.Code, e.Description)
}
return GroupError{
err: err.(*Error),
MigratedTo: int64(migratedTo.(float64)),
}
default:
return err
}
switch e.Code {
case http.StatusTooManyRequests:
retryAfter, ok := e.Parameters["retry_after"]
if !ok {
return NewError(e.Code, e.Description)
}
err = FloodError{
err: NewError(e.Code, e.Description),
RetryAfter: int(retryAfter.(float64)),
}
default:
err = fmt.Errorf("telegram: %s (%d)", e.Description, e.Code)
}
return err
}
// extractMessage extracts common Message result from given data.
// Should be called after extractOk or b.Raw() to handle possible errors.
func extractMessage(data []byte) (*Message, error) {
var resp struct {
Result *Message
}
if err := json.Unmarshal(data, &resp); err != nil {
var resp struct {
Result bool
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
if resp.Result {
return nil, ErrTrueResult
}
return nil, wrapError(err)
}
return resp.Result, nil
}
func verbose(method string, payload interface{}, data []byte) {
body, _ := json.Marshal(payload)
body = bytes.ReplaceAll(body, []byte(`\"`), []byte(`"`))
body = bytes.ReplaceAll(body, []byte(`"{`), []byte(`{`))
body = bytes.ReplaceAll(body, []byte(`}"`), []byte(`}`))
indent := func(b []byte) string {
var buf bytes.Buffer
json.Indent(&buf, b, "", " ")
return buf.String()
}
log.Printf(
"[verbose] telebot: sent request\nMethod: %v\nParams: %v\nResponse: %v",
method, indent(body), indent(data),
)
}

@ -9,6 +9,8 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
)
@ -72,3 +74,47 @@ func TestRaw(t *testing.T) {
_, err = b.Raw("testUnknownError", nil)
assert.EqualError(t, err, "telegram: unknown error (400)")
}
func TestExtractOk(t *testing.T) {
data := []byte(`{"ok": true, "result": {}}`)
require.NoError(t, extractOk(data))
data = []byte(`{
"ok": false,
"error_code": 400,
"description": "Bad Request: reply message not found"
}`)
assert.EqualError(t, extractOk(data), ErrNotFoundToReply.Error())
data = []byte(`{
"ok": false,
"error_code": 429,
"description": "Too Many Requests: retry after 8",
"parameters": {"retry_after": 8}
}`)
assert.Equal(t, FloodError{
err: NewError(429, "Too Many Requests: retry after 8"),
RetryAfter: 8,
}, extractOk(data))
data = []byte(`{
"ok": false,
"error_code": 400,
"description": "Bad Request: group chat was upgraded to a supergroup chat",
"parameters": {"migrate_to_chat_id": -100123456789}
}`)
assert.Equal(t, GroupError{
err: ErrGroupMigrated,
MigratedTo: -100123456789,
}, extractOk(data))
}
func TestExtractMessage(t *testing.T) {
data := []byte(`{"ok":true,"result":true}`)
_, err := extractMessage(data)
assert.Equal(t, ErrTrueResult, err)
data = []byte(`{"ok":true,"result":{"foo":"bar"}}`)
_, err = extractMessage(data)
require.NoError(t, err)
}

493
bot.go

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"regexp"
@ -120,40 +121,24 @@ type Settings struct {
Offline bool
}
// 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"`
InlineResult *InlineResult `json:"chosen_inline_result,omitempty"`
ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"`
PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"`
Poll *Poll `json:"poll,omitempty"`
PollAnswer *PollAnswer `json:"poll_answer,omitempty"`
MyChatMember *ChatMemberUpdate `json:"my_chat_member,omitempty"`
ChatMember *ChatMemberUpdate `json:"chat_member,omitempty"`
ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"`
}
// Command represents a bot command.
type Command struct {
// Text is a text of the command, 1-32 characters.
// Can contain only lowercase English letters, digits and underscores.
Text string `json:"command"`
// Description of the command, 3-256 characters.
Description string `json:"description"`
var defaultOnError = func(err error, c Context) {
if c != nil {
log.Println(c.Update().ID, err)
} else {
log.Println(err)
}
}
func (b *Bot) OnError(err error, c Context) {
b.onError(err, c)
}
func (b *Bot) debug(err error) {
if b.verbose {
b.OnError(err, nil)
}
}
// Group returns a new group.
func (b *Bot) Group() *Group {
return &Group{b: b}
@ -164,6 +149,11 @@ func (b *Bot) Use(middleware ...MiddlewareFunc) {
b.group.Use(middleware...)
}
var (
cmdRx = regexp.MustCompile(`^(/\w+)(@(\w+))?(\s|$)(.+)?`)
cbackRx = regexp.MustCompile(`^\f([-\w]+)(\|(.+))?$`)
)
// Handle lets you set the handler for some command name or
// one of the supported endpoints. It also applies middleware
// if such passed to the function.
@ -201,11 +191,6 @@ func (b *Bot) Handle(endpoint interface{}, h HandlerFunc, m ...MiddlewareFunc) {
}
}
var (
cmdRx = regexp.MustCompile(`^(/\w+)(@(\w+))?(\s|$)(.+)?`)
cbackRx = regexp.MustCompile(`^\f([-\w]+)(\|(.+))?$`)
)
// Start brings bot into motion by consuming incoming
// updates (see Bot.Updates channel).
func (b *Bot) Start() {
@ -267,307 +252,6 @@ func (b *Bot) NewContext(u Update) Context {
}
}
// ProcessUpdate processes a single incoming update.
// A started bot calls this function automatically.
func (b *Bot) ProcessUpdate(u Update) {
c := b.NewContext(u)
if u.Message != nil {
m := u.Message
if m.PinnedMessage != nil {
b.handle(OnPinned, c)
return
}
// Commands
if m.Text != "" {
// Filtering malicious messages
if m.Text[0] == '\a' {
return
}
match := cmdRx.FindAllStringSubmatch(m.Text, -1)
if match != nil {
// Syntax: "</command>@<bot> <payload>"
command, botName := match[0][1], match[0][3]
if botName != "" && !strings.EqualFold(b.Me.Username, botName) {
return
}
m.Payload = match[0][5]
if b.handle(command, c) {
return
}
}
// 1:1 satisfaction
if b.handle(m.Text, c) {
return
}
b.handle(OnText, c)
return
}
if b.handleMedia(c) {
return
}
if m.Contact != nil {
b.handle(OnContact, c)
return
}
if m.Location != nil {
b.handle(OnLocation, c)
return
}
if m.Venue != nil {
b.handle(OnVenue, c)
return
}
if m.Game != nil {
b.handle(OnGame, c)
return
}
if m.Dice != nil {
b.handle(OnDice, c)
return
}
if m.Invoice != nil {
b.handle(OnInvoice, c)
return
}
if m.Payment != nil {
b.handle(OnPayment, c)
return
}
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, c)
return
}
if m.UserJoined != nil {
b.handle(OnUserJoined, c)
return
}
if m.UsersJoined != nil {
for _, user := range m.UsersJoined {
m.UserJoined = &user
b.handle(OnUserJoined, c)
}
return
}
if m.UserLeft != nil {
b.handle(OnUserLeft, c)
return
}
if m.NewGroupTitle != "" {
b.handle(OnNewGroupTitle, c)
return
}
if m.NewGroupPhoto != nil {
b.handle(OnNewGroupPhoto, c)
return
}
if m.GroupPhotoDeleted {
b.handle(OnGroupPhotoDeleted, c)
return
}
if m.GroupCreated {
b.handle(OnGroupCreated, c)
return
}
if m.SuperGroupCreated {
b.handle(OnSuperGroupCreated, c)
return
}
if m.ChannelCreated {
b.handle(OnChannelCreated, c)
return
}
if m.MigrateTo != 0 {
m.MigrateFrom = m.Chat.ID
b.handle(OnMigration, c)
return
}
if m.VideoChatStarted != nil {
b.handle(OnVideoChatStarted, c)
return
}
if m.VideoChatEnded != nil {
b.handle(OnVideoChatEnded, c)
return
}
if m.VideoChatParticipants != nil {
b.handle(OnVideoChatParticipants, c)
return
}
if m.VideoChatScheduled != nil {
b.handle(OnVideoChatScheduled, c)
return
}
if m.WebAppData != nil {
b.handle(OnWebApp, c)
}
if m.ProximityAlert != nil {
b.handle(OnProximityAlert, c)
return
}
if m.AutoDeleteTimer != nil {
b.handle(OnAutoDeleteTimer, c)
return
}
}
if u.EditedMessage != nil {
b.handle(OnEdited, c)
return
}
if u.ChannelPost != nil {
m := u.ChannelPost
if m.PinnedMessage != nil {
b.handle(OnPinned, c)
return
}
b.handle(OnChannelPost, c)
return
}
if u.EditedChannelPost != nil {
b.handle(OnEditedChannelPost, c)
return
}
if u.Callback != nil {
if data := u.Callback.Data; data != "" && 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 {
u.Callback.Unique = unique
u.Callback.Data = payload
b.runHandler(handler, c)
return
}
}
}
b.handle(OnCallback, c)
return
}
if u.Query != nil {
b.handle(OnQuery, c)
return
}
if u.InlineResult != nil {
b.handle(OnInlineResult, c)
return
}
if u.ShippingQuery != nil {
b.handle(OnShipping, c)
return
}
if u.PreCheckoutQuery != nil {
b.handle(OnCheckout, c)
return
}
if u.Poll != nil {
b.handle(OnPoll, c)
return
}
if u.PollAnswer != nil {
b.handle(OnPollAnswer, c)
return
}
if u.MyChatMember != nil {
b.handle(OnMyChatMember, c)
return
}
if u.ChatMember != nil {
b.handle(OnChatMember, c)
return
}
if u.ChatJoinRequest != nil {
b.handle(OnChatJoinRequest, c)
return
}
}
func (b *Bot) handle(end string, c Context) bool {
if handler, ok := b.handlers[end]; ok {
b.runHandler(handler, c)
return true
}
return false
}
func (b *Bot) handleMedia(c Context) bool {
var (
m = c.Message()
fired = true
)
switch {
case m.Photo != nil:
fired = b.handle(OnPhoto, c)
case m.Voice != nil:
fired = b.handle(OnVoice, c)
case m.Audio != nil:
fired = b.handle(OnAudio, c)
case m.Animation != nil:
fired = b.handle(OnAnimation, c)
case m.Document != nil:
fired = b.handle(OnDocument, c)
case m.Sticker != nil:
fired = b.handle(OnSticker, c)
case m.Video != nil:
fired = b.handle(OnVideo, c)
case m.VideoNote != nil:
fired = b.handle(OnVideoNote, c)
default:
return false
}
if !fired {
return b.handle(OnMedia, c)
}
return true
}
// Send accepts 2+ arguments, starting with destination chat, followed by
// some Sendable (or string!) and optional send options.
//
@ -1268,115 +952,6 @@ func (b *Bot) StopPoll(msg Editable, opts ...interface{}) (*Poll, error) {
return resp.Result, nil
}
func (b *Bot) CreateInvoice(i Invoice) (string, error) {
data, err := b.Raw("createInvoiceLink", i.params())
if err != nil {
return "", err
}
var resp struct {
Result string
}
if err := json.Unmarshal(data, &resp); err != nil {
return "", wrapError(err)
}
return resp.Result, nil
}
// InviteLink should be used to export chat's invite link.
func (b *Bot) InviteLink(chat *Chat) (string, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
}
data, err := b.Raw("exportChatInviteLink", params)
if err != nil {
return "", err
}
var resp struct {
Result string
}
if err := json.Unmarshal(data, &resp); err != nil {
return "", wrapError(err)
}
return resp.Result, nil
}
// SetGroupTitle should be used to update group title.
func (b *Bot) SetGroupTitle(chat *Chat, title string) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"title": title,
}
_, err := b.Raw("setChatTitle", params)
return err
}
// SetGroupDescription should be used to update group description.
func (b *Bot) SetGroupDescription(chat *Chat, description string) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"description": description,
}
_, err := b.Raw("setChatDescription", params)
return err
}
// 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(),
}
_, err := b.sendFiles("setChatPhoto", map[string]File{"photo": p.File}, params)
return err
}
// 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,
}
_, err := b.Raw("setChatStickerSet", params)
return err
}
// SetGroupPermissions sets default chat permissions for all members.
func (b *Bot) SetGroupPermissions(chat *Chat, perms Rights) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
"permissions": perms,
}
_, err := b.Raw("setChatPermissions", params)
return err
}
// DeleteGroupPhoto should be used to just remove group photo.
func (b *Bot) DeleteGroupPhoto(chat *Chat) error {
params := map[string]string{
"chat_id": chat.Recipient(),
}
_, err := b.Raw("deleteChatPhoto", params)
return err
}
// 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(),
}
_, err := b.Raw("deleteChatStickerSet", params)
return err
}
// Leave makes bot leave a group, supergroup or channel.
func (b *Bot) Leave(chat *Chat) error {
params := map[string]string{
@ -1422,7 +997,6 @@ func (b *Bot) Unpin(chat *Chat, messageID ...int) error {
}
// UnpinAll unpins all messages in a supergroup or a channel.
//
// It supports tb.Silent option.
func (b *Bot) UnpinAll(chat *Chat) error {
params := map[string]string{
@ -1509,37 +1083,6 @@ func (b *Bot) ChatMemberOf(chat, user Recipient) (*ChatMember, error) {
return resp.Result, nil
}
// Commands returns the current list of the bot's commands for the given scope and user language.
func (b *Bot) Commands(opts ...interface{}) ([]Command, error) {
params := extractCommandsParams(opts...)
data, err := b.Raw("getMyCommands", params)
if err != nil {
return nil, err
}
var resp struct {
Result []Command
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
}
// SetCommands changes the list of the bot's commands.
func (b *Bot) SetCommands(opts ...interface{}) error {
params := extractCommandsParams(opts...)
_, err := b.Raw("setMyCommands", params)
return err
}
// DeleteCommands deletes the list of the bot's commands for the given scope and user language.
func (b *Bot) DeleteCommands(opts ...interface{}) error {
params := extractCommandsParams(opts...)
_, err := b.Raw("deleteMyCommands", params)
return err
}
// MenuButton returns the current value of the bot's menu button in a private chat,
// or the default menu button.
func (b *Bot) MenuButton(chat *User) (*MenuButton, error) {

@ -1,7 +1,5 @@
package telebot
import "encoding/json"
// CallbackEndpoint is an interface any element capable
// of responding to a callback `\f<unique>`.
type CallbackEndpoint interface {
@ -72,51 +70,9 @@ type CallbackResponse struct {
URL string `json:"url,omitempty"`
}
// InlineButton represents a button displayed in the message.
type InlineButton struct {
// Unique slagish name for this kind of button,
// try to be as specific as possible.
//
// It will be used as a callback endpoint.
Unique string `json:"unique,omitempty"`
Text string `json:"text"`
URL string `json:"url,omitempty"`
Data string `json:"callback_data,omitempty"`
InlineQuery string `json:"switch_inline_query,omitempty"`
InlineQueryChat string `json:"switch_inline_query_current_chat"`
WebApp *WebApp `json:"web_app,omitempty"`
Login *Login `json:"login_url,omitempty"`
}
// MarshalJSON implements json.Marshaler interface.
// It needed to avoid InlineQueryChat and Login fields conflict.
// If you have Login field in your button, InlineQueryChat must be skipped.
func (t *InlineButton) MarshalJSON() ([]byte, error) {
type IB InlineButton
if t.Login != nil {
return json.Marshal(struct {
IB
InlineQueryChat string `json:"switch_inline_query_current_chat,omitempty"`
}{
IB: IB(*t),
})
}
return json.Marshal(IB(*t))
}
// With returns a copy of the button with data.
func (t *InlineButton) With(data string) *InlineButton {
return &InlineButton{
Unique: t.Unique,
Text: t.Text,
URL: t.URL,
InlineQuery: t.InlineQuery,
InlineQueryChat: t.InlineQueryChat,
Login: t.Login,
Data: data,
}
// CallbackUnique returns ReplyButton.Text.
func (t *ReplyButton) CallbackUnique() string {
return t.Text
}
// CallbackUnique returns InlineButton.Unique.
@ -124,11 +80,6 @@ func (t *InlineButton) CallbackUnique() string {
return "\f" + t.Unique
}
// CallbackUnique returns KeyboardButton.Text.
func (t *ReplyButton) CallbackUnique() string {
return t.Text
}
// CallbackUnique implements CallbackEndpoint.
func (t *Btn) CallbackUnique() string {
if t.Unique != "" {
@ -136,13 +87,3 @@ func (t *Btn) CallbackUnique() string {
}
return t.Text
}
// Login represents a parameter of the inline keyboard button
// used to automatically authorize a user. Serves as a great replacement
// for the Telegram Login Widget when the user is coming from Telegram.
type Login struct {
URL string `json:"url"`
Text string `json:"forward_text,omitempty"`
Username string `json:"bot_username,omitempty"`
WriteAccess bool `json:"request_write_access,omitempty"`
}

@ -60,6 +60,23 @@ type Chat struct {
NoVoiceAndVideo bool `json:"has_restricted_voice_and_video_messages"`
}
// Recipient returns chat ID (see Recipient interface).
func (c *Chat) Recipient() string {
return strconv.FormatInt(c.ID, 10)
}
// ChatType represents one of the possible chat types.
type ChatType string
const (
ChatPrivate ChatType = "private"
ChatGroup ChatType = "group"
ChatSuperGroup ChatType = "supergroup"
ChatChannel ChatType = "channel"
ChatChannelPrivate ChatType = "privatechannel"
)
// ChatLocation represents a location to which a chat is connected.
type ChatLocation struct {
Location Location `json:"location,omitempty"`
Address string `json:"address,omitempty"`
@ -76,11 +93,6 @@ type ChatPhoto struct {
BigUniqueID string `json:"big_file_unique_id"`
}
// Recipient returns chat ID (see Recipient interface).
func (c *Chat) Recipient() string {
return strconv.FormatInt(c.ID, 10)
}
// ChatMember object represents information about a single chat member.
type ChatMember struct {
Rights
@ -104,6 +116,45 @@ type ChatMember struct {
JoinByRequest string `json:"join_by_request"`
}
// MemberStatus is one's chat status.
type MemberStatus string
const (
Creator MemberStatus = "creator"
Administrator MemberStatus = "administrator"
Member MemberStatus = "member"
Restricted MemberStatus = "restricted"
Left MemberStatus = "left"
Kicked MemberStatus = "kicked"
)
// ChatMemberUpdate object represents changes in the status of a chat member.
type ChatMemberUpdate struct {
// Chat where the user belongs to.
Chat *Chat `json:"chat"`
// Sender which user the action was triggered.
Sender *User `json:"from"`
// Unixtime, use Date() to get time.Time.
Unixtime int64 `json:"date"`
// Previous information about the chat member.
OldChatMember *ChatMember `json:"old_chat_member"`
// New information about the chat member.
NewChatMember *ChatMember `json:"new_chat_member"`
// (Optional) InviteLink which was used by the user to
// join the chat; for joining by invite link events only.
InviteLink *ChatInviteLink `json:"invite_link"`
}
// Time returns the moment of the change in local time.
func (c *ChatMemberUpdate) Time() time.Time {
return time.Unix(c.Unixtime, 0)
}
// ChatID represents a chat or an user integer ID, which can be used
// as recipient in bot methods. It is very useful in cases where
// you have special group IDs, for example in your config, and don't
@ -145,11 +196,69 @@ type ChatJoinRequest struct {
InviteLink *ChatInviteLink `json:"invite_link"`
}
// ChatInviteLink object represents an invite for a chat.
type ChatInviteLink struct {
// The invite link.
InviteLink string `json:"invite_link"`
// Invite link name.
Name string `json:"name"`
// The creator of the link.
Creator *User `json:"creator"`
// If the link is primary.
IsPrimary bool `json:"is_primary"`
// If the link is revoked.
IsRevoked bool `json:"is_revoked"`
// (Optional) Point in time when the link will expire,
// use ExpireDate() to get time.Time.
ExpireUnixtime int64 `json:"expire_date,omitempty"`
// (Optional) Maximum number of users that can be members of
// the chat simultaneously.
MemberLimit int `json:"member_limit,omitempty"`
// (Optional) True, if users joining the chat via the link need to
// be approved by chat administrators. If True, member_limit can't be specified.
JoinRequest bool `json:"creates_join_request"`
// (Optional) Number of pending join requests created using this link.
PendingCount int `json:"pending_join_request_count"`
}
// ExpireDate returns the moment of the link expiration in local time.
func (c *ChatInviteLink) ExpireDate() time.Time {
return time.Unix(c.ExpireUnixtime, 0)
}
// Time returns the moment of chat join request sending in local time.
func (r ChatJoinRequest) Time() time.Time {
return time.Unix(r.Unixtime, 0)
}
// InviteLink should be used to export chat's invite link.
func (b *Bot) InviteLink(chat *Chat) (string, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
}
data, err := b.Raw("exportChatInviteLink", params)
if err != nil {
return "", err
}
var resp struct {
Result string
}
if err := json.Unmarshal(data, &resp); err != nil {
return "", wrapError(err)
}
return resp.Result, nil
}
// CreateInviteLink creates an additional invite link for a chat.
func (b *Bot) CreateInviteLink(chat Recipient, link *ChatInviteLink) (*ChatInviteLink, error) {
params := map[string]string{
@ -239,8 +348,8 @@ func (b *Bot) RevokeInviteLink(chat Recipient, link string) (*ChatInviteLink, er
return &resp.Result, nil
}
// ApproveChatJoinRequest approves a chat join request.
func (b *Bot) ApproveChatJoinRequest(chat Recipient, user *User) error {
// ApproveJoinRequest approves a chat join request.
func (b *Bot) ApproveJoinRequest(chat Recipient, user *User) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"user_id": user.Recipient(),
@ -254,8 +363,8 @@ func (b *Bot) ApproveChatJoinRequest(chat Recipient, user *User) error {
return extractOk(data)
}
// DeclineChatJoinRequest declines a chat join request.
func (b *Bot) DeclineChatJoinRequest(chat Recipient, user *User) error {
// DeclineJoinRequest declines a chat join request.
func (b *Bot) DeclineJoinRequest(chat Recipient, user *User) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"user_id": user.Recipient(),
@ -268,3 +377,77 @@ func (b *Bot) DeclineChatJoinRequest(chat Recipient, user *User) error {
return extractOk(data)
}
// SetGroupTitle should be used to update group title.
func (b *Bot) SetGroupTitle(chat *Chat, title string) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"title": title,
}
_, err := b.Raw("setChatTitle", params)
return err
}
// SetGroupDescription should be used to update group description.
func (b *Bot) SetGroupDescription(chat *Chat, description string) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"description": description,
}
_, err := b.Raw("setChatDescription", params)
return err
}
// 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(),
}
_, err := b.sendFiles("setChatPhoto", map[string]File{"photo": p.File}, params)
return err
}
// 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,
}
_, err := b.Raw("setChatStickerSet", params)
return err
}
// SetGroupPermissions sets default chat permissions for all members.
func (b *Bot) SetGroupPermissions(chat *Chat, perms Rights) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
"permissions": perms,
}
_, err := b.Raw("setChatPermissions", params)
return err
}
// DeleteGroupPhoto should be used to just remove group photo.
func (b *Bot) DeleteGroupPhoto(chat *Chat) error {
params := map[string]string{
"chat_id": chat.Recipient(),
}
_, err := b.Raw("deleteChatPhoto", params)
return err
}
// 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(),
}
_, err := b.Raw("deleteChatStickerSet", params)
return err
}

@ -0,0 +1,85 @@
package telebot
import "encoding/json"
// Command represents a bot command.
type Command struct {
// Text is a text of the command, 1-32 characters.
// Can contain only lowercase English letters, digits and underscores.
Text string `json:"command"`
// Description of the command, 3-256 characters.
Description string `json:"description"`
}
// CommandParams controls parameters for commands-related methods (setMyCommands, deleteMyCommands and getMyCommands).
type CommandParams struct {
Commands []Command `json:"commands,omitempty"`
Scope *CommandScope `json:"scope,omitempty"`
LanguageCode string `json:"language_code,omitempty"`
}
type CommandScopeType = string
const (
CommandScopeDefault CommandScopeType = "default"
CommandScopeAllPrivateChats CommandScopeType = "all_private_chats"
CommandScopeAllGroupChats CommandScopeType = "all_group_chats"
CommandScopeAllChatAdmin CommandScopeType = "all_chat_administrators"
CommandScopeChat CommandScopeType = "chat"
CommandScopeChatAdmin CommandScopeType = "chat_administrators"
CommandScopeChatMember CommandScopeType = "chat_member"
)
// CommandScope object represents a scope to which bot commands are applied.
type CommandScope struct {
Type CommandScopeType `json:"type"`
ChatID int64 `json:"chat_id,omitempty"`
UserID int64 `json:"user_id,omitempty"`
}
// Commands returns the current list of the bot's commands for the given scope and user language.
func (b *Bot) Commands(opts ...interface{}) ([]Command, error) {
params := extractCommandsParams(opts...)
data, err := b.Raw("getMyCommands", params)
if err != nil {
return nil, err
}
var resp struct {
Result []Command
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
}
// SetCommands changes the list of the bot's commands.
func (b *Bot) SetCommands(opts ...interface{}) error {
params := extractCommandsParams(opts...)
_, err := b.Raw("setMyCommands", params)
return err
}
// DeleteCommands deletes the list of the bot's commands for the given scope and user language.
func (b *Bot) DeleteCommands(opts ...interface{}) error {
params := extractCommandsParams(opts...)
_, err := b.Raw("deleteMyCommands", params)
return err
}
// extractCommandsParams extracts parameters for commands-related methods from the given options.
func extractCommandsParams(opts ...interface{}) (params CommandParams) {
for _, opt := range opts {
switch value := opt.(type) {
case []Command:
params.Commands = value
case string:
params.LanguageCode = value
case CommandScope:
params.Scope = &value
}
}
return
}

@ -235,3 +235,8 @@ func Err(s string) error {
return nil
}
}
// wrapError returns new wrapped telebot-related error.
func wrapError(err error) error {
return fmt.Errorf("telebot: %w", err)
}

@ -0,0 +1,315 @@
package telebot
import (
"encoding/json"
"fmt"
"strings"
)
// ReplyMarkup controls two convenient options for bot-user communications
// such as reply keyboard and inline "keyboard" (a grid of buttons as a part
// of the message).
type ReplyMarkup struct {
// InlineKeyboard is a grid of InlineButtons displayed in the message.
//
// Note: DO NOT confuse with ReplyKeyboard and other keyboard properties!
InlineKeyboard [][]InlineButton `json:"inline_keyboard,omitempty"`
// ReplyKeyboard is a grid, consisting of keyboard buttons.
//
// Note: you don't need to set HideCustomKeyboard field to show custom keyboard.
ReplyKeyboard [][]ReplyButton `json:"keyboard,omitempty"`
// ForceReply forces Telegram clients to display
// a reply interface to the user (act as if the user
// has selected the bots message and tapped "Reply").
ForceReply bool `json:"force_reply,omitempty"`
// Requests clients to resize the keyboard vertically for optimal fit
// (e.g. make the keyboard smaller if there are just two rows of buttons).
//
// Defaults to false, in which case the custom keyboard is always of the
// same height as the app's standard keyboard.
ResizeKeyboard bool `json:"resize_keyboard,omitempty"`
// Requests clients to hide the reply keyboard as soon as it's been used.
//
// Defaults to false.
OneTimeKeyboard bool `json:"one_time_keyboard,omitempty"`
// Requests clients to remove the reply keyboard.
//
// Defaults to false.
RemoveKeyboard bool `json:"remove_keyboard,omitempty"`
// Use this param if you want to force reply from
// specific users only.
//
// Targets:
// 1) Users that are @mentioned in the text of the Message object;
// 2) If the bot's message is a reply (has SendOptions.ReplyTo),
// sender of the original message.
Selective bool `json:"selective,omitempty"`
// Placeholder will be shown in the input field when the reply is active.
Placeholder string `json:"input_field_placeholder,omitempty"`
}
func (r *ReplyMarkup) copy() *ReplyMarkup {
cp := *r
if len(r.ReplyKeyboard) > 0 {
cp.ReplyKeyboard = make([][]ReplyButton, len(r.ReplyKeyboard))
for i, row := range r.ReplyKeyboard {
cp.ReplyKeyboard[i] = make([]ReplyButton, len(row))
copy(cp.ReplyKeyboard[i], row)
}
}
if len(r.InlineKeyboard) > 0 {
cp.InlineKeyboard = make([][]InlineButton, len(r.InlineKeyboard))
for i, row := range r.InlineKeyboard {
cp.InlineKeyboard[i] = make([]InlineButton, len(row))
copy(cp.InlineKeyboard[i], row)
}
}
return &cp
}
// Btn is a constructor button, which will later become either a reply, or an inline button.
type Btn struct {
Unique string `json:"unique,omitempty"`
Text string `json:"text,omitempty"`
URL string `json:"url,omitempty"`
Data string `json:"callback_data,omitempty"`
InlineQuery string `json:"switch_inline_query,omitempty"`
InlineQueryChat string `json:"switch_inline_query_current_chat,omitempty"`
Contact bool `json:"request_contact,omitempty"`
Location bool `json:"request_location,omitempty"`
Poll PollType `json:"request_poll,omitempty"`
Login *Login `json:"login_url,omitempty"`
}
// Row represents an array of buttons, a row.
type Row []Btn
// Row creates a row of buttons.
func (r *ReplyMarkup) Row(many ...Btn) Row {
return many
}
// Split splits the keyboard into the rows with N maximum number of buttons.
// For example, if you pass six buttons and 3 as the max, you get two rows with
// three buttons in each.
//
// `Split(3, []Btn{six buttons...}) -> [[1, 2, 3], [4, 5, 6]]`
// `Split(2, []Btn{six buttons...}) -> [[1, 2],[3, 4],[5, 6]]`
//
func (r *ReplyMarkup) Split(max int, btns []Btn) []Row {
rows := make([]Row, (max-1+len(btns))/max)
for i, b := range btns {
i /= max
rows[i] = append(rows[i], b)
}
return rows
}
func (r *ReplyMarkup) Inline(rows ...Row) {
inlineKeys := make([][]InlineButton, 0, len(rows))
for i, row := range rows {
keys := make([]InlineButton, 0, len(row))
for j, btn := range row {
btn := btn.Inline()
if btn == nil {
panic(fmt.Sprintf(
"telebot: button row %d column %d is not an inline button",
i, j))
}
keys = append(keys, *btn)
}
inlineKeys = append(inlineKeys, keys)
}
r.InlineKeyboard = inlineKeys
}
func (r *ReplyMarkup) Reply(rows ...Row) {
replyKeys := make([][]ReplyButton, 0, len(rows))
for i, row := range rows {
keys := make([]ReplyButton, 0, len(row))
for j, btn := range row {
btn := btn.Reply()
if btn == nil {
panic(fmt.Sprintf(
"telebot: button row %d column %d is not a reply button",
i, j))
}
keys = append(keys, *btn)
}
replyKeys = append(replyKeys, keys)
}
r.ReplyKeyboard = replyKeys
}
func (r *ReplyMarkup) Text(text string) Btn {
return Btn{Text: text}
}
func (r *ReplyMarkup) Contact(text string) Btn {
return Btn{Contact: true, Text: text}
}
func (r *ReplyMarkup) Location(text string) Btn {
return Btn{Location: true, Text: text}
}
func (r *ReplyMarkup) Poll(text string, poll PollType) Btn {
return Btn{Poll: poll, Text: text}
}
func (r *ReplyMarkup) Data(text, unique string, data ...string) Btn {
return Btn{
Unique: unique,
Text: text,
Data: strings.Join(data, "|"),
}
}
func (r *ReplyMarkup) URL(text, url string) Btn {
return Btn{Text: text, URL: url}
}
func (r *ReplyMarkup) Query(text, query string) Btn {
return Btn{Text: text, InlineQuery: query}
}
func (r *ReplyMarkup) QueryChat(text, query string) Btn {
return Btn{Text: text, InlineQueryChat: query}
}
func (r *ReplyMarkup) Login(text string, login *Login) Btn {
return Btn{Login: login, Text: text}
}
// ReplyButton represents a button displayed in reply-keyboard.
//
// Set either Contact or Location to true in order to request
// sensitive info, such as user's phone number or current location.
//
type ReplyButton struct {
Text string `json:"text"`
Contact bool `json:"request_contact,omitempty"`
Location bool `json:"request_location,omitempty"`
Poll PollType `json:"request_poll,omitempty"`
WebApp *WebApp `json:"web_app,omitempty"`
}
// MarshalJSON implements json.Marshaler. It allows passing PollType as a
// keyboard's poll type instead of KeyboardButtonPollType object.
func (pt PollType) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Type string `json:"type"`
}{
Type: string(pt),
})
}
// InlineButton represents a button displayed in the message.
type InlineButton struct {
// Unique slagish name for this kind of button,
// try to be as specific as possible.
//
// It will be used as a callback endpoint.
Unique string `json:"unique,omitempty"`
Text string `json:"text"`
URL string `json:"url,omitempty"`
Data string `json:"callback_data,omitempty"`
InlineQuery string `json:"switch_inline_query,omitempty"`
InlineQueryChat string `json:"switch_inline_query_current_chat"`
WebApp *WebApp `json:"web_app,omitempty"`
Login *Login `json:"login_url,omitempty"`
}
// MarshalJSON implements json.Marshaler interface.
// It needed to avoid InlineQueryChat and Login fields conflict.
// If you have Login field in your button, InlineQueryChat must be skipped.
func (t *InlineButton) MarshalJSON() ([]byte, error) {
type IB InlineButton
if t.Login != nil {
return json.Marshal(struct {
IB
InlineQueryChat string `json:"switch_inline_query_current_chat,omitempty"`
}{
IB: IB(*t),
})
}
return json.Marshal(IB(*t))
}
// With returns a copy of the button with data.
func (t *InlineButton) With(data string) *InlineButton {
return &InlineButton{
Unique: t.Unique,
Text: t.Text,
URL: t.URL,
InlineQuery: t.InlineQuery,
InlineQueryChat: t.InlineQueryChat,
Login: t.Login,
Data: data,
}
}
func (b Btn) Reply() *ReplyButton {
if b.Unique != "" {
return nil
}
return &ReplyButton{
Text: b.Text,
Contact: b.Contact,
Location: b.Location,
Poll: b.Poll,
}
}
func (b Btn) Inline() *InlineButton {
return &InlineButton{
Unique: b.Unique,
Text: b.Text,
URL: b.URL,
Data: b.Data,
InlineQuery: b.InlineQuery,
InlineQueryChat: b.InlineQueryChat,
Login: b.Login,
}
}
// Login represents a parameter of the inline keyboard button
// used to automatically authorize a user. Serves as a great replacement
// for the Telegram Login Widget when the user is coming from Telegram.
type Login struct {
URL string `json:"url"`
Text string `json:"forward_text,omitempty"`
Username string `json:"bot_username,omitempty"`
WriteAccess bool `json:"request_write_access,omitempty"`
}
// MenuButton describes the bot's menu button in a private chat.
type MenuButton struct {
Type MenuButtonType `json:"type"`
Text string `json:"text,omitempty"`
WebApp *WebApp `json:"web_app,omitempty"`
}
type MenuButtonType = string
const (
MenuButtonDefault MenuButtonType = "default"
MenuButtonCommands MenuButtonType = "commands"
MenuButtonWebApp MenuButtonType = "web_app"
)

@ -341,3 +341,15 @@ type Dice struct {
Type DiceType `json:"emoji"`
Value int `json:"value"`
}
// DiceType defines dice types.
type DiceType string
var (
Cube = &Dice{Type: "🎲"}
Dart = &Dice{Type: "🎯"}
Ball = &Dice{Type: "🏀"}
Goal = &Dice{Type: "⚽"}
Slot = &Dice{Type: "🎰"}
Bowl = &Dice{Type: "🎳"}
)

@ -279,6 +279,29 @@ type MessageEntity struct {
CustomEmoji string `json:"custom_emoji_id"`
}
// EntityType is a MessageEntity type.
type EntityType string
const (
EntityMention EntityType = "mention"
EntityTMention EntityType = "text_mention"
EntityHashtag EntityType = "hashtag"
EntityCashtag EntityType = "cashtag"
EntityCommand EntityType = "bot_command"
EntityURL EntityType = "url"
EntityEmail EntityType = "email"
EntityPhone EntityType = "phone_number"
EntityBold EntityType = "bold"
EntityItalic EntityType = "italic"
EntityUnderline EntityType = "underline"
EntityStrikethrough EntityType = "strikethrough"
EntityCode EntityType = "code"
EntityCodeBlock EntityType = "pre"
EntityTextLink EntityType = "text_link"
EntitySpoiler EntityType = "spoiler"
EntityCustomEmoji EntityType = "custom_emoji"
)
// Entities is used to set message's text entities as a send option.
type Entities []MessageEntity

@ -4,6 +4,13 @@ package telebot
// which get called before the endpoint group or specific handler.
type MiddlewareFunc func(HandlerFunc) HandlerFunc
func applyMiddleware(h HandlerFunc, m ...MiddlewareFunc) HandlerFunc {
for i := len(m) - 1; i >= 0; i-- {
h = m[i](h)
}
return h
}
// Group is a separated group of handlers, united by the general middleware.
type Group struct {
b *Bot

@ -2,8 +2,7 @@ package telebot
import (
"encoding/json"
"fmt"
"strings"
"strconv"
)
// Option is a shortcut flag type for certain message features
@ -90,282 +89,125 @@ func (og *SendOptions) copy() *SendOptions {
return &cp
}
// ReplyMarkup controls two convenient options for bot-user communications
// such as reply keyboard and inline "keyboard" (a grid of buttons as a part
// of the message).
type ReplyMarkup struct {
// InlineKeyboard is a grid of InlineButtons displayed in the message.
//
// Note: DO NOT confuse with ReplyKeyboard and other keyboard properties!
InlineKeyboard [][]InlineButton `json:"inline_keyboard,omitempty"`
// ReplyKeyboard is a grid, consisting of keyboard buttons.
//
// Note: you don't need to set HideCustomKeyboard field to show custom keyboard.
ReplyKeyboard [][]ReplyButton `json:"keyboard,omitempty"`
// ForceReply forces Telegram clients to display
// a reply interface to the user (act as if the user
// has selected the bots message and tapped "Reply").
ForceReply bool `json:"force_reply,omitempty"`
// Requests clients to resize the keyboard vertically for optimal fit
// (e.g. make the keyboard smaller if there are just two rows of buttons).
//
// Defaults to false, in which case the custom keyboard is always of the
// same height as the app's standard keyboard.
ResizeKeyboard bool `json:"resize_keyboard,omitempty"`
// Requests clients to hide the reply keyboard as soon as it's been used.
//
// Defaults to false.
OneTimeKeyboard bool `json:"one_time_keyboard,omitempty"`
// Requests clients to remove the reply keyboard.
//
// Defaults to false.
RemoveKeyboard bool `json:"remove_keyboard,omitempty"`
// Use this param if you want to force reply from
// specific users only.
//
// Targets:
// 1) Users that are @mentioned in the text of the Message object;
// 2) If the bot's message is a reply (has SendOptions.ReplyTo),
// sender of the original message.
Selective bool `json:"selective,omitempty"`
// Placeholder will be shown in the input field when the reply is active.
Placeholder string `json:"input_field_placeholder,omitempty"`
}
func (r *ReplyMarkup) copy() *ReplyMarkup {
cp := *r
if len(r.ReplyKeyboard) > 0 {
cp.ReplyKeyboard = make([][]ReplyButton, len(r.ReplyKeyboard))
for i, row := range r.ReplyKeyboard {
cp.ReplyKeyboard[i] = make([]ReplyButton, len(row))
copy(cp.ReplyKeyboard[i], row)
}
}
func extractOptions(how []interface{}) *SendOptions {
opts := &SendOptions{}
if len(r.InlineKeyboard) > 0 {
cp.InlineKeyboard = make([][]InlineButton, len(r.InlineKeyboard))
for i, row := range r.InlineKeyboard {
cp.InlineKeyboard[i] = make([]InlineButton, len(row))
copy(cp.InlineKeyboard[i], row)
for _, prop := range how {
switch opt := prop.(type) {
case *SendOptions:
opts = opt.copy()
case *ReplyMarkup:
if opt != nil {
opts.ReplyMarkup = opt.copy()
}
case Option:
switch opt {
case NoPreview:
opts.DisableWebPagePreview = true
case Silent:
opts.DisableNotification = true
case AllowWithoutReply:
opts.AllowWithoutReply = true
case ForceReply:
if opts.ReplyMarkup == nil {
opts.ReplyMarkup = &ReplyMarkup{}
}
opts.ReplyMarkup.ForceReply = true
case OneTimeKeyboard:
if opts.ReplyMarkup == nil {
opts.ReplyMarkup = &ReplyMarkup{}
}
opts.ReplyMarkup.OneTimeKeyboard = true
case RemoveKeyboard:
if opts.ReplyMarkup == nil {
opts.ReplyMarkup = &ReplyMarkup{}
}
opts.ReplyMarkup.RemoveKeyboard = true
case Protected:
opts.Protected = true
default:
panic("telebot: unsupported flag-option")
}
case ParseMode:
opts.ParseMode = opt
case Entities:
opts.Entities = opt
default:
panic("telebot: unsupported send-option")
}
}
return &cp
return opts
}
// ReplyButton represents a button displayed in reply-keyboard.
//
// Set either Contact or Location to true in order to request
// sensitive info, such as user's phone number or current location.
//
type ReplyButton struct {
Text string `json:"text"`
Contact bool `json:"request_contact,omitempty"`
Location bool `json:"request_location,omitempty"`
Poll PollType `json:"request_poll,omitempty"`
WebApp *WebApp `json:"web_app,omitempty"`
}
func (b *Bot) embedSendOptions(params map[string]string, opt *SendOptions) {
if b.parseMode != ModeDefault {
params["parse_mode"] = b.parseMode
}
// MarshalJSON implements json.Marshaler. It allows to pass
// PollType as keyboard's poll type instead of KeyboardButtonPollType object.
func (pt PollType) MarshalJSON() ([]byte, error) {
var aux = struct {
Type string `json:"type"`
}{
Type: string(pt),
if opt == nil {
return
}
return json.Marshal(&aux)
}
// Row represents an array of buttons, a row.
type Row []Btn
if opt.ReplyTo != nil && opt.ReplyTo.ID != 0 {
params["reply_to_message_id"] = strconv.Itoa(opt.ReplyTo.ID)
}
// Row creates a row of buttons.
func (r *ReplyMarkup) Row(many ...Btn) Row {
return many
}
if opt.DisableWebPagePreview {
params["disable_web_page_preview"] = "true"
}
// Split splits the keyboard into the rows with N maximum number of buttons.
// For example, if you pass six buttons and 3 as the max, you get two rows with
// three buttons in each.
//
// `Split(3, []Btn{six buttons...}) -> [[1, 2, 3], [4, 5, 6]]`
// `Split(2, []Btn{six buttons...}) -> [[1, 2],[3, 4],[5, 6]]`
//
func (r *ReplyMarkup) Split(max int, btns []Btn) []Row {
rows := make([]Row, (max-1+len(btns))/max)
for i, b := range btns {
i /= max
rows[i] = append(rows[i], b)
if opt.DisableNotification {
params["disable_notification"] = "true"
}
return rows
}
func (r *ReplyMarkup) Inline(rows ...Row) {
inlineKeys := make([][]InlineButton, 0, len(rows))
for i, row := range rows {
keys := make([]InlineButton, 0, len(row))
for j, btn := range row {
btn := btn.Inline()
if btn == nil {
panic(fmt.Sprintf(
"telebot: button row %d column %d is not an inline button",
i, j))
}
keys = append(keys, *btn)
}
inlineKeys = append(inlineKeys, keys)
if opt.ParseMode != ModeDefault {
params["parse_mode"] = opt.ParseMode
}
r.InlineKeyboard = inlineKeys
}
if len(opt.Entities) > 0 {
delete(params, "parse_mode")
entities, _ := json.Marshal(opt.Entities)
func (r *ReplyMarkup) Reply(rows ...Row) {
replyKeys := make([][]ReplyButton, 0, len(rows))
for i, row := range rows {
keys := make([]ReplyButton, 0, len(row))
for j, btn := range row {
btn := btn.Reply()
if btn == nil {
panic(fmt.Sprintf(
"telebot: button row %d column %d is not a reply button",
i, j))
}
keys = append(keys, *btn)
if params["caption"] != "" {
params["caption_entities"] = string(entities)
} else {
params["entities"] = string(entities)
}
replyKeys = append(replyKeys, keys)
}
r.ReplyKeyboard = replyKeys
}
func (r *ReplyMarkup) Text(text string) Btn {
return Btn{Text: text}
}
func (r *ReplyMarkup) Contact(text string) Btn {
return Btn{Contact: true, Text: text}
}
func (r *ReplyMarkup) Location(text string) Btn {
return Btn{Location: true, Text: text}
}
func (r *ReplyMarkup) Poll(text string, poll PollType) Btn {
return Btn{Poll: poll, Text: text}
}
func (r *ReplyMarkup) Data(text, unique string, data ...string) Btn {
return Btn{
Unique: unique,
Text: text,
Data: strings.Join(data, "|"),
if opt.AllowWithoutReply {
params["allow_sending_without_reply"] = "true"
}
}
func (r *ReplyMarkup) URL(text, url string) Btn {
return Btn{Text: text, URL: url}
}
func (r *ReplyMarkup) Query(text, query string) Btn {
return Btn{Text: text, InlineQuery: query}
}
func (r *ReplyMarkup) QueryChat(text, query string) Btn {
return Btn{Text: text, InlineQueryChat: query}
}
func (r *ReplyMarkup) Login(text string, login *Login) Btn {
return Btn{Login: login, Text: text}
}
// Btn is a constructor button, which will later become either a reply, or an inline button.
type Btn struct {
Unique string `json:"unique,omitempty"`
Text string `json:"text,omitempty"`
URL string `json:"url,omitempty"`
Data string `json:"callback_data,omitempty"`
InlineQuery string `json:"switch_inline_query,omitempty"`
InlineQueryChat string `json:"switch_inline_query_current_chat,omitempty"`
Contact bool `json:"request_contact,omitempty"`
Location bool `json:"request_location,omitempty"`
Poll PollType `json:"request_poll,omitempty"`
Login *Login `json:"login_url,omitempty"`
}
if opt.ReplyMarkup != nil {
processButtons(opt.ReplyMarkup.InlineKeyboard)
replyMarkup, _ := json.Marshal(opt.ReplyMarkup)
params["reply_markup"] = string(replyMarkup)
}
func (b Btn) Inline() *InlineButton {
return &InlineButton{
Unique: b.Unique,
Text: b.Text,
URL: b.URL,
Data: b.Data,
InlineQuery: b.InlineQuery,
InlineQueryChat: b.InlineQueryChat,
Login: b.Login,
if opt.Protected {
params["protect_content"] = "true"
}
}
func (b Btn) Reply() *ReplyButton {
if b.Unique != "" {
return nil
func processButtons(keys [][]InlineButton) {
if keys == nil || len(keys) < 1 || len(keys[0]) < 1 {
return
}
return &ReplyButton{
Text: b.Text,
Contact: b.Contact,
Location: b.Location,
Poll: b.Poll,
for i := range keys {
for j := range keys[i] {
key := &keys[i][j]
if key.Unique != "" {
// Format: "\f<callback_name>|<data>"
data := key.Data
if data == "" {
key.Data = "\f" + key.Unique
} else {
key.Data = "\f" + key.Unique + "|" + data
}
}
}
}
}
// CommandParams controls parameters for commands-related methods (setMyCommands, deleteMyCommands and getMyCommands).
type CommandParams struct {
Commands []Command `json:"commands,omitempty"`
Scope *CommandScope `json:"scope,omitempty"`
LanguageCode string `json:"language_code,omitempty"`
}
// CommandScope object represents a scope to which bot commands are applied.
type CommandScope struct {
Type CommandScopeType `json:"type"`
ChatID int64 `json:"chat_id,omitempty"`
UserID int64 `json:"user_id,omitempty"`
}
type CommandScopeType = string
// CommandScope types.
const (
CommandScopeDefault CommandScopeType = "default"
CommandScopeAllPrivateChats CommandScopeType = "all_private_chats"
CommandScopeAllGroupChats CommandScopeType = "all_group_chats"
CommandScopeAllChatAdmin CommandScopeType = "all_chat_administrators"
CommandScopeChat CommandScopeType = "chat"
CommandScopeChatAdmin CommandScopeType = "chat_administrators"
CommandScopeChatMember CommandScopeType = "chat_member"
)
// MenuButton describes the bot's menu button in a private chat.
type MenuButton struct {
Type MenuButtonType `json:"type"`
Text string `json:"text,omitempty"`
WebApp *WebApp `json:"web_app,omitempty"`
}
type MenuButtonType = string
// MenuButton types.
const (
MenuButtonDefault MenuButtonType = "default"
MenuButtonCommands MenuButtonType = "commands"
MenuButtonWebApp MenuButtonType = "web_app"
)

@ -4,7 +4,6 @@ import (
"encoding/json"
"math"
"strconv"
"strings"
)
// ShippingQuery contains information about an incoming shipping query.
@ -132,7 +131,13 @@ func (i Invoice) params() map[string]string {
params["prices"] = string(data)
}
if len(i.SuggestedTipAmounts) > 0 {
params["suggested_tip_amounts"] = "[" + strings.Join(intsToStrs(i.SuggestedTipAmounts), ",") + "]"
var amounts []string
for _, n := range i.SuggestedTipAmounts {
amounts = append(amounts, strconv.Itoa(n))
}
data, _ := json.Marshal(amounts)
params["suggested_tip_amounts"] = string(data)
}
return params
}
@ -166,11 +171,18 @@ func (c Currency) ToTotal(total float64) int {
return int(total) * int(math.Pow(10, float64(c.Exp)))
}
var SupportedCurrencies = make(map[string]Currency)
func init() {
err := json.Unmarshal([]byte(dataCurrencies), &SupportedCurrencies)
// CreateInvoiceLink creates a link for a payment invoice.
func (b *Bot) CreateInvoiceLink(i Invoice) (string, error) {
data, err := b.Raw("createInvoiceLink", i.params())
if err != nil {
panic(err)
return "", err
}
var resp struct {
Result string
}
if err := json.Unmarshal(data, &resp); err != nil {
return "", wrapError(err)
}
return resp.Result, nil
}

File diff suppressed because one or more lines are too long

@ -2,6 +2,19 @@ package telebot
import "time"
// PollType defines poll types.
type PollType string
const (
// NOTE:
// Despite "any" type isn't described in documentation,
// it needed for proper KeyboardButtonPollType marshaling.
PollAny PollType = "any"
PollQuiz PollType = "quiz"
PollRegular PollType = "regular"
)
// Poll contains information about a poll.
type Poll struct {
ID string `json:"id"`

@ -1,8 +1,6 @@
package telebot
import (
"time"
)
import "time"
// Poller is a provider of Updates.
//
@ -20,6 +18,53 @@ type Poller interface {
Poll(b *Bot, updates chan Update, stop chan struct{})
}
// LongPoller is a classic LongPoller with timeout.
type LongPoller struct {
Limit int
Timeout time.Duration
LastUpdateID int
// AllowedUpdates contains the update types
// you want your bot to receive.
//
// Possible values:
// message
// edited_message
// channel_post
// edited_channel_post
// inline_query
// chosen_inline_result
// callback_query
// shipping_query
// pre_checkout_query
// poll
// poll_answer
//
AllowedUpdates []string `yaml:"allowed_updates"`
}
// Poll does long polling.
func (p *LongPoller) Poll(b *Bot, dest chan Update, stop chan struct{}) {
for {
select {
case <-stop:
return
default:
}
updates, err := b.getUpdates(p.LastUpdateID+1, p.Limit, p.Timeout, p.AllowedUpdates)
if err != nil {
b.debug(err)
continue
}
for _, update := range updates {
p.LastUpdateID = update.ID
dest <- update
}
}
}
// MiddlewarePoller is a special kind of poller that acts
// like a filter for updates. It could be used for spam
// handling, banning or whatever.
@ -68,50 +113,3 @@ func (p *MiddlewarePoller) Poll(b *Bot, dest chan Update, stop chan struct{}) {
}
}
}
// LongPoller is a classic LongPoller with timeout.
type LongPoller struct {
Limit int
Timeout time.Duration
LastUpdateID int
// AllowedUpdates contains the update types
// you want your bot to receive.
//
// Possible values:
// message
// edited_message
// channel_post
// edited_channel_post
// inline_query
// chosen_inline_result
// callback_query
// shipping_query
// pre_checkout_query
// poll
// poll_answer
//
AllowedUpdates []string `yaml:"allowed_updates"`
}
// Poll does long polling.
func (p *LongPoller) Poll(b *Bot, dest chan Update, stop chan struct{}) {
for {
select {
case <-stop:
return
default:
}
updates, err := b.getUpdates(p.LastUpdateID+1, p.Limit, p.Timeout, p.AllowedUpdates)
if err != nil {
b.debug(err)
continue
}
for _, update := range updates {
p.LastUpdateID = update.ID
dest <- update
}
}
}

@ -399,3 +399,10 @@ func (g *Game) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
return extractMessage(data)
}
func thumbnailToFilemap(thumb *Photo) map[string]File {
if thumb != nil {
return map[string]File{"thumb": thumb.File}
}
return nil
}

@ -7,7 +7,6 @@ import (
type StickerSetType = string
// StickerSet types.
const (
StickerRegular = "regular"
StickerMask = "mask"
@ -40,6 +39,16 @@ type MaskPosition struct {
Scale float32 `json:"scale"`
}
// MaskFeature defines sticker mask position.
type MaskFeature string
const (
FeatureForehead MaskFeature = "forehead"
FeatureEyes MaskFeature = "eyes"
FeatureMouth MaskFeature = "mouth"
FeatureChin MaskFeature = "chin"
)
// UploadSticker uploads a PNG file with a sticker for later use.
func (b *Bot) UploadSticker(to Recipient, png *File) (*File, error) {
files := map[string]File{

@ -46,35 +46,30 @@ const DefaultApiURL = "https://api.telegram.org"
//
const (
// Basic message handlers.
OnText = "\atext"
OnEdited = "\aedited"
OnPhoto = "\aphoto"
OnAudio = "\aaudio"
OnAnimation = "\aanimation"
OnDocument = "\adocument"
OnSticker = "\asticker"
OnVideo = "\avideo"
OnVoice = "\avoice"
OnVideoNote = "\avideo_note"
OnContact = "\acontact"
OnLocation = "\alocation"
OnVenue = "\avenue"
OnDice = "\adice"
OnInvoice = "\ainvoice"
OnPayment = "\apayment"
OnGame = "\agame"
OnPoll = "\apoll"
OnPollAnswer = "\apoll_answer"
OnPinned = "\apinned"
// Will fire on channel posts.
OnText = "\atext"
OnEdited = "\aedited"
OnPhoto = "\aphoto"
OnAudio = "\aaudio"
OnAnimation = "\aanimation"
OnDocument = "\adocument"
OnSticker = "\asticker"
OnVideo = "\avideo"
OnVoice = "\avoice"
OnVideoNote = "\avideo_note"
OnContact = "\acontact"
OnLocation = "\alocation"
OnVenue = "\avenue"
OnDice = "\adice"
OnInvoice = "\ainvoice"
OnPayment = "\apayment"
OnGame = "\agame"
OnPoll = "\apoll"
OnPollAnswer = "\apoll_answer"
OnPinned = "\apinned"
OnChannelPost = "\achannel_post"
OnEditedChannelPost = "\aedited_channel_post"
// Will fire when bot is added to a group.
OnAddedToGroup = "\aadded_to_group"
// Service events:
OnAddedToGroup = "\aadded_to_group"
OnUserJoined = "\auser_joined"
OnUserLeft = "\auser_left"
OnNewGroupTitle = "\anew_chat_title"
@ -84,59 +79,29 @@ const (
OnSuperGroupCreated = "\asupergroup_created"
OnChannelCreated = "\achannel_created"
// Migration happens when group switches to
// OnMigration happens when group switches to
// a supergroup. You might want to update
// your internal references to this chat
// upon switching as its ID will change.
OnMigration = "\amigration"
// Will fire on any unhandled media.
OnMedia = "\amedia"
// Will fire on callback requests.
OnCallback = "\acallback"
// Will fire on incoming inline queries.
OnQuery = "\aquery"
// Will fire on chosen inline results.
OnInlineResult = "\ainline_result"
// Will fire on a shipping query.
OnShipping = "\ashipping_query"
// Will fire on pre checkout query.
OnCheckout = "\apre_checkout_query"
// Will fire on bot's chat member changes.
OnMyChatMember = "\amy_chat_member"
// Will fire on chat member's changes.
OnChatMember = "\achat_member"
// Will fire on chat join request.
OnMedia = "\amedia"
OnCallback = "\acallback"
OnQuery = "\aquery"
OnInlineResult = "\ainline_result"
OnShipping = "\ashipping_query"
OnCheckout = "\apre_checkout_query"
OnMyChatMember = "\amy_chat_member"
OnChatMember = "\achat_member"
OnChatJoinRequest = "\achat_join_request"
// Will fire on the start of a video chat.
OnVideoChatStarted = "\avideo_chat_started"
// Will fire on the end of a video chat.
OnVideoChatEnded = "\avideo_chat_ended"
// Will fire on invited participants to the video chat.
OnVideoChatParticipants = "\avideo_chat_participants_invited"
// Will fire on scheduling a video chat.
OnVideoChatScheduled = "\avideo_chat_scheduled"
// Will fire on a proximity alert.
OnProximityAlert = "\aproximity_alert_triggered"
// Will fire on auto delete timer set.
OnProximityAlert = "\aproximity_alert_triggered"
OnAutoDeleteTimer = "\amessage_auto_delete_timer_changed"
OnWebApp = "\aweb_app"
// Will fire on the web app data.
OnWebApp = "\aweb_app"
OnVideoChatStarted = "\avideo_chat_started"
OnVideoChatEnded = "\avideo_chat_ended"
OnVideoChatParticipants = "\avideo_chat_participants_invited"
OnVideoChatScheduled = "\avideo_chat_scheduled"
)
// ChatAction is a client-side status indicating bot activity.
@ -166,85 +131,6 @@ const (
ModeHTML ParseMode = "HTML"
)
// EntityType is a MessageEntity type.
type EntityType string
const (
EntityMention EntityType = "mention"
EntityTMention EntityType = "text_mention"
EntityHashtag EntityType = "hashtag"
EntityCashtag EntityType = "cashtag"
EntityCommand EntityType = "bot_command"
EntityURL EntityType = "url"
EntityEmail EntityType = "email"
EntityPhone EntityType = "phone_number"
EntityBold EntityType = "bold"
EntityItalic EntityType = "italic"
EntityUnderline EntityType = "underline"
EntityStrikethrough EntityType = "strikethrough"
EntityCode EntityType = "code"
EntityCodeBlock EntityType = "pre"
EntityTextLink EntityType = "text_link"
EntitySpoiler EntityType = "spoiler"
EntityCustomEmoji EntityType = "custom_emoji"
)
// ChatType represents one of the possible chat types.
type ChatType string
const (
ChatPrivate ChatType = "private"
ChatGroup ChatType = "group"
ChatSuperGroup ChatType = "supergroup"
ChatChannel ChatType = "channel"
ChatChannelPrivate ChatType = "privatechannel"
)
// MemberStatus is one's chat status.
type MemberStatus string
const (
Creator MemberStatus = "creator"
Administrator MemberStatus = "administrator"
Member MemberStatus = "member"
Restricted MemberStatus = "restricted"
Left MemberStatus = "left"
Kicked MemberStatus = "kicked"
)
// MaskFeature defines sticker mask position.
type MaskFeature string
const (
FeatureForehead MaskFeature = "forehead"
FeatureEyes MaskFeature = "eyes"
FeatureMouth MaskFeature = "mouth"
FeatureChin MaskFeature = "chin"
)
// PollType defines poll types.
type PollType string
const (
// Despite "any" type isn't described in documentation,
// it needed for proper KeyboardButtonPollType marshaling.
PollAny PollType = "any"
PollQuiz PollType = "quiz"
PollRegular PollType = "regular"
)
type DiceType string
var (
Cube = &Dice{Type: "🎲"}
Dart = &Dice{Type: "🎯"}
Ball = &Dice{Type: "🏀"}
Goal = &Dice{Type: "⚽"}
Slot = &Dice{Type: "🎰"}
Bowl = &Dice{Type: "🎳"}
)
// M is a shortcut for map[string]interface{}. Use it for passing
// arguments to the layout functions.
type M = map[string]interface{}

@ -0,0 +1,346 @@
package telebot
import "strings"
// 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"`
InlineResult *InlineResult `json:"chosen_inline_result,omitempty"`
ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"`
PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"`
Poll *Poll `json:"poll,omitempty"`
PollAnswer *PollAnswer `json:"poll_answer,omitempty"`
MyChatMember *ChatMemberUpdate `json:"my_chat_member,omitempty"`
ChatMember *ChatMemberUpdate `json:"chat_member,omitempty"`
ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"`
}
// ProcessUpdate processes a single incoming update.
// A started bot calls this function automatically.
func (b *Bot) ProcessUpdate(u Update) {
c := b.NewContext(u)
if u.Message != nil {
m := u.Message
if m.PinnedMessage != nil {
b.handle(OnPinned, c)
return
}
// Commands
if m.Text != "" {
// Filtering malicious messages
if m.Text[0] == '\a' {
return
}
match := cmdRx.FindAllStringSubmatch(m.Text, -1)
if match != nil {
// Syntax: "</command>@<bot> <payload>"
command, botName := match[0][1], match[0][3]
if botName != "" && !strings.EqualFold(b.Me.Username, botName) {
return
}
m.Payload = match[0][5]
if b.handle(command, c) {
return
}
}
// 1:1 satisfaction
if b.handle(m.Text, c) {
return
}
b.handle(OnText, c)
return
}
if b.handleMedia(c) {
return
}
if m.Contact != nil {
b.handle(OnContact, c)
return
}
if m.Location != nil {
b.handle(OnLocation, c)
return
}
if m.Venue != nil {
b.handle(OnVenue, c)
return
}
if m.Game != nil {
b.handle(OnGame, c)
return
}
if m.Dice != nil {
b.handle(OnDice, c)
return
}
if m.Invoice != nil {
b.handle(OnInvoice, c)
return
}
if m.Payment != nil {
b.handle(OnPayment, c)
return
}
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, c)
return
}
if m.UserJoined != nil {
b.handle(OnUserJoined, c)
return
}
if m.UsersJoined != nil {
for _, user := range m.UsersJoined {
m.UserJoined = &user
b.handle(OnUserJoined, c)
}
return
}
if m.UserLeft != nil {
b.handle(OnUserLeft, c)
return
}
if m.NewGroupTitle != "" {
b.handle(OnNewGroupTitle, c)
return
}
if m.NewGroupPhoto != nil {
b.handle(OnNewGroupPhoto, c)
return
}
if m.GroupPhotoDeleted {
b.handle(OnGroupPhotoDeleted, c)
return
}
if m.GroupCreated {
b.handle(OnGroupCreated, c)
return
}
if m.SuperGroupCreated {
b.handle(OnSuperGroupCreated, c)
return
}
if m.ChannelCreated {
b.handle(OnChannelCreated, c)
return
}
if m.MigrateTo != 0 {
m.MigrateFrom = m.Chat.ID
b.handle(OnMigration, c)
return
}
if m.VideoChatStarted != nil {
b.handle(OnVideoChatStarted, c)
return
}
if m.VideoChatEnded != nil {
b.handle(OnVideoChatEnded, c)
return
}
if m.VideoChatParticipants != nil {
b.handle(OnVideoChatParticipants, c)
return
}
if m.VideoChatScheduled != nil {
b.handle(OnVideoChatScheduled, c)
return
}
if m.WebAppData != nil {
b.handle(OnWebApp, c)
}
if m.ProximityAlert != nil {
b.handle(OnProximityAlert, c)
return
}
if m.AutoDeleteTimer != nil {
b.handle(OnAutoDeleteTimer, c)
return
}
}
if u.EditedMessage != nil {
b.handle(OnEdited, c)
return
}
if u.ChannelPost != nil {
m := u.ChannelPost
if m.PinnedMessage != nil {
b.handle(OnPinned, c)
return
}
b.handle(OnChannelPost, c)
return
}
if u.EditedChannelPost != nil {
b.handle(OnEditedChannelPost, c)
return
}
if u.Callback != nil {
if data := u.Callback.Data; data != "" && 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 {
u.Callback.Unique = unique
u.Callback.Data = payload
b.runHandler(handler, c)
return
}
}
}
b.handle(OnCallback, c)
return
}
if u.Query != nil {
b.handle(OnQuery, c)
return
}
if u.InlineResult != nil {
b.handle(OnInlineResult, c)
return
}
if u.ShippingQuery != nil {
b.handle(OnShipping, c)
return
}
if u.PreCheckoutQuery != nil {
b.handle(OnCheckout, c)
return
}
if u.Poll != nil {
b.handle(OnPoll, c)
return
}
if u.PollAnswer != nil {
b.handle(OnPollAnswer, c)
return
}
if u.MyChatMember != nil {
b.handle(OnMyChatMember, c)
return
}
if u.ChatMember != nil {
b.handle(OnChatMember, c)
return
}
if u.ChatJoinRequest != nil {
b.handle(OnChatJoinRequest, c)
return
}
}
func (b *Bot) handle(end string, c Context) bool {
if handler, ok := b.handlers[end]; ok {
b.runHandler(handler, c)
return true
}
return false
}
func (b *Bot) handleMedia(c Context) bool {
var (
m = c.Message()
fired = true
)
switch {
case m.Photo != nil:
fired = b.handle(OnPhoto, c)
case m.Voice != nil:
fired = b.handle(OnVoice, c)
case m.Audio != nil:
fired = b.handle(OnAudio, c)
case m.Animation != nil:
fired = b.handle(OnAnimation, c)
case m.Document != nil:
fired = b.handle(OnDocument, c)
case m.Sticker != nil:
fired = b.handle(OnSticker, c)
case m.Video != nil:
fired = b.handle(OnVideo, c)
case m.VideoNote != nil:
fired = b.handle(OnVideoNote, c)
default:
return false
}
if !fired {
return b.handle(OnMedia, c)
}
return true
}
func (b *Bot) runHandler(h HandlerFunc, c Context) {
f := func() {
if err := h(c); err != nil {
b.OnError(err, c)
}
}
if b.synchronous {
f()
} else {
go f()
}
}
func isUserInList(user *User, list []User) bool {
for _, user2 := range list {
if user.ID == user2.ID {
return true
}
}
return false
}

@ -1,306 +0,0 @@
package telebot
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
)
var defaultOnError = func(err error, c Context) {
if c != nil {
log.Println(c.Update().ID, err)
} else {
log.Println(err)
}
}
func (b *Bot) debug(err error) {
if b.verbose {
b.OnError(err, nil)
}
}
func verbose(method string, payload interface{}, data []byte) {
body, _ := json.Marshal(payload)
body = bytes.ReplaceAll(body, []byte(`\"`), []byte(`"`))
body = bytes.ReplaceAll(body, []byte(`"{`), []byte(`{`))
body = bytes.ReplaceAll(body, []byte(`}"`), []byte(`}`))
indent := func(b []byte) string {
var buf bytes.Buffer
json.Indent(&buf, b, "", " ")
return buf.String()
}
log.Printf(
"[verbose] telebot: sent request\nMethod: %v\nParams: %v\nResponse: %v",
method, indent(body), indent(data),
)
}
func (b *Bot) runHandler(h HandlerFunc, c Context) {
f := func() {
if err := h(c); err != nil {
b.OnError(err, c)
}
}
if b.synchronous {
f()
} else {
go f()
}
}
func applyMiddleware(h HandlerFunc, m ...MiddlewareFunc) HandlerFunc {
for i := len(m) - 1; i >= 0; i-- {
h = m[i](h)
}
return h
}
// wrapError returns new wrapped telebot-related error.
func wrapError(err error) error {
return fmt.Errorf("telebot: %w", err)
}
// extractOk checks given result for error. If result is ok returns nil.
// In other cases it extracts API error. If error is not presented
// in errors.go, it will be prefixed with `unknown` keyword.
func extractOk(data []byte) error {
var e struct {
Ok bool `json:"ok"`
Code int `json:"error_code"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
}
if json.NewDecoder(bytes.NewReader(data)).Decode(&e) != nil {
return nil // FIXME
}
if e.Ok {
return nil
}
err := Err(e.Description)
switch err {
case nil:
case ErrGroupMigrated:
migratedTo, ok := e.Parameters["migrate_to_chat_id"]
if !ok {
return NewError(e.Code, e.Description)
}
return GroupError{
err: err.(*Error),
MigratedTo: int64(migratedTo.(float64)),
}
default:
return err
}
switch e.Code {
case http.StatusTooManyRequests:
retryAfter, ok := e.Parameters["retry_after"]
if !ok {
return NewError(e.Code, e.Description)
}
err = FloodError{
err: NewError(e.Code, e.Description),
RetryAfter: int(retryAfter.(float64)),
}
default:
err = fmt.Errorf("telegram: %s (%d)", e.Description, e.Code)
}
return err
}
// extractMessage extracts common Message result from given data.
// Should be called after extractOk or b.Raw() to handle possible errors.
func extractMessage(data []byte) (*Message, error) {
var resp struct {
Result *Message
}
if err := json.Unmarshal(data, &resp); err != nil {
var resp struct {
Result bool
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
if resp.Result {
return nil, ErrTrueResult
}
return nil, wrapError(err)
}
return resp.Result, nil
}
func extractOptions(how []interface{}) *SendOptions {
opts := &SendOptions{}
for _, prop := range how {
switch opt := prop.(type) {
case *SendOptions:
opts = opt.copy()
case *ReplyMarkup:
if opt != nil {
opts.ReplyMarkup = opt.copy()
}
case Option:
switch opt {
case NoPreview:
opts.DisableWebPagePreview = true
case Silent:
opts.DisableNotification = true
case AllowWithoutReply:
opts.AllowWithoutReply = true
case ForceReply:
if opts.ReplyMarkup == nil {
opts.ReplyMarkup = &ReplyMarkup{}
}
opts.ReplyMarkup.ForceReply = true
case OneTimeKeyboard:
if opts.ReplyMarkup == nil {
opts.ReplyMarkup = &ReplyMarkup{}
}
opts.ReplyMarkup.OneTimeKeyboard = true
case RemoveKeyboard:
if opts.ReplyMarkup == nil {
opts.ReplyMarkup = &ReplyMarkup{}
}
opts.ReplyMarkup.RemoveKeyboard = true
case Protected:
opts.Protected = true
default:
panic("telebot: unsupported flag-option")
}
case ParseMode:
opts.ParseMode = opt
case Entities:
opts.Entities = opt
default:
panic("telebot: unsupported send-option")
}
}
return opts
}
// extractCommandsParams extracts parameters for commands-related methods from the given options.
func extractCommandsParams(opts ...interface{}) (params CommandParams) {
for _, opt := range opts {
switch value := opt.(type) {
case []Command:
params.Commands = value
case string:
params.LanguageCode = value
case CommandScope:
params.Scope = &value
}
}
return
}
func (b *Bot) embedSendOptions(params map[string]string, opt *SendOptions) {
if b.parseMode != ModeDefault {
params["parse_mode"] = b.parseMode
}
if opt == nil {
return
}
if opt.ReplyTo != nil && opt.ReplyTo.ID != 0 {
params["reply_to_message_id"] = strconv.Itoa(opt.ReplyTo.ID)
}
if opt.DisableWebPagePreview {
params["disable_web_page_preview"] = "true"
}
if opt.DisableNotification {
params["disable_notification"] = "true"
}
if opt.ParseMode != ModeDefault {
params["parse_mode"] = opt.ParseMode
}
if len(opt.Entities) > 0 {
delete(params, "parse_mode")
entities, _ := json.Marshal(opt.Entities)
if params["caption"] != "" {
params["caption_entities"] = string(entities)
} else {
params["entities"] = string(entities)
}
}
if opt.AllowWithoutReply {
params["allow_sending_without_reply"] = "true"
}
if opt.ReplyMarkup != nil {
processButtons(opt.ReplyMarkup.InlineKeyboard)
replyMarkup, _ := json.Marshal(opt.ReplyMarkup)
params["reply_markup"] = string(replyMarkup)
}
if opt.Protected {
params["protect_content"] = "true"
}
}
func processButtons(keys [][]InlineButton) {
if keys == nil || len(keys) < 1 || len(keys[0]) < 1 {
return
}
for i := range keys {
for j := range keys[i] {
key := &keys[i][j]
if key.Unique != "" {
// Format: "\f<callback_name>|<data>"
data := key.Data
if data == "" {
key.Data = "\f" + key.Unique
} else {
key.Data = "\f" + key.Unique + "|" + data
}
}
}
}
}
func embedRights(p map[string]interface{}, rights Rights) {
data, _ := json.Marshal(rights)
_ = json.Unmarshal(data, &p)
}
func thumbnailToFilemap(thumb *Photo) map[string]File {
if thumb != nil {
return map[string]File{"thumb": thumb.File}
}
return nil
}
func isUserInList(user *User, list []User) bool {
for _, user2 := range list {
if user.ID == user2.ID {
return true
}
}
return false
}
func intsToStrs(ns []int) (s []string) {
for _, n := range ns {
s = append(s, strconv.Itoa(n))
}
return
}

@ -1,84 +0,0 @@
package telebot
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExtractOk(t *testing.T) {
data := []byte(`{"ok": true, "result": {}}`)
require.NoError(t, extractOk(data))
data = []byte(`{
"ok": false,
"error_code": 400,
"description": "Bad Request: reply message not found"
}`)
assert.EqualError(t, extractOk(data), ErrNotFoundToReply.Error())
data = []byte(`{
"ok": false,
"error_code": 429,
"description": "Too Many Requests: retry after 8",
"parameters": {"retry_after": 8}
}`)
assert.Equal(t, FloodError{
err: NewError(429, "Too Many Requests: retry after 8"),
RetryAfter: 8,
}, extractOk(data))
data = []byte(`{
"ok": false,
"error_code": 400,
"description": "Bad Request: group chat was upgraded to a supergroup chat",
"parameters": {"migrate_to_chat_id": -100123456789}
}`)
assert.Equal(t, GroupError{
err: ErrGroupMigrated,
MigratedTo: -100123456789,
}, extractOk(data))
}
func TestExtractMessage(t *testing.T) {
data := []byte(`{"ok":true,"result":true}`)
_, err := extractMessage(data)
assert.Equal(t, ErrTrueResult, err)
data = []byte(`{"ok":true,"result":{"foo":"bar"}}`)
_, err = extractMessage(data)
require.NoError(t, err)
}
func TestEmbedRights(t *testing.T) {
rights := NoRestrictions()
params := map[string]interface{}{
"chat_id": "1",
"user_id": "2",
}
embedRights(params, rights)
expected := map[string]interface{}{
"is_anonymous": false,
"chat_id": "1",
"user_id": "2",
"can_be_edited": true,
"can_send_messages": true,
"can_send_media_messages": true,
"can_send_polls": true,
"can_send_other_messages": true,
"can_add_web_page_previews": true,
"can_change_info": false,
"can_post_messages": false,
"can_edit_messages": false,
"can_delete_messages": false,
"can_invite_users": false,
"can_restrict_members": false,
"can_pin_messages": false,
"can_promote_members": false,
"can_manage_video_chats": false,
"can_manage_chat": false,
}
assert.Equal(t, expected, params)
}
Loading…
Cancel
Save