Compare commits

...

62 Commits
v3.1.2 ... v3

Author SHA1 Message Date
Demian a8acf84301 react: refactor 2 weeks ago
Demian 10fbde9b66 boost: refactor 2 weeks ago
Demian 1acc928a92 middleware: fix tests 2 weeks ago
Demian c9cec214a9 bot: rename DeleteMessages 2 weeks ago
Demian c4fce10013 bot: rename ForwardMessages and CopyMessages 2 weeks ago
Demian 47227931ec bot: refactor Trigger 4 weeks ago
Nash-Well e0c24df69d
context: add boost send cases, boost: fix UserBoosts (#686)
* context: add boost send cases, boost: fix UserBoosts

* context: change method order

* boost: add error wrap

* context: update sender method
1 month ago
Nash-Well 77f77a6840
bot: implement Trigger function (#687)
* bot: trigger init

* bot: add execution of registered handlers in trigger func
1 month ago
Demian a26ba9f0bd middleware: add context arg to the Recover 3 months ago
Demian af50a953b5 message: remove InaccessibleMessage completely 3 months ago
nash e16cc083f7 telebot,update: change OnBoostRemoved register 3 months ago
Demian 35d5dd372a layout: implement NewFromFS 3 months ago
Demian 9eb53434a0 react: refactor 4 months ago
Nash-Well 6fdf666a11
api: features 7.0, 7.1 (#658)
* api: feature 7.0 init

* message: remove blockquote

* api: partrly implement replies 2.0

* api: update comments

* message: add giveaway fields

* message,chat: other changes init

* boost: implement boost updates

* reactions: refactor

* api: implement multiple users request section

* api: provide preview customization

* api: implement 7.1 features

* message: fix story issue

* admin,context: refactor

* bot_test: type refactor

* api: partly fix review comments

* api: small tidy up

* react: reactions init

* telebot: added boost_updated,boost_removed constants

* message: fix InaccessibleMessage message struct parse

* message,reac: naming refactor

* context: add boost to the context of current events
4 months ago
Nikita b67df6444e media/test: refactor 4 months ago
Nikita 260a67b811 media: implement SetCaption in Album 4 months ago
Nikita cb88a11861 poller: add allowed_updates list 4 months ago
Demian 0d0d5e7473 topic: rename IconCustomEmojiID to IconCustomEmoji 4 months ago
Patrick e4f891ba6c Use recipient instead of Chat 4 months ago
Nash-Well af31945794
context: implement RespondText,RespondAlert methods (#660)
* context: implement RespondText,RespondAlert methods

* context: refactor
4 months ago
Nash-Well 5ff8b89b2b
errors: provide an error bot is not channel chat member (#659)
* errors: provide an error bot is not channel chat member

* errors: refactor
4 months ago
Nikita df250e64a2 api: implement bot api 6.9 4 months ago
Nash-Well 4670ccf5a3
api: implement 6.8 features (#652)
* api: implement 6.8 features

* chat,topic: refactor

* chat: change unixtime to lowercase
4 months ago
Demian 435ad606fa sticker: finish stickers api 4 months ago
Nash-Well 32b47a49ef
api: implement 6.6, 6.7 features (#655)
* api: implement 6.6-6.7 features
4 months ago
Reza Alipour 9c4cc7713e
inline: add game result support (#648)
* feat: add game to the list of inline query results

* fix: documentation and naming
4 months ago
Reza Alipour 49db4f5fac
callback: add missing struct fields (#647)
* fix: callback struct fields

* fix: documentation
4 months ago
70sh1 06bef0a71e Improve Args payload parsing
This us achieved by using strings.Fields instead of strings.Split

This results in a cleaner output when parsing messages like "/tags a          lot         of        spaces"

Output before: `[]string{"a", "", "", "", "", "", "", "", "", "", "lot", "", "", "", "", "", "", "", "", "of", "", "", "", "", "", "", "", "spaces"}`

Output after the change: `[]string{"a", "lot", "of", "spaces"}`
4 months ago
落心 59775c24a0 add entrytype blockquote 4 months ago
david 4cce303fbc bot,api: fix stop channel race 4 months ago
Demian 71ac2995cc errors: implement ErrIs 7 months ago
Demian b1cf07b13d markup: add reply user and chat Btn constructors 7 months ago
Demian 10d3480782 topic: finish and refactor the implementation 7 months ago
Demian 06bbf9e69f chat: add user_chat_id field 7 months ago
Demian 7b08a8f939 admin: add use_independent_chat_permissions param 7 months ago
Mikhail e6590d806a MarshalJSON should produce unique result ID
Result variable inside for-loop actually have the same address on each iteration, so we need to take address in underlying slice.
7 months ago
Demian 78610849e1 api 6.5: implement request_user and request_chat support params in ReplyButton 7 months ago
Demian cd009a68b3 media: remove omitempty flag from json tag in Sticker 7 months ago
Vlad Lukyanov 270e53bb90
media: add missed CustomEmoji field to the Sticker (#579)
Co-authored-by: Vladislav Lukianov <vladislav_lukianov@epam.com>
7 months ago
Demian 76f1c0eef8 errors: add missed entries to Err 7 months ago
awohsen 4e31cc6eac
errors: add join request related errors (#570) 7 months ago
George 3c4d8425f4 Added is_member field for ChatMember. Previously, it was not possible to check whether a ChatMemberRestricted had joined or left a chat. 7 months ago
Demian c581e8389b bot: describe how to include the caption in SendAlbum docs 7 months ago
Nikita fb8ce2a4b1
api: update to 6.4 (#606)
Co-authored-by: Demian <optigan@protonmail.com>
7 months ago
Demian 03dcac73d8 chat,message: naming 7 months ago
Demian d883371ded telebot: refactor bot api 6.3 7 months ago
Nikita 5d4079a489 api: update to 6.3 7 months ago
demget 2f38e4952f
Merge pull request #628 from tucnak/v3-middleware-fix
middleware: fix the append's capacity reusing
8 months ago
Demian 5f0d89877d tests: refactor middleware test cases 8 months ago
Demian 48c941a8d9 middleware: move m appending into separate func 8 months ago
demget bb76a9a642
Merge pull request #588 from mkuznets/mkuznets/fix-middleware-clobbering
Fix: handler middleware is overwritten when combined with group or global middleware
8 months ago
demget 699e5dbbdd
errors: add ErrKickedFromChannel 1 year ago
demget de01d4553e
Merge pull request #601 from Akionka/patch-1
Update layout.go
1 year ago
Akionka eb290fd947
Update layout.go 1 year ago
Max Kuznetsov bae47b52d4 Fix handler middleware clobbering when group middleware slice has extra capacity 1 year ago
demget fbd35f2103
Merge pull request #560 from osinniy/patch-1
Change Webhook.Poll log behaviour
1 year ago
Demian 92739f1414 api: always specify allowed_updates in getUpdates 1 year ago
demget 8a14d8a96b
Merge pull request #572 from hararudoka/patch-2
typo in docs
2 years ago
Ruslan 0b9f842a27
typo in docs 2 years ago
Demian ad84cbde5a context: add message Entities function 2 years ago
Demian 4c17ca7dc7 layout: fix text template func inside the inline results 2 years ago
Alex Osinniy c2b50e878f
Make SetWebhook error loggable by default (#559) 2 years ago

@ -21,12 +21,33 @@ type Rights struct {
CanRestrictMembers bool `json:"can_restrict_members"`
CanPromoteMembers bool `json:"can_promote_members"`
CanSendMessages bool `json:"can_send_messages"`
CanSendMedia bool `json:"can_send_media_messages"`
CanSendPolls bool `json:"can_send_polls"`
CanSendOther bool `json:"can_send_other_messages"`
CanAddPreviews bool `json:"can_add_web_page_previews"`
CanManageVideoChats bool `json:"can_manage_video_chats"`
CanManageChat bool `json:"can_manage_chat"`
CanManageTopics bool `json:"can_manage_topics"`
CanSendMedia bool `json:"can_send_media_messages,omitempty"` // deprecated
CanSendAudios bool `json:"can_send_audios"`
CanSendDocuments bool `json:"can_send_documents"`
CanSendPhotos bool `json:"can_send_photos"`
CanSendVideos bool `json:"can_send_videos"`
CanSendVideoNotes bool `json:"can_send_video_notes"`
CanSendVoiceNotes bool `json:"can_send_voice_notes"`
CanPostStories bool `json:"can_post_stories"`
CanEditStories bool `json:"can_edit_stories"`
CanDeleteStories bool `json:"can_delete_stories"`
// Independent defines whether the chat permissions are set independently.
// If not, the can_send_other_messages and can_add_web_page_previews permissions
// will imply the can_send_messages, can_send_audios, can_send_documents, can_send_photos,
// can_send_videos, can_send_video_notes, and can_send_voice_notes permissions;
// the can_send_polls permission will imply the can_send_messages permission.
//
// Works for Restrict and SetGroupPermissions methods only.
Independent bool `json:"-"`
}
// NoRights is the default Rights{}.
@ -35,9 +56,8 @@ func NoRights() Rights { return Rights{} }
// NoRestrictions should be used when un-restricting or
// un-promoting user.
//
// member.Rights = tele.NoRestrictions()
// b.Restrict(chat, member)
//
// member.Rights = tele.NoRestrictions()
// b.Restrict(chat, member)
func NoRestrictions() Rights {
return Rights{
CanBeEdited: true,
@ -50,12 +70,18 @@ func NoRestrictions() Rights {
CanPinMessages: false,
CanPromoteMembers: false,
CanSendMessages: true,
CanSendMedia: true,
CanSendPolls: true,
CanSendOther: true,
CanAddPreviews: true,
CanManageVideoChats: false,
CanManageChat: false,
CanManageTopics: false,
CanSendAudios: true,
CanSendDocuments: true,
CanSendPhotos: true,
CanSendVideos: true,
CanSendVideoNotes: true,
CanSendVoiceNotes: true,
}
}
@ -72,12 +98,21 @@ func AdminRights() Rights {
CanPinMessages: true,
CanPromoteMembers: true,
CanSendMessages: true,
CanSendMedia: true,
CanSendPolls: true,
CanSendOther: true,
CanAddPreviews: true,
CanManageVideoChats: true,
CanManageChat: true,
CanManageTopics: true,
CanSendAudios: true,
CanSendDocuments: true,
CanSendPhotos: true,
CanSendVideos: true,
CanSendVideoNotes: true,
CanSendVoiceNotes: true,
CanPostStories: true,
CanEditStories: true,
CanDeleteStories: true,
}
}
@ -120,20 +155,22 @@ func (b *Bot) Unban(chat *Chat, user *User, forBanned ...bool) error {
// Restrict lets you restrict a subset of member's rights until
// member.RestrictedUntil, such as:
//
// * can send messages
// * can send media
// * can send other
// * can add web page previews
//
// - can send messages
// - can send media
// - can send other
// - can add web page previews
func (b *Bot) Restrict(chat *Chat, member *ChatMember) error {
prv, until := member.Rights, member.RestrictedUntil
perms, until := member.Rights, member.RestrictedUntil
params := map[string]interface{}{
"chat_id": chat.Recipient(),
"user_id": member.User.Recipient(),
"until_date": strconv.FormatInt(until, 10),
"chat_id": chat.Recipient(),
"user_id": member.User.Recipient(),
"until_date": strconv.FormatInt(until, 10),
"permissions": perms,
}
if perms.Independent {
params["use_independent_chat_permissions"] = true
}
embedRights(params, prv)
_, err := b.Raw("restrictChatMember", params)
return err
@ -141,24 +178,21 @@ func (b *Bot) Restrict(chat *Chat, member *ChatMember) error {
// Promote lets you update member's admin rights, such as:
//
// * can change info
// * can post messages
// * can edit messages
// * can delete messages
// * can invite users
// * can restrict members
// * can pin messages
// * can promote members
//
// - can change info
// - can post messages
// - can edit messages
// - can delete messages
// - can invite users
// - can restrict members
// - can pin messages
// - can promote members
func (b *Bot) Promote(chat *Chat, member *ChatMember) error {
prv := member.Rights
params := map[string]interface{}{
"chat_id": chat.Recipient(),
"user_id": member.User.Recipient(),
"is_anonymous": member.Anonymous,
}
embedRights(params, prv)
embedRights(params, member.Rights)
_, err := b.Raw("promoteChatMember", params)
return err
@ -171,7 +205,6 @@ func (b *Bot) Promote(chat *Chat, member *ChatMember) error {
//
// 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(),

@ -20,7 +20,6 @@ func TestEmbedRights(t *testing.T) {
"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,
@ -34,6 +33,16 @@ func TestEmbedRights(t *testing.T) {
"can_promote_members": false,
"can_manage_video_chats": false,
"can_manage_chat": false,
"can_manage_topics": false,
"can_send_audios": true,
"can_send_documents": true,
"can_send_photos": true,
"can_send_videos": true,
"can_send_video_notes": true,
"can_send_voice_notes": true,
"can_post_stories": false,
"can_edit_stories": false,
"can_delete_stories": false,
}
assert.Equal(t, expected, params)
}

@ -27,18 +27,21 @@ func (b *Bot) Raw(method string, payload interface{}) ([]byte, error) {
return nil, err
}
// Cancel the request immediately without waiting for the timeout when bot is about to stop.
// Cancel the request immediately without waiting for the timeout
// when bot is about to stop.
// This may become important if doing long polling with long timeout.
exit := make(chan struct{})
defer close(exit)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
b.stopMu.RLock()
stopCh := b.stopClient
b.stopMu.RUnlock()
select {
case <-b.stopClient:
case <-stopCh:
cancel()
case <-exit:
case <-ctx.Done():
}
}()
@ -160,6 +163,19 @@ func addFileToWriter(writer *multipart.Writer, filename, field string, file inte
return err
}
func (f *File) process(name string, files map[string]File) string {
switch {
case f.InCloud():
return f.FileID
case f.FileURL != "":
return f.FileURL
case f.OnDisk() || f.FileReader != nil:
files[name] = *f
return "attach://" + name
}
return ""
}
func (b *Bot) sendText(to Recipient, text string, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
@ -217,13 +233,12 @@ func (b *Bot) getUpdates(offset, limit int, timeout time.Duration, allowed []str
"timeout": strconv.Itoa(int(timeout / time.Second)),
}
data, _ := json.Marshal(allowed)
params["allowed_updates"] = string(data)
if limit != 0 {
params["limit"] = strconv.Itoa(limit)
}
if len(allowed) > 0 {
data, _ := json.Marshal(allowed)
params["allowed_updates"] = string(data)
}
data, err := b.Raw("getUpdates", params)
if err != nil {
@ -239,6 +254,37 @@ func (b *Bot) getUpdates(offset, limit int, timeout time.Duration, allowed []str
return resp.Result, nil
}
func (b *Bot) forwardCopyMany(to Recipient, msgs []Editable, key string, opts ...*SendOptions) ([]Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
}
embedMessages(params, msgs)
if len(opts) > 0 {
b.embedSendOptions(params, opts[0])
}
data, err := b.Raw(key, params)
if err != nil {
return nil, err
}
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)
}
return nil, wrapError(err)
}
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.

@ -0,0 +1,113 @@
package telebot
import (
"encoding/json"
"time"
)
// Boost contains information about a chat boost.
type Boost struct {
// Unique identifier of the boost.
ID string `json:"boost_id"`
// Point in time (Unix timestamp) when the chat was boosted.
AddUnixtime int64 `json:"add_date"`
// Point in time (Unix timestamp) when the boost will automatically expire,
// unless the booster's Telegram Premium subscription is prolonged.
ExpirationUnixtime int64 `json:"expiration_date"`
// Source of the added boost.
Source *BoostSource `json:"source"`
}
// AddDate returns the moment of time when the chat has been boosted in local time.
func (c *Boost) AddDate() time.Time {
return time.Unix(c.AddUnixtime, 0)
}
// ExpirationDate returns the moment of time when the boost of the channel
// will expire in local time.
func (c *Boost) ExpirationDate() time.Time {
return time.Unix(c.ExpirationUnixtime, 0)
}
// BoostSourceType describes a type of boost.
type BoostSourceType = string
const (
BoostPremium BoostSourceType = "premium"
BoostGiftCode BoostSourceType = "gift_code"
BoostGiveaway BoostSourceType = "giveaway"
)
// BoostSource describes the source of a chat boost.
type BoostSource struct {
// Source of the boost, always (“premium”, “gift_code”, “giveaway”).
Source BoostSourceType `json:"source"`
// User that boosted the chat.
Booster *User `json:"user"`
// Identifier of a message in the chat with the giveaway; the message
// could have been deleted already. May be 0 if the message isn't sent yet.
GiveawayMessageID int `json:"giveaway_message_id,omitempty"`
// (Optional) True, if the giveaway was completed, but there was
// no user to win the prize.
Unclaimed bool `json:"is_unclaimed,omitempty"`
}
// BoostAdded represents a service message about a user boosting a chat.
type BoostAdded struct {
// Number of boosts added by the user.
Count int `json:"boost_count"`
}
// BoostUpdated represents a boost added to a chat or changed.
type BoostUpdated struct {
// Chat which was boosted.
Chat *Chat `json:"chat"`
// Information about the chat boost.
Boost *Boost `json:"boost"`
}
// BoostRemoved represents a boost removed from a chat.
type BoostRemoved struct {
// Chat which was boosted.
Chat *Chat `json:"chat"`
// Unique identifier of the boost.
BoostID string `json:"boost_id"`
// Point in time (Unix timestamp) when the boost was removed.
RemoveUnixtime int64 `json:"remove_date"`
// Source of the removed boost.
Source *BoostSource `json:"source"`
}
// UserBoosts gets the list of boosts added to a chat by a user.
// Requires administrator rights in the chat.
func (b *Bot) UserBoosts(chat, user Recipient) ([]Boost, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
"user_id": user.Recipient(),
}
data, err := b.Raw("getUserChatBoosts", params)
if err != nil {
return nil, err
}
var resp struct {
Result struct {
Boosts []Boost `json:"boosts"`
}
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result.Boosts, nil
}

287
bot.go

@ -10,6 +10,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"time"
)
@ -81,7 +82,9 @@ type Bot struct {
parseMode ParseMode
stop chan chan struct{}
client *http.Client
stopClient chan struct{}
stopMu sync.RWMutex
stopClient chan struct{}
}
// Settings represents a utility struct for passing certain
@ -160,35 +163,45 @@ var (
//
// Example:
//
// b.Handle("/start", func (c tele.Context) error {
// return c.Reply("Hello!")
// })
// b.Handle("/start", func (c tele.Context) error {
// return c.Reply("Hello!")
// })
//
// b.Handle(&inlineButton, func (c tele.Context) error {
// return c.Respond(&tele.CallbackResponse{Text: "Hello!"})
// })
// b.Handle(&inlineButton, func (c tele.Context) error {
// return c.Respond(&tele.CallbackResponse{Text: "Hello!"})
// })
//
// Middleware usage:
//
// b.Handle("/ban", onBan, middleware.Whitelist(ids...))
//
// b.Handle("/ban", onBan, middleware.Whitelist(ids...))
func (b *Bot) Handle(endpoint interface{}, h HandlerFunc, m ...MiddlewareFunc) {
end := extractEndpoint(endpoint)
if end == "" {
panic("telebot: unsupported endpoint")
}
if len(b.group.middleware) > 0 {
m = append(b.group.middleware, m...)
m = appendMiddleware(b.group.middleware, m)
}
handler := func(c Context) error {
b.handlers[end] = func(c Context) error {
return applyMiddleware(h, m...)(c)
}
}
switch end := endpoint.(type) {
case string:
b.handlers[end] = handler
case CallbackEndpoint:
b.handlers[end.CallbackUnique()] = handler
default:
panic("telebot: unsupported endpoint")
// Trigger executes the registered handler by the endpoint.
func (b *Bot) Trigger(endpoint interface{}, c Context) error {
end := extractEndpoint(endpoint)
if end == "" {
return fmt.Errorf("telebot: unsupported endpoint")
}
handler, ok := b.handlers[end]
if !ok {
return fmt.Errorf("telebot: no handler found for given endpoint")
}
return handler(c)
}
// Start brings bot into motion by consuming incoming
@ -199,10 +212,14 @@ func (b *Bot) Start() {
}
// do nothing if called twice
b.stopMu.Lock()
if b.stopClient != nil {
b.stopMu.Unlock()
return
}
b.stopClient = make(chan struct{})
b.stopMu.Unlock()
stop := make(chan struct{})
stopConfirm := make(chan struct{})
@ -222,7 +239,6 @@ func (b *Bot) Start() {
close(stop)
<-stopConfirm
close(confirm)
b.stopClient = nil
return
}
}
@ -230,9 +246,13 @@ func (b *Bot) Start() {
// Stop gracefully shuts the poller down.
func (b *Bot) Stop() {
b.stopMu.Lock()
if b.stopClient != nil {
close(b.stopClient)
b.stopClient = nil
}
b.stopMu.Unlock()
confirm := make(chan struct{})
b.stop <- confirm
<-confirm
@ -256,16 +276,16 @@ func (b *Bot) NewContext(u Update) Context {
// some Sendable (or string!) and optional send options.
//
// NOTE:
// Since most arguments are of type interface{}, but have pointer
// method receivers, make sure to pass them by-pointer, NOT by-value.
//
// What is a send option exactly? It can be one of the following types:
// Since most arguments are of type interface{}, but have pointer
// method receivers, make sure to pass them by-pointer, NOT by-value.
//
// - *SendOptions (the actual object accepted by Telegram API)
// - *ReplyMarkup (a component of SendOptions)
// - Option (a shortcut flag for popular options)
// - ParseMode (HTML, Markdown, etc)
// 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 shortcut flag for popular options)
// - ParseMode (HTML, Markdown, etc)
func (b *Bot) Send(to Recipient, what interface{}, opts ...interface{}) (*Message, error) {
if to == nil {
return nil, ErrBadRecipient
@ -284,6 +304,7 @@ func (b *Bot) Send(to Recipient, what interface{}, opts ...interface{}) (*Messag
}
// SendAlbum sends multiple instances of media as a single message.
// To include the caption, make sure the first Inputtable of an album has it.
// From all existing options, it only supports tele.Silent.
func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message, error) {
if to == nil {
@ -295,21 +316,8 @@ func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message,
files := make(map[string]File)
for i, x := range a {
var (
repr string
data []byte
file = x.MediaFile()
)
switch {
case file.InCloud():
repr = file.FileID
case file.FileURL != "":
repr = file.FileURL
case file.OnDisk() || file.FileReader != nil:
repr = "attach://" + strconv.Itoa(i)
files[strconv.Itoa(i)] = *file
default:
repr := x.MediaFile().process(strconv.Itoa(i), files)
if repr == "" {
return nil, fmt.Errorf("telebot: album entry #%d does not exist", i)
}
@ -322,7 +330,7 @@ func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message,
im.ParseMode = sendOpts.ParseMode
}
data, _ = json.Marshal(im)
data, _ := json.Marshal(im)
media[i] = string(data)
}
@ -403,6 +411,17 @@ func (b *Bot) Forward(to Recipient, msg Editable, opts ...interface{}) (*Message
return extractMessage(data)
}
// ForwardMany method forwards multiple messages of any kind.
// If some of the specified messages can't be found or forwarded, they are skipped.
// Service messages and messages with protected content can't be forwarded.
// Album grouping is kept for forwarded messages.
func (b *Bot) ForwardMany(to Recipient, msgs []Editable, opts ...*SendOptions) ([]Message, error) {
if to == nil {
return nil, ErrBadRecipient
}
return b.forwardCopyMany(to, msgs, "forwardMessages", opts...)
}
// Copy behaves just like Forward() but the copied message doesn't have a link to the original message (see Bots API).
//
// This function will panic upon nil Editable.
@ -429,6 +448,20 @@ func (b *Bot) Copy(to Recipient, msg Editable, options ...interface{}) (*Message
return extractMessage(data)
}
// CopyMany this method makes a copy of messages of any kind.
// If some of the specified messages can't be found or copied, they are skipped.
// Service messages, giveaway messages, giveaway winners messages, and
// invoice messages can't be copied. A quiz poll can be copied only if the value of the field
// correct_option_id is known to the bot. The method is analogous
// to the method forwardMessages, but the copied messages don't have a link to the original message.
// Album grouping is kept for copied messages.
func (b *Bot) CopyMany(to Recipient, msgs []Editable, opts ...*SendOptions) ([]Message, error) {
if to == nil {
return nil, ErrBadRecipient
}
return b.forwardCopyMany(to, msgs, "copyMessages", opts...)
}
// Edit is magic, it lets you change already sent message.
// This function will panic upon nil Editable.
//
@ -437,14 +470,13 @@ func (b *Bot) Copy(to Recipient, msg Editable, options ...interface{}) (*Message
//
// Use cases:
//
// b.Edit(m, m.Text, newMarkup)
// b.Edit(m, "new <b>text</b>", tele.ModeHTML)
// b.Edit(m, &tele.ReplyMarkup{...})
// b.Edit(m, &tele.Photo{File: ...})
// b.Edit(m, tele.Location{42.1337, 69.4242})
// b.Edit(c, "edit inline message from the callback")
// b.Edit(r, "edit message from chosen inline result")
//
// b.Edit(m, m.Text, newMarkup)
// b.Edit(m, "new <b>text</b>", tele.ModeHTML)
// b.Edit(m, &tele.ReplyMarkup{...})
// b.Edit(m, &tele.Photo{File: ...})
// b.Edit(m, tele.Location{42.1337, 69.4242})
// b.Edit(c, "edit inline message from the callback")
// b.Edit(r, "edit message from chosen inline result")
func (b *Bot) Edit(msg Editable, what interface{}, opts ...interface{}) (*Message, error) {
var (
method string
@ -503,7 +535,6 @@ func (b *Bot) Edit(msg Editable, what interface{}, opts ...interface{}) (*Messag
//
// If edited message is sent by the bot, returns it,
// otherwise returns nil and ErrTrueResult.
//
func (b *Bot) EditReplyMarkup(msg Editable, markup *ReplyMarkup) (*Message, error) {
msgID, chatID := msg.MessageSig()
params := make(map[string]string)
@ -537,7 +568,6 @@ func (b *Bot) EditReplyMarkup(msg Editable, markup *ReplyMarkup) (*Message, erro
//
// If edited message is sent by the bot, returns it,
// otherwise returns nil and ErrTrueResult.
//
func (b *Bot) EditCaption(msg Editable, caption string, opts ...interface{}) (*Message, error) {
msgID, chatID := msg.MessageSig()
@ -571,9 +601,8 @@ func (b *Bot) EditCaption(msg Editable, caption string, opts ...interface{}) (*M
//
// Use cases:
//
// b.EditMedia(m, &tele.Photo{File: tele.FromDisk("chicken.jpg")})
// b.EditMedia(m, &tele.Video{File: tele.FromURL("http://video.mp4")})
//
// b.EditMedia(m, &tele.Photo{File: tele.FromDisk("chicken.jpg")})
// b.EditMedia(m, &tele.Video{File: tele.FromURL("http://video.mp4")})
func (b *Bot) EditMedia(msg Editable, media Inputtable, opts ...interface{}) (*Message, error) {
var (
repr string
@ -655,15 +684,14 @@ func (b *Bot) EditMedia(msg Editable, media Inputtable, opts ...interface{}) (*M
// Delete removes the message, including service messages.
// This function will panic upon nil Editable.
//
// - A message can only be deleted if it was sent less than 48 hours ago.
// - A dice message in a private chat can only be deleted if it was sent more than 24 hours ago.
// - Bots can delete outgoing messages in private chats, groups, and supergroups.
// - Bots can delete incoming messages in private chats.
// - 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.
//
// - A message can only be deleted if it was sent less than 48 hours ago.
// - A dice message in a private chat can only be deleted if it was sent more than 24 hours ago.
// - Bots can delete outgoing messages in private chats, groups, and supergroups.
// - Bots can delete incoming messages in private chats.
// - 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(msg Editable) error {
msgID, chatID := msg.MessageSig()
@ -676,6 +704,16 @@ func (b *Bot) Delete(msg Editable) error {
return err
}
// DeleteMany deletes multiple messages simultaneously.
// If some of the specified messages can't be found, they are skipped.
func (b *Bot) DeleteMany(msgs []Editable) error {
params := make(map[string]string)
embedMessages(params, msgs)
_, err := b.Raw("deleteMessages", params)
return err
}
// Notify updates the chat action for recipient.
//
// Chat action is a status message that recipient would see where
@ -685,8 +723,7 @@ func (b *Bot) Delete(msg Editable) error {
//
// Currently, Telegram supports only a narrow range of possible
// actions, these are aligned as constants of this package.
//
func (b *Bot) Notify(to Recipient, action ChatAction) error {
func (b *Bot) Notify(to Recipient, action ChatAction, threadID ...int) error {
if to == nil {
return ErrBadRecipient
}
@ -696,6 +733,10 @@ func (b *Bot) Notify(to Recipient, action ChatAction) error {
"action": string(action),
}
if len(threadID) > 0 {
params["message_thread_id"] = strconv.Itoa(threadID[0])
}
_, err := b.Raw("sendChatAction", params)
return err
}
@ -705,10 +746,9 @@ func (b *Bot) Notify(to Recipient, action ChatAction) error {
//
// Example:
//
// b.Ship(query) // OK
// b.Ship(query, opts...) // OK with options
// b.Ship(query, "Oops!") // Error message
//
// b.Ship(query) // OK
// b.Ship(query, opts...) // OK with options
// b.Ship(query, "Oops!") // Error message
func (b *Bot) Ship(query *ShippingQuery, what ...interface{}) error {
params := map[string]string{
"shipping_query_id": query.ID,
@ -761,9 +801,8 @@ func (b *Bot) Accept(query *PreCheckoutQuery, errorMessage ...string) error {
//
// Example:
//
// b.Respond(c)
// b.Respond(c, response)
//
// b.Respond(c)
// b.Respond(c, response)
func (b *Bot) Respond(c *Callback, resp ...*CallbackResponse) error {
var r *CallbackResponse
if resp == nil {
@ -821,7 +860,6 @@ func (b *Bot) AnswerWebApp(query *Query, r Result) (*WebAppMessage, error) {
//
// 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,
@ -901,7 +939,6 @@ func (b *Bot) File(file *File) (io.ReadCloser, error) {
//
// If the message is sent by the bot, returns it,
// otherwise returns nil and ErrTrueResult.
//
func (b *Bot) StopLiveLocation(msg Editable, opts ...interface{}) (*Message, error) {
msgID, chatID := msg.MessageSig()
@ -926,7 +963,6 @@ func (b *Bot) StopLiveLocation(msg Editable, opts ...interface{}) (*Message, err
//
// It supports ReplyMarkup.
// This function will panic upon nil Editable.
//
func (b *Bot) StopPoll(msg Editable, opts ...interface{}) (*Poll, error) {
msgID, chatID := msg.MessageSig()
@ -953,7 +989,7 @@ func (b *Bot) StopPoll(msg Editable, opts ...interface{}) (*Poll, error) {
}
// Leave makes bot leave a group, supergroup or channel.
func (b *Bot) Leave(chat *Chat) error {
func (b *Bot) Leave(chat Recipient) error {
params := map[string]string{
"chat_id": chat.Recipient(),
}
@ -966,7 +1002,6 @@ func (b *Bot) Leave(chat *Chat) error {
//
// It supports Silent option.
// This function will panic upon nil Editable.
//
func (b *Bot) Pin(msg Editable, opts ...interface{}) error {
msgID, chatID := msg.MessageSig()
@ -984,7 +1019,7 @@ func (b *Bot) Pin(msg Editable, opts ...interface{}) error {
// Unpin unpins a message in a supergroup or a channel.
// It supports tb.Silent option.
func (b *Bot) Unpin(chat *Chat, messageID ...int) error {
func (b *Bot) Unpin(chat Recipient, messageID ...int) error {
params := map[string]string{
"chat_id": chat.Recipient(),
}
@ -998,7 +1033,7 @@ 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 {
func (b *Bot) UnpinAll(chat Recipient) error {
params := map[string]string{
"chat_id": chat.Recipient(),
}
@ -1011,7 +1046,6 @@ func (b *Bot) UnpinAll(chat *Chat) error {
//
// Including current name of the user for one-on-one conversations,
// current username of a user, group or channel, etc.
//
func (b *Bot) ChatByID(id int64) (*Chat, error) {
return b.ChatByUsername(strconv.FormatInt(id, 10))
}
@ -1109,9 +1143,8 @@ func (b *Bot) MenuButton(chat *User) (*MenuButton, error) {
//
// It accepts two kinds of menu button arguments:
//
// - MenuButtonType for simple menu buttons (default, commands)
// - MenuButton complete structure for web_app menu button type
//
// - MenuButtonType for simple menu buttons (default, commands)
// - MenuButton complete structure for web_app menu button type
func (b *Bot) SetMenuButton(chat *User, mb interface{}) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
@ -1161,3 +1194,89 @@ func (b *Bot) Close() (bool, error) {
return resp.Result, nil
}
// BotInfo represents a single object of BotName, BotDescription, BotShortDescription instances.
type BotInfo struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
ShortDescription string `json:"short_description,omitempty"`
}
// SetMyName change's the bot name.
func (b *Bot) SetMyName(name, language string) error {
params := map[string]string{
"name": name,
"language_code": language,
}
_, err := b.Raw("setMyName", params)
return err
}
// MyName returns the current bot name for the given user language.
func (b *Bot) MyName(language string) (*BotInfo, error) {
return b.botInfo(language, "getMyName")
}
// SetMyDescription change's the bot description, which is shown in the chat
// with the bot if the chat is empty.
func (b *Bot) SetMyDescription(desc, language string) error {
params := map[string]string{
"description": desc,
"language_code": language,
}
_, err := b.Raw("setMyDescription", params)
return err
}
// MyDescription the current bot description for the given user language.
func (b *Bot) MyDescription(language string) (*BotInfo, error) {
return b.botInfo(language, "getMyDescription")
}
// SetMyShortDescription change's the bot short description, which is shown on
// the bot's profile page and is sent together with the link when users share the bot.
func (b *Bot) SetMyShortDescription(desc, language string) error {
params := map[string]string{
"short_description": desc,
"language_code": language,
}
_, err := b.Raw("setMyShortDescription", params)
return err
}
// MyShortDescription the current bot short description for the given user language.
func (b *Bot) MyShortDescription(language string) (*BotInfo, error) {
return b.botInfo(language, "getMyShortDescription")
}
func (b *Bot) botInfo(language, key string) (*BotInfo, error) {
params := map[string]string{
"language_code": language,
}
data, err := b.Raw(key, params)
if err != nil {
return nil, err
}
var resp struct {
Result *BotInfo
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
}
func extractEndpoint(endpoint interface{}) string {
switch end := endpoint.(type) {
case string:
return end
case CallbackEndpoint:
return end.CallbackUnique()
}
return ""
}

@ -24,6 +24,9 @@ var (
b, _ = newTestBot() // cached bot instance to avoid getMe method flooding
to = &Chat{ID: chatID} // to chat recipient for send and edit methods
user = &User{ID: userID} // to user recipient for some special cases
logo = FromURL("https://telegra.ph/file/c95b8fe46dd3df15d12e5.png")
thumb = FromURL("https://telegra.ph/file/fe28e378784b3a4e367fb.png")
)
func defaultSettings() Settings {
@ -373,6 +376,116 @@ func TestBotOnError(t *testing.T) {
assert.True(t, ok)
}
func TestBotMiddleware(t *testing.T) {
t.Run("calling order", func(t *testing.T) {
var trace []string
handler := func(name string) HandlerFunc {
return func(c Context) error {
trace = append(trace, name)
return nil
}
}
middleware := func(name string) MiddlewareFunc {
return func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
trace = append(trace, name+":in")
err := next(c)
trace = append(trace, name+":out")
return err
}
}
}
b, err := NewBot(Settings{Synchronous: true, Offline: true})
if err != nil {
t.Fatal(err)
}
b.Use(middleware("global1"), middleware("global2"))
b.Handle("/a", handler("/a"), middleware("handler1a"), middleware("handler2a"))
group := b.Group()
group.Use(middleware("group1"), middleware("group2"))
group.Handle("/b", handler("/b"), middleware("handler1b"))
b.ProcessUpdate(Update{
Message: &Message{Text: "/a"},
})
assert.Equal(t, []string{
"global1:in", "global2:in",
"handler1a:in", "handler2a:in",
"/a",
"handler2a:out", "handler1a:out",
"global2:out", "global1:out",
}, trace)
trace = trace[:0]
b.ProcessUpdate(Update{
Message: &Message{Text: "/b"},
})
assert.Equal(t, []string{
"global1:in", "global2:in",
"group1:in", "group2:in",
"handler1b:in",
"/b",
"handler1b:out",
"group2:out", "group1:out",
"global2:out", "global1:out",
}, trace)
})
fatal := func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
t.Fatal("fatal middleware should not be called")
return nil
}
}
nop := func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
return next(c)
}
}
t.Run("combining with global middleware", func(t *testing.T) {
b, err := NewBot(Settings{Synchronous: true, Offline: true})
if err != nil {
t.Fatal(err)
}
// Pre-allocate middleware slice to make sure
// it has extra capacity after group-level middleware is added.
b.group.middleware = make([]MiddlewareFunc, 0, 2)
b.Use(nop)
b.Handle("/a", func(c Context) error { return nil }, nop)
b.Handle("/b", func(c Context) error { return nil }, fatal)
b.ProcessUpdate(Update{Message: &Message{Text: "/a"}})
})
t.Run("combining with group middleware", func(t *testing.T) {
b, err := NewBot(Settings{Synchronous: true, Offline: true})
if err != nil {
t.Fatal(err)
}
g := b.Group()
// Pre-allocate middleware slice to make sure
// it has extra capacity after group-level middleware is added.
g.middleware = make([]MiddlewareFunc, 0, 2)
g.Use(nop)
g.Handle("/a", func(c Context) error { return nil }, nop)
g.Handle("/b", func(c Context) error { return nil }, fatal)
b.ProcessUpdate(Update{Message: &Message{Text: "/a"}})
})
}
func TestBot(t *testing.T) {
if b == nil {
t.Skip("Cached bot instance is bad (probably wrong or empty TELEBOT_SECRET)")
@ -392,7 +505,7 @@ func TestBot(t *testing.T) {
assert.Equal(t, ErrBadRecipient, err)
photo := &Photo{
File: FromURL("https://telegra.ph/file/65c5237b040ebf80ec278.jpg"),
File: logo,
Caption: t.Name(),
}
var msg *Message

@ -26,6 +26,15 @@ type Callback struct {
// a bad client can send arbitrary data in this field.
Data string `json:"data"`
// ChatInstance is a global identifier, uniquely corresponding to
// the chat to which the message with the callback button was sent.
ChatInstance string `json:"chat_instance"`
// GameShortName is a unique identifier of the game for which a URL
// is requested from the bot when a user presses the Play button of
// that game. GameShortName may be empty
GameShortName string `json:"game_short_name"`
// Unique displays an unique of the button from which the
// callback was fired. Sets immediately before the handling,
// while the Data field stores only with payload.

@ -10,13 +10,16 @@ import (
type User struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
LanguageCode string `json:"language_code"`
IsBot bool `json:"is_bot"`
IsPremium bool `json:"is_premium"`
AddedToMenu bool `json:"added_to_attachment_menu"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
IsForum bool `json:"is_forum"`
Username string `json:"username"`
LanguageCode string `json:"language_code"`
IsBot bool `json:"is_bot"`
IsPremium bool `json:"is_premium"`
AddedToMenu bool `json:"added_to_attachment_menu"`
Usernames []string `json:"active_usernames"`
CustomEmojiStatus string `json:"emoji_status_custom_emoji_id"`
// Returns only in getMe
CanJoinGroups bool `json:"can_join_groups"`
@ -44,20 +47,32 @@ type Chat struct {
Username string `json:"username"`
// Returns only in getChat
Bio string `json:"bio,omitempty"`
Photo *ChatPhoto `json:"photo,omitempty"`
Description string `json:"description,omitempty"`
InviteLink string `json:"invite_link,omitempty"`
PinnedMessage *Message `json:"pinned_message,omitempty"`
Permissions *Rights `json:"permissions,omitempty"`
SlowMode int `json:"slow_mode_delay,omitempty"`
StickerSet string `json:"sticker_set_name,omitempty"`
CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"`
LinkedChatID int64 `json:"linked_chat_id,omitempty"`
ChatLocation *ChatLocation `json:"location,omitempty"`
Private bool `json:"has_private_forwards,omitempty"`
Protected bool `json:"has_protected_content,omitempty"`
NoVoiceAndVideo bool `json:"has_restricted_voice_and_video_messages"`
Bio string `json:"bio,omitempty"`
Photo *ChatPhoto `json:"photo,omitempty"`
Description string `json:"description,omitempty"`
InviteLink string `json:"invite_link,omitempty"`
PinnedMessage *Message `json:"pinned_message,omitempty"`
Permissions *Rights `json:"permissions,omitempty"`
Reactions []Reaction `json:"available_reactions"`
SlowMode int `json:"slow_mode_delay,omitempty"`
StickerSet string `json:"sticker_set_name,omitempty"`
CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"`
CustomEmojiSetName string `json:"custom_emoji_sticker_set_name"`
LinkedChatID int64 `json:"linked_chat_id,omitempty"`
ChatLocation *ChatLocation `json:"location,omitempty"`
Private bool `json:"has_private_forwards,omitempty"`
Protected bool `json:"has_protected_content,omitempty"`
NoVoiceAndVideo bool `json:"has_restricted_voice_and_video_messages"`
HasHiddenMembers bool `json:"has_hidden_members,omitempty"`
AggressiveAntiSpam bool `json:"has_aggressive_anti_spam_enabled,omitempty"`
CustomEmojiID string `json:"emoji_status_custom_emoji_id"`
EmojiExpirationUnixtime int64 `json:"emoji_status_expiration_date"`
BackgroundEmojiID string `json:"background_custom_emoji_id"`
AccentColorID int `json:"accent_color_id"`
ProfileAccentColorID int `json:"profile_accent_color_id"`
ProfileBackgroundEmojiID string `json:"profile_background_custom_emoji_id"`
HasVisibleHistory bool `json:"has_visible_history"`
UnrestrictBoosts int `json:"unrestrict_boost_count"`
}
// Recipient returns chat ID (see Recipient interface).
@ -101,6 +116,7 @@ type ChatMember struct {
Role MemberStatus `json:"status"`
Title string `json:"custom_title"`
Anonymous bool `json:"is_anonymous"`
Member bool `json:"is_member,omitempty"`
// Date when restrictions will be lifted for the user, unix time.
//
@ -148,6 +164,9 @@ type ChatMemberUpdate struct {
// (Optional) InviteLink which was used by the user to
// join the chat; for joining by invite link events only.
InviteLink *ChatInviteLink `json:"invite_link"`
// (Optional) True, if the user joined the chat via a chat folder invite link.
ViaFolderLink bool `json:"via_chat_folder_invite_link"`
}
// Time returns the moment of the change in local time.
@ -162,14 +181,13 @@ func (c *ChatMemberUpdate) Time() time.Time {
//
// Example:
//
// group := tele.ChatID(-100756389456)
// b.Send(group, "Hello!")
//
// type Config struct {
// AdminGroup tele.ChatID `json:"admin_group"`
// }
// b.Send(conf.AdminGroup, "Hello!")
// group := tele.ChatID(-100756389456)
// b.Send(group, "Hello!")
//
// type Config struct {
// AdminGroup tele.ChatID `json:"admin_group"`
// }
// b.Send(conf.AdminGroup, "Hello!")
type ChatID int64
// Recipient returns chat ID (see Recipient interface).
@ -185,6 +203,12 @@ type ChatJoinRequest struct {
// Sender is the user that sent the join request.
Sender *User `json:"from"`
// UserChatID is an ID of a private chat with the user
// who sent the join request. The bot can use this ID
// for 5 minutes to send messages until the join request
// is processed, assuming no other administrator contacted the user.
UserChatID int64 `json:"user_chat_id"`
// Unixtime, use ChatJoinRequest.Time() to get time.Time.
Unixtime int64 `json:"date"`
@ -229,6 +253,14 @@ type ChatInviteLink struct {
PendingCount int `json:"pending_join_request_count"`
}
type Story struct {
// Unique identifier for the story in the chat
ID int `json:"id"`
// Chat that posted the story
Poster *Chat `json:"chat"`
}
// ExpireDate returns the moment of the link expiration in local time.
func (c *ChatInviteLink) ExpireDate() time.Time {
return time.Unix(c.ExpireUnixtime, 0)
@ -239,6 +271,11 @@ func (r ChatJoinRequest) Time() time.Time {
return time.Unix(r.Unixtime, 0)
}
// Time returns the moment of the emoji status expiration.
func (c *Chat) Time() time.Time {
return time.Unix(c.EmojiExpirationUnixtime, 0)
}
// InviteLink should be used to export chat's invite link.
func (b *Bot) InviteLink(chat *Chat) (string, error) {
params := map[string]string{
@ -427,6 +464,9 @@ func (b *Bot) SetGroupPermissions(chat *Chat, perms Rights) error {
"chat_id": chat.Recipient(),
"permissions": perms,
}
if perms.Independent {
params["use_independent_chat_permissions"] = true
}
_, err := b.Raw("setChatPermissions", params)
return err

@ -46,12 +46,21 @@ type Context interface {
// ChatMember returns chat member changes.
ChatMember() *ChatMemberUpdate
// ChatJoinRequest returns cha
// ChatJoinRequest returns the chat join request.
ChatJoinRequest() *ChatJoinRequest
// Migration returns both migration from and to chat IDs.
Migration() (int64, int64)
// Topic returns the topic changes.
Topic() *Topic
// Boost returns the boost instance.
Boost() *BoostUpdated
// BoostRemoved returns the boost removed from a chat instance.
BoostRemoved() *BoostRemoved
// Sender returns the current recipient, depending on the context type.
// Returns nil if user is not presented.
Sender() *User
@ -70,6 +79,10 @@ type Context interface {
// In the case when no related data presented, returns an empty string.
Text() string
// Entities returns the message entities, whether it's media caption's or the text's.
// In the case when no entities presented, returns a nil.
Entities() Entities
// Data returns the current data, depending on the context type.
// If the context contains command, returns its arguments string.
// If the context contains payment, returns its payload.
@ -145,6 +158,12 @@ type Context interface {
// See Respond from bot.go.
Respond(resp ...*CallbackResponse) error
// RespondText sends a popup response for the current callback query.
RespondText(text string) error
// RespondAlert sends an alert response for the current callback query.
RespondAlert(text string) error
// Get retrieves data from the context.
Get(key string) interface{}
@ -236,6 +255,30 @@ func (c *nativeContext) Migration() (int64, int64) {
return c.u.Message.MigrateFrom, c.u.Message.MigrateTo
}
func (c *nativeContext) Topic() *Topic {
m := c.u.Message
if m == nil {
return nil
}
switch {
case m.TopicCreated != nil:
return m.TopicCreated
case m.TopicReopened != nil:
return m.TopicReopened
case m.TopicEdited != nil:
return m.TopicEdited
}
return nil
}
func (c *nativeContext) Boost() *BoostUpdated {
return c.u.Boost
}
func (c *nativeContext) BoostRemoved() *BoostRemoved {
return c.u.BoostRemoved
}
func (c *nativeContext) Sender() *User {
switch {
case c.u.Callback != nil:
@ -258,9 +301,16 @@ func (c *nativeContext) Sender() *User {
return c.u.ChatMember.Sender
case c.u.ChatJoinRequest != nil:
return c.u.ChatJoinRequest.Sender
default:
return nil
case c.u.Boost != nil:
if b := c.u.Boost.Boost; b != nil && b.Source != nil {
return b.Source.Booster
}
case c.u.BoostRemoved != nil:
if b := c.u.BoostRemoved; b.Source != nil {
return b.Source.Booster
}
}
return nil
}
func (c *nativeContext) Chat() *Chat {
@ -297,6 +347,17 @@ func (c *nativeContext) Text() string {
return m.Text
}
func (c *nativeContext) Entities() Entities {
m := c.Message()
if m == nil {
return nil
}
if len(m.CaptionEntities) > 0 {
return m.CaptionEntities
}
return m.Entities
}
func (c *nativeContext) Data() string {
switch {
case c.u.Message != nil:
@ -321,7 +382,7 @@ func (c *nativeContext) Args() []string {
case c.u.Message != nil:
payload := strings.Trim(c.u.Message.Payload, " ")
if payload != "" {
return strings.Split(payload, " ")
return strings.Fields(payload)
}
case c.u.Callback != nil:
return strings.Split(c.u.Callback.Data, "|")
@ -447,6 +508,14 @@ func (c *nativeContext) Respond(resp ...*CallbackResponse) error {
return c.b.Respond(c.u.Callback, resp...)
}
func (c *nativeContext) RespondText(text string) error {
return c.Respond(&CallbackResponse{Text: text})
}
func (c *nativeContext) RespondAlert(text string) error {
return c.Respond(&CallbackResponse{Text: text, ShowAlert: true})
}
func (c *nativeContext) Answer(resp *QueryResponse) error {
if c.u.Query == nil {
return errors.New("telebot: context inline query is nil")

@ -1,6 +1,7 @@
package telebot
import (
"errors"
"fmt"
"strings"
)
@ -77,6 +78,7 @@ var (
// Bad request errors
var (
ErrBadButtonData = NewError(400, "Bad Request: BUTTON_DATA_INVALID")
ErrBadUserID = NewError(400, "Bad Request: USER_ID_INVALID")
ErrBadPollOptions = NewError(400, "Bad Request: expected an Array of String as options")
ErrBadURLContent = NewError(400, "Bad Request: failed to get HTTP URL content")
ErrCantEditMessage = NewError(400, "Bad Request: message can't be edited")
@ -117,6 +119,10 @@ var (
ErrWrongTypeOfContent = NewError(400, "Bad Request: wrong type of the web page content")
ErrWrongURL = NewError(400, "Bad Request: wrong HTTP URL specified")
ErrForwardMessage = NewError(400, "Bad Request: administrators of the chat restricted message forwarding")
ErrUserAlreadyParticipant = NewError(400, "Bad Request: USER_ALREADY_PARTICIPANT", "User is already a participant")
ErrHideRequesterMissing = NewError(400, "Bad Request: HIDE_REQUESTER_MISSING")
ErrChannelsTooMuch = NewError(400, "Bad Request: CHANNELS_TOO_MUCH")
ErrChannelsTooMuchUser = NewError(400, "Bad Request: USER_CHANNELS_TOO_MUCH")
)
// Forbidden errors
@ -124,8 +130,10 @@ var (
ErrBlockedByUser = NewError(403, "Forbidden: bot was blocked by the user")
ErrKickedFromGroup = NewError(403, "Forbidden: bot was kicked from the group chat")
ErrKickedFromSuperGroup = NewError(403, "Forbidden: bot was kicked from the supergroup chat")
ErrKickedFromChannel = NewError(403, "Forbidden: bot was kicked from the channel chat")
ErrNotStartedByUser = NewError(403, "Forbidden: bot can't initiate conversation with a user")
ErrUserIsDeactivated = NewError(403, "Forbidden: user is deactivated")
ErrNotChannelMember = NewError(403, "Forbidden: bot is not a member of the channel chat")
)
// Err returns Error instance by given description.
@ -141,6 +149,8 @@ func Err(s string) error {
return ErrInternal
case ErrBadButtonData.ʔ():
return ErrBadButtonData
case ErrBadUserID.ʔ():
return ErrBadUserID
case ErrBadPollOptions.ʔ():
return ErrBadPollOptions
case ErrBadURLContent.ʔ():
@ -225,17 +235,34 @@ func Err(s string) error {
return ErrKickedFromGroup
case ErrKickedFromSuperGroup.ʔ():
return ErrKickedFromSuperGroup
case ErrKickedFromChannel.ʔ():
return ErrKickedFromChannel
case ErrNotStartedByUser.ʔ():
return ErrNotStartedByUser
case ErrUserIsDeactivated.ʔ():
return ErrUserIsDeactivated
case ErrForwardMessage.ʔ():
return ErrForwardMessage
case ErrUserAlreadyParticipant.ʔ():
return ErrUserAlreadyParticipant
case ErrHideRequesterMissing.ʔ():
return ErrHideRequesterMissing
case ErrChannelsTooMuch.ʔ():
return ErrChannelsTooMuch
case ErrChannelsTooMuchUser.ʔ():
return ErrChannelsTooMuchUser
case ErrNotChannelMember.ʔ():
return ErrNotChannelMember
default:
return nil
}
}
// ErrIs checks if the error with given description matches an error err.
func ErrIs(s string, err error) bool {
return errors.Is(err, Err(s))
}
// wrapError returns new wrapped telebot-related error.
func wrapError(err error) error {
return fmt.Errorf("telebot: %w", err)

@ -0,0 +1,103 @@
package telebot
import "time"
// Giveaway represents a message about a scheduled giveaway.
type Giveaway struct {
// The list of chats which the user must join to participate in the giveaway.
Chats []Chat `json:"chats"`
// Point in time (Unix timestamp) when winners of the giveaway will be selected.
SelectionUnixtime int64 `json:"winners_selection_date"`
// The number of users which are supposed to be selected as winners of the giveaway.
WinnerCount int `json:"winner_count"`
// (Optional) True, if only users who join the chats after the giveaway
// started should be eligible to win.
OnlyNewMembers bool `json:"only_new_members"`
// (Optional) True, if the list of giveaway winners will be visible to everyone.
HasPublicWinners bool `json:"has_public_winners"`
// (Optional) Description of additional giveaway prize.
PrizeDescription string `json:"prize_description"`
// (Optional) A list of two-letter ISO 3166-1 alpha-2 country codes indicating
// the countries from which eligible users for the giveaway must come.
// If empty, then all users can participate in the giveaway. Users with a phone number
// that was bought on Fragment can always participate in giveaways.
CountryCodes []string `json:"country_codes"`
// (Optional) The number of months the Telegram Premium subscription won from
// the giveaway will be active for.
PremiumMonthCount int `json:"premium_subscription_month_count"`
}
// SelectionDate returns the moment of when winners of the giveaway were selected in local time.
func (g *Giveaway) SelectionDate() time.Time {
return time.Unix(g.SelectionUnixtime, 0)
}
// GiveawayWinners object represents a message about the completion of a
// giveaway with public winners.
type GiveawayWinners struct {
// The chat that created the giveaway.
Chat *Chat `json:"chat"`
// Identifier of the message with the giveaway in the chat.
MessageID int `json:"message_id"`
// Point in time (Unix timestamp) when winners of the giveaway were selected.
SelectionUnixtime int64 `json:"winners_selection_date"`
// The number of users which are supposed to be selected as winners of the giveaway.
WinnerCount int `json:"winner_count"`
// List of up to 100 winners of the giveaway.
Winners []User `json:"winners"`
// (Optional) The number of other chats the user had to join in order
// to be eligible for the giveaway.
AdditionalChats int `json:"additional_chat_count"`
// (Optional) The number of months the Telegram Premium subscription won from
// the giveaway will be active for.
PremiumMonthCount int `json:"premium_subscription_month_count"`
// (Optional) Number of undistributed prizes.
UnclaimedPrizes int `json:"unclaimed_prize_count"`
// (Optional) True, if only users who had joined the chats after the giveaway started
// were eligible to win.
OnlyNewMembers bool `json:"only_new_members"`
// (Optional) True, if the giveaway was canceled because the payment for it was refunded.
Refunded bool `json:"was_refunded"`
// (Optional) Description of additional giveaway prize.
PrizeDescription string `json:"prize_description"`
}
// SelectionDate returns the moment of when winners of the giveaway
// were selected in local time.
func (g *GiveawayWinners) SelectionDate() time.Time {
return time.Unix(g.SelectionUnixtime, 0)
}
// GiveawayCreated represents a service message about the creation of a scheduled giveaway.
// Currently holds no information.
type GiveawayCreated struct{}
// GiveawayCompleted represents a service message about the completion of a
// giveaway without public winners.
type GiveawayCompleted struct {
// Number of winners in the giveaway.
WinnerCount int `json:"winner_count"`
// (Optional) Number of undistributed prizes.
UnclaimedPrizes int `json:"unclaimed_prize_count"`
// (Optional) Message with the giveaway that was completed, if it wasn't deleted.
Message *Message `json:"giveaway_message"`
}

@ -1,6 +1,6 @@
module gopkg.in/telebot.v3
go 1.13
go 1.16
require (
github.com/goccy/go-yaml v1.9.5

@ -61,6 +61,46 @@ type QueryResponse struct {
// (Optional) Parameter for the start message sent to the bot when user
// presses the switch button.
SwitchPMParameter string `json:"switch_pm_parameter,omitempty"`
// (Optional) A JSON-serialized object describing a button to be shown
// above inline query results.
Button *QueryResponseButton `json:"button,omitempty"`
}
// QueryResponseButton represents a button to be shown above inline query results.
// You must use exactly one of the optional fields.
type QueryResponseButton struct {
// Label text on the button
Text string `json:"text"`
// (Optional) Description of the Web App that will be launched when the
// user presses the button. The Web App will be able to switch back to the
// inline mode using the method switchInlineQuery inside the Web App.
WebApp *WebApp `json:"web_app"`
// (Optional) Deep-linking parameter for the /start message sent to the bot
// when a user presses the button. 1-64 characters, only A-Z, a-z, 0-9, _ and - are allowed.
Start string `json:"start_parameter"`
}
// SwitchInlineQuery represents an inline button that switches the current
// user to inline mode in a chosen chat, with an optional default inline query.
type SwitchInlineQuery struct {
// (Optional) The default inline query to be inserted in the input field.
// If left empty, only the bot's username will be inserted.
Query string `json:"query"`
// (Optional) True, if private chats with users can be chosen.
AllowUserChats bool `json:"allow_user_chats"`
// (Optional) True, if private chats with bots can be chosen.
AllowBotChats bool `json:"allow_bot_chats"`
// (Optional) True, if group and supergroup chats can be chosen.
AllowGroupChats bool `json:"allow_group_chats"`
// (Optional) True, if channel chats can be chosen.
AllowChannelChats bool `json:"allow_channel_chats"`
}
// InlineResult represents a result of an inline query that was chosen
@ -93,9 +133,9 @@ type Results []Result
// MarshalJSON makes sure IQRs have proper IDs and Type variables set.
func (results Results) MarshalJSON() ([]byte, error) {
for _, result := range results {
for i, result := range results {
if result.ResultID() == "" {
result.SetResultID(fmt.Sprintf("%d", &result))
result.SetResultID(fmt.Sprintf("%d", &results[i]))
}
if err := inferIQR(result); err != nil {
return nil, err
@ -131,6 +171,8 @@ func inferIQR(result Result) error {
r.Type = "voice"
case *StickerResult:
r.Type = "sticker"
case *GameResult:
r.Type = "game"
default:
return fmt.Errorf("telebot: result %v is not supported", result)
}

@ -60,6 +60,16 @@ func (r *ResultBase) Process(b *Bot) {
}
}
// GameResult represents a game. Game is a content type
// supported by Telegram, which can be sent back to the
// user as a result for an inline query.
type GameResult struct {
ResultBase
// ShortName is a unique identifier of the game.
ShortName string `json:"game_short_name"`
}
// ArticleResult represents a link to an article or web page.
type ArticleResult struct {
ResultBase
@ -81,13 +91,13 @@ type ArticleResult struct {
Description string `json:"description,omitempty"`
// Optional. URL of the thumbnail for the result.
ThumbURL string `json:"thumb_url,omitempty"`
ThumbURL string `json:"thumbnail_url,omitempty"`
// Optional. Width of the thumbnail for the result.
ThumbWidth int `json:"thumb_width,omitempty"`
ThumbWidth int `json:"thumbnail_width,omitempty"`
// Optional. Height of the thumbnail for the result.
ThumbHeight int `json:"thumb_height,omitempty"`
ThumbHeight int `json:"thumbnail_height,omitempty"`
}
// AudioResult represents a link to an mp3 audio file.
@ -130,13 +140,13 @@ type ContactResult struct {
LastName string `json:"last_name,omitempty"`
// Optional. URL of the thumbnail for the result.
ThumbURL string `json:"thumb_url,omitempty"`
ThumbURL string `json:"thumbnail_url,omitempty"`
// Optional. Width of the thumbnail for the result.
ThumbWidth int `json:"thumb_width,omitempty"`
ThumbWidth int `json:"thumbnail_width,omitempty"`
// Optional. Height of the thumbnail for the result.
ThumbHeight int `json:"thumb_height,omitempty"`
ThumbHeight int `json:"thumbnail_height,omitempty"`
}
// DocumentResult represents a link to a file.
@ -160,13 +170,13 @@ type DocumentResult struct {
Description string `json:"description,omitempty"`
// Optional. URL of the thumbnail (jpeg only) for the file.
ThumbURL string `json:"thumb_url,omitempty"`
ThumbURL string `json:"thumbnail_url,omitempty"`
// Optional. Width of the thumbnail for the result.
ThumbWidth int `json:"thumb_width,omitempty"`
ThumbWidth int `json:"thumbnail_width,omitempty"`
// Optional. Height of the thumbnail for the result.
ThumbHeight int `json:"thumb_height,omitempty"`
ThumbHeight int `json:"thumbnail_height,omitempty"`
// If Cache != "", it'll be used instead
Cache string `json:"document_file_id,omitempty"`
@ -189,11 +199,11 @@ type GifResult struct {
Duration int `json:"gif_duration,omitempty"`
// URL of the static thumbnail for the result (jpeg or gif).
ThumbURL string `json:"thumb_url"`
ThumbURL string `json:"thumbnail_url"`
// Optional. MIME type of the thumbnail, must be one of
// “image/jpeg”, “image/gif”, or “video/mp4”.
ThumbMIME string `json:"thumb_mime_type,omitempty"`
ThumbMIME string `json:"thumbnail_mime_type,omitempty"`
// Optional. Title for the result.
Title string `json:"title,omitempty"`
@ -215,7 +225,7 @@ type LocationResult struct {
Title string `json:"title"`
// Optional. Url of the thumbnail for the result.
ThumbURL string `json:"thumb_url,omitempty"`
ThumbURL string `json:"thumbnail_url,omitempty"`
}
// Mpeg4GifResult represents a link to a video animation
@ -236,11 +246,11 @@ type Mpeg4GifResult struct {
Duration int `json:"mpeg4_duration,omitempty"`
// URL of the static thumbnail (jpeg or gif) for the result.
ThumbURL string `json:"thumb_url,omitempty"`
ThumbURL string `json:"thumbnail_url,omitempty"`
// Optional. MIME type of the thumbnail, must be one of
// “image/jpeg”, “image/gif”, or “video/mp4”.
ThumbMIME string `json:"thumb_mime_type,omitempty"`
ThumbMIME string `json:"thumbnail_mime_type,omitempty"`
// Optional. Title for the result.
Title string `json:"title,omitempty"`
@ -276,7 +286,7 @@ type PhotoResult struct {
Caption string `json:"caption,omitempty"`
// URL of the thumbnail for the photo.
ThumbURL string `json:"thumb_url"`
ThumbURL string `json:"thumbnail_url"`
// If Cache != "", it'll be used instead
Cache string `json:"photo_file_id,omitempty"`
@ -298,13 +308,13 @@ type VenueResult struct {
FoursquareID string `json:"foursquare_id,omitempty"`
// Optional. URL of the thumbnail for the result.
ThumbURL string `json:"thumb_url,omitempty"`
ThumbURL string `json:"thumbnail_url,omitempty"`
// Optional. Width of the thumbnail for the result.
ThumbWidth int `json:"thumb_width,omitempty"`
ThumbWidth int `json:"thumbnail_width,omitempty"`
// Optional. Height of the thumbnail for the result.
ThumbHeight int `json:"thumb_height,omitempty"`
ThumbHeight int `json:"thumbnail_height,omitempty"`
}
// VideoResult represents a link to a page containing an embedded
@ -319,7 +329,7 @@ type VideoResult struct {
MIME string `json:"mime_type"`
// URL of the thumbnail (jpeg only) for the video.
ThumbURL string `json:"thumb_url"`
ThumbURL string `json:"thumbnail_url"`
// Title for the result.
Title string `json:"title"`

@ -12,12 +12,12 @@ type InputTextMessageContent struct {
// Text of the message to be sent, 1-4096 characters.
Text string `json:"message_text"`
// Optional. Send Markdown or HTML, if you want Telegram apps to show
// (Optional) Send Markdown or HTML, if you want Telegram apps to show
// bold, italic, fixed-width text or inline URLs in your bot's message.
ParseMode string `json:"parse_mode,omitempty"`
// Optional. Disables link previews for links in the sent message.
DisablePreview bool `json:"disable_web_page_preview"`
// (Optional) Link preview generation options for the message.
PreviewOptions *PreviewOptions `json:"link_preview_options,omitempty"`
}
func (input *InputTextMessageContent) IsInputMessageContent() bool {

@ -19,7 +19,7 @@ func (c *Config) Unmarshal(v interface{}) error {
return c.v.Unmarshal(v)
}
// UnmarshalKey parses the specific ke in the config into the out value.
// UnmarshalKey parses the specific key in the config into the out value.
func (c *Config) UnmarshalKey(k string, v interface{}) error {
return c.v.UnmarshalKey(k, v)
}

@ -72,5 +72,5 @@ results:
id: '{{ .ID }}'
title: '{{ .Title }}'
description: '{{ .Description }}'
message_text: '{{ .Content }}'
thumb_url: '{{ .PreviewURL }}'
thumbnail_url: '{{ .PreviewURL }}'
message_text: '{{ text `article_message` }}'

@ -3,8 +3,9 @@ package layout
import (
"bytes"
"encoding/json"
"io/ioutil"
"io/fs"
"log"
"os"
"strings"
"sync"
"text/template"
@ -51,9 +52,10 @@ type (
// Result represents layout-specific result to be parsed.
Result struct {
result *template.Template
ResultBase `yaml:",inline"`
Markup string `yaml:"markup"`
result *template.Template
tele.ResultBase `yaml:",inline"`
Content ResultContent `yaml:"content"`
Markup string `yaml:"markup"`
}
// ResultBase represents layout-specific result's base to be parsed.
@ -68,7 +70,13 @@ type (
// New parses the given layout file.
func New(path string, funcs ...template.FuncMap) (*Layout, error) {
data, err := ioutil.ReadFile(path)
return NewFromFS(os.DirFS("."), path, funcs...)
}
// NewFromFS parses the layout from the given fs.FS. It allows to read layout
// from the go:embed filesystem.
func NewFromFS(fsys fs.FS, path string, funcs ...template.FuncMap) (*Layout, error) {
data, err := fs.ReadFile(fsys, path)
if err != nil {
return nil, err
}
@ -109,21 +117,21 @@ var builtinFuncs = template.FuncMap{
// Settings returns built telebot Settings required for bot initializing.
//
// settings:
// url: (custom url if needed)
// token: (not recommended)
// updates: (chan capacity)
// locales_dir: (optional)
// token_env: (token env var name, example: TOKEN)
// parse_mode: (default parse mode)
// long_poller: (long poller settings)
// webhook: (or webhook settings)
// settings:
// url: (custom url if needed)
// token: (not recommended)
// updates: (chan capacity)
// locales_dir: (optional)
// token_env: (token env var name, example: TOKEN)
// parse_mode: (default parse mode)
// long_poller: (long poller settings)
// webhook: (or webhook settings)
//
// Usage:
// lt, err := layout.New("bot.yml")
// b, err := tele.NewBot(lt.Settings())
// // That's all!
//
// lt, err := layout.New("bot.yml")
// b, err := tele.NewBot(lt.Settings())
// // That's all!
func (lt *Layout) Settings() tele.Settings {
if lt.pref == nil {
panic("telebot/layout: settings is empty")
@ -182,16 +190,20 @@ func (lt *Layout) Commands() (cmds []tele.Command) {
// used in b.SetCommands later.
//
// Example of bot.yml:
//
// commands:
// /start: '{{ text "cmdStart" }}'
// /start: '{{ text `cmdStart` }}'
//
// en.yml:
//
// cmdStart: Start the bot
//
// ru.yml:
//
// cmdStart: Запуск бота
//
// Usage:
//
// b.SetCommands(lt.CommandsLocale("en"), "en")
// b.SetCommands(lt.CommandsLocale("ru"), "ru")
func (lt *Layout) CommandsLocale(locale string, args ...interface{}) (cmds []tele.Command) {
@ -225,13 +237,14 @@ func (lt *Layout) CommandsLocale(locale string, args ...interface{}) (cmds []tel
// The given optional argument will be passed to the template engine.
//
// Example of en.yml:
// start: Hi, {{.FirstName}}!
//
// start: Hi, {{.FirstName}}!
//
// Usage:
// func onStart(c tele.Context) error {
// return c.Send(lt.Text(c, "start", c.Sender()))
// }
//
// func onStart(c tele.Context) error {
// return c.Send(lt.Text(c, "start", c.Sender()))
// }
func (lt *Layout) Text(c tele.Context, k string, args ...interface{}) string {
locale, ok := lt.Locale(c)
if !ok {
@ -265,9 +278,9 @@ func (lt *Layout) TextLocale(locale, k string, args ...interface{}) string {
// Callback returns a callback endpoint used to handle buttons.
//
// Example:
// // Handling settings button
// b.Handle(lt.Callback("settings"), onSettings)
//
// // Handling settings button
// b.Handle(lt.Callback("settings"), onSettings)
func (lt *Layout) Callback(k string) tele.CallbackEndpoint {
btn, ok := lt.buttons[k]
if !ok {
@ -279,28 +292,28 @@ func (lt *Layout) Callback(k string) tele.CallbackEndpoint {
// Button returns a button, which locale is dependent on the context.
// The given optional argument will be passed to the template engine.
//
// buttons:
// item:
// unique: item
// callback_data: {{.ID}}
// text: Item #{{.Number}}
// buttons:
// item:
// unique: item
// callback_data: {{.ID}}
// text: Item #{{.Number}}
//
// Usage:
// btns := make([]tele.Btn, len(items))
// for i, item := range items {
// btns[i] = lt.Button(c, "item", struct {
// Number int
// Item Item
// }{
// Number: i,
// Item: item,
// })
// }
//
// m := b.NewMarkup()
// m.Inline(m.Row(btns...))
// // Your generated markup is ready.
// btns := make([]tele.Btn, len(items))
// for i, item := range items {
// btns[i] = lt.Button(c, "item", struct {
// Number int
// Item Item
// }{
// Number: i,
// Item: item,
// })
// }
//
// m := b.NewMarkup()
// m.Inline(m.Row(btns...))
// // Your generated markup is ready.
func (lt *Layout) Button(c tele.Context, k string, args ...interface{}) *tele.Btn {
locale, ok := lt.Locale(c)
if !ok {
@ -352,20 +365,20 @@ func (lt *Layout) ButtonLocale(locale, k string, args ...interface{}) *tele.Btn
// Markup returns a markup, which locale is dependent on the context.
// The given optional argument will be passed to the template engine.
//
// buttons:
// settings: 'Settings'
// markups:
// menu:
// - [settings]
// buttons:
// settings: 'Settings'
// markups:
// menu:
// - [settings]
//
// Usage:
// func onStart(c tele.Context) error {
// return c.Send(
// lt.Text(c, "start"),
// lt.Markup(c, "menu"),
// )
// }
//
// func onStart(c tele.Context) error {
// return c.Send(
// lt.Text(c, "start"),
// lt.Markup(c, "menu"),
// )
// }
func (lt *Layout) Markup(c tele.Context, k string, args ...interface{}) *tele.ReplyMarkup {
locale, ok := lt.Locale(c)
if !ok {
@ -416,27 +429,27 @@ func (lt *Layout) MarkupLocale(locale, k string, args ...interface{}) *tele.Repl
// Result returns an inline result, which locale is dependent on the context.
// The given optional argument will be passed to the template engine.
//
// results:
// article:
// type: article
// id: '{{ .ID }}'
// title: '{{ .Title }}'
// description: '{{ .Description }}'
// message_text: '{{ .Content }}'
// thumb_url: '{{ .PreviewURL }}'
// results:
// article:
// type: article
// id: '{{ .ID }}'
// title: '{{ .Title }}'
// description: '{{ .Description }}'
// message_text: '{{ .Content }}'
// thumb_url: '{{ .PreviewURL }}'
//
// Usage:
// func onQuery(c tele.Context) error {
// results := make(tele.Results, len(articles))
// for i, article := range articles {
// results[i] = lt.Result(c, "article", article)
// }
// return c.Answer(&tele.QueryResponse{
// Results: results,
// CacheTime: 100,
// })
// }
//
// func onQuery(c tele.Context) error {
// results := make(tele.Results, len(articles))
// for i, article := range articles {
// results[i] = lt.Result(c, "article", article)
// }
// return c.Answer(&tele.QueryResponse{
// Results: results,
// CacheTime: 100,
// })
// }
func (lt *Layout) Result(c tele.Context, k string, args ...interface{}) tele.Result {
locale, ok := lt.Locale(c)
if !ok {
@ -466,7 +479,7 @@ func (lt *Layout) ResultLocale(locale, k string, args ...interface{}) tele.Resul
var (
data = buf.Bytes()
base ResultBase
base Result
r tele.Result
)
@ -543,6 +556,7 @@ func (lt *Layout) ResultLocale(locale, k string, args ...interface{}) tele.Resul
if base.Content != nil {
r.SetContent(base.Content)
}
if result.Markup != "" {
markup := lt.MarkupLocale(locale, result.Markup, args...)
if markup == nil {

@ -1,6 +1,7 @@
package layout
import (
"embed"
"os"
"testing"
"time"
@ -9,6 +10,9 @@ import (
tele "gopkg.in/telebot.v3"
)
//go:embed *
var fsys embed.FS
func TestLayout(t *testing.T) {
os.Setenv("TOKEN", "TEST")
@ -17,10 +21,16 @@ func TestLayout(t *testing.T) {
t.Fatal(err)
}
ltfs, err := NewFromFS(fsys, "example.yml")
if err != nil {
t.Fatal(err)
}
pref := lt.Settings()
assert.Equal(t, "TEST", pref.Token)
assert.Equal(t, "html", pref.ParseMode)
assert.Equal(t, &tele.LongPoller{}, pref.Poller)
assert.Equal(t, pref, ltfs.Settings())
assert.ElementsMatch(t, []tele.Command{{
Text: "start",
@ -109,19 +119,17 @@ func TestLayout(t *testing.T) {
},
Title: "Some title",
Description: "Some description",
Text: "The text of the article",
ThumbURL: "https://preview.picture",
Text: "This is an article.",
}, lt.ResultLocale("en", "article", struct {
ID int
Title string
Description string
Content string
PreviewURL string
}{
ID: 1853,
Title: "Some title",
Description: "Some description",
Content: "The text of the article",
PreviewURL: "https://preview.picture",
}))
}

@ -0,0 +1 @@
article_message: This is an article.

@ -53,6 +53,9 @@ type ReplyMarkup struct {
// Placeholder will be shown in the input field when the reply is active.
Placeholder string `json:"input_field_placeholder,omitempty"`
// IsPersistent allows to control when the keyboard is shown.
IsPersistent bool `json:"is_persistent,omitempty"`
}
func (r *ReplyMarkup) copy() *ReplyMarkup {
@ -79,17 +82,19 @@ func (r *ReplyMarkup) copy() *ReplyMarkup {
// 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"`
WebApp *WebApp `json:"web_app,omitempty"`
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"`
Login *Login `json:"login_url,omitempty"`
WebApp *WebApp `json:"web_app,omitempty"`
Contact bool `json:"request_contact,omitempty"`
Location bool `json:"request_location,omitempty"`
Poll PollType `json:"request_poll,omitempty"`
User *ReplyRecipient `json:"request_user,omitempty"`
Chat *ReplyRecipient `json:"request_chat,omitempty"`
}
// Row represents an array of buttons, a row.
@ -106,7 +111,6 @@ func (r *ReplyMarkup) Row(many ...Btn) Row {
//
// `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 {
@ -158,18 +162,6 @@ 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,
@ -190,6 +182,26 @@ func (r *ReplyMarkup) QueryChat(text, query string) Btn {
return Btn{Text: text, InlineQueryChat: query}
}
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) User(text string, user *ReplyRecipient) Btn {
return Btn{Text: text, User: user}
}
func (r *ReplyMarkup) Chat(text string, chat *ReplyRecipient) Btn {
return Btn{Text: text, Chat: chat}
}
func (r *ReplyMarkup) Login(text string, login *Login) Btn {
return Btn{Login: login, Text: text}
}
@ -202,14 +214,15 @@ func (r *ReplyMarkup) WebApp(text string, app *WebApp) Btn {
//
// 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"`
Contact bool `json:"request_contact,omitempty"`
Location bool `json:"request_location,omitempty"`
Poll PollType `json:"request_poll,omitempty"`
User *ReplyRecipient `json:"request_users,omitempty"`
Chat *ReplyRecipient `json:"request_chat,omitempty"`
WebApp *WebApp `json:"web_app,omitempty"`
}
// MarshalJSON implements json.Marshaler. It allows passing PollType as a
@ -222,6 +235,35 @@ func (pt PollType) MarshalJSON() ([]byte, error) {
})
}
// ReplyRecipient combines both KeyboardButtonRequestUser
// and KeyboardButtonRequestChat objects. Use inside ReplyButton
// to request the user or chat sharing with respective settings.
//
// To pass the pointers to bool use a special tele.Flag function,
// that way you will be able to reflect the three-state bool (nil, false, true).
type ReplyRecipient struct {
ID int32 `json:"request_id"`
Bot *bool `json:"user_is_bot,omitempty"` // user only, optional
Premium *bool `json:"user_is_premium,omitempty"` // user only, optional
Quantity int `json:"max_quantity,omitempty"` // user only, optional
Channel bool `json:"chat_is_channel,omitempty"` // chat only, required
Forum *bool `json:"chat_is_forum,omitempty"` // chat only, optional
WithUsername *bool `json:"chat_has_username,omitempty"` // chat only, optional
Created *bool `json:"chat_is_created,omitempty"` // chat only, optional
UserRights *Rights `json:"user_administrator_rights,omitempty"` // chat only, optional
BotRights *Rights `json:"bot_administrator_rights,omitempty"` // chat only, optional
BotMember *bool `json:"bot_is_member,omitempty"` // chat only, optional
}
// RecipientShared combines both UserShared and ChatShared objects.
type RecipientShared struct {
ID int32 `json:"request_id"`
UserID int64 `json:"user_id"`
ChatID int64 `json:"chat_id"`
}
// InlineButton represents a button displayed in the message.
type InlineButton struct {
// Unique slagish name for this kind of button,
@ -230,13 +272,14 @@ type InlineButton struct {
// 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"`
Login *Login `json:"login_url,omitempty"`
WebApp *WebApp `json:"web_app,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"`
InlineQueryChosenChat *SwitchInlineQuery `json:"switch_inline_query_chosen_chat,omitempty"`
Login *Login `json:"login_url,omitempty"`
WebApp *WebApp `json:"web_app,omitempty"`
}
// MarshalJSON implements json.Marshaler interface.
@ -279,6 +322,8 @@ func (b Btn) Reply() *ReplyButton {
Contact: b.Contact,
Location: b.Location,
Poll: b.Poll,
User: b.User,
Chat: b.Chat,
WebApp: b.WebApp,
}
}

@ -19,7 +19,7 @@ type InputMedia struct {
Type string `json:"type"`
Media string `json:"media"`
Caption string `json:"caption"`
Thumbnail string `json:"thumb,omitempty"`
Thumbnail string `json:"thumbnail,omitempty"`
ParseMode string `json:"parse_mode,omitempty"`
Entities Entities `json:"caption_entities,omitempty"`
Width int `json:"width,omitempty"`
@ -29,6 +29,7 @@ type InputMedia struct {
Performer string `json:"performer,omitempty"`
Streaming bool `json:"supports_streaming,omitempty"`
DisableTypeDetection bool `json:"disable_content_type_detection,omitempty"`
HasSpoiler bool `json:"is_spoiler,omitempty"`
}
// Inputtable is a generic type for all kinds of media you
@ -44,6 +45,24 @@ type Inputtable interface {
// Album lets you group multiple media into a single message.
type Album []Inputtable
func (a Album) SetCaption(caption string) {
if len(a) < 1 {
return
}
switch a[0].MediaType() {
case "audio":
a[0].(*Audio).Caption = caption
case "video":
a[0].(*Video).Caption = caption
case "document":
a[0].(*Document).Caption = caption
case "photo":
a[0].(*Photo).Caption = caption
case "animation":
a[0].(*Animation).Caption = caption
}
}
// Photo object represents a single photo file.
type Photo struct {
File
@ -112,7 +131,7 @@ type Audio struct {
// (Optional)
Caption string `json:"caption,omitempty"`
Thumbnail *Photo `json:"thumb,omitempty"`
Thumbnail *Photo `json:"thumbnail,omitempty"`
Title string `json:"title,omitempty"`
Performer string `json:"performer,omitempty"`
MIME string `json:"mime_type,omitempty"`
@ -144,7 +163,7 @@ type Document struct {
File
// (Optional)
Thumbnail *Photo `json:"thumb,omitempty"`
Thumbnail *Photo `json:"thumbnail,omitempty"`
Caption string `json:"caption,omitempty"`
MIME string `json:"mime_type"`
FileName string `json:"file_name,omitempty"`
@ -178,7 +197,7 @@ type Video struct {
// (Optional)
Caption string `json:"caption,omitempty"`
Thumbnail *Photo `json:"thumb,omitempty"`
Thumbnail *Photo `json:"thumbnail,omitempty"`
Streaming bool `json:"supports_streaming,omitempty"`
MIME string `json:"mime_type,omitempty"`
FileName string `json:"file_name,omitempty"`
@ -214,7 +233,7 @@ type Animation struct {
// (Optional)
Caption string `json:"caption,omitempty"`
Thumbnail *Photo `json:"thumb,omitempty"`
Thumbnail *Photo `json:"thumbnail,omitempty"`
MIME string `json:"mime_type,omitempty"`
FileName string `json:"file_name,omitempty"`
}
@ -264,7 +283,7 @@ type VideoNote struct {
Duration int `json:"duration"`
// (Optional)
Thumbnail *Photo `json:"thumb,omitempty"`
Thumbnail *Photo `json:"thumbnail,omitempty"`
Length int `json:"length,omitempty"`
}
@ -279,15 +298,18 @@ func (v *VideoNote) MediaFile() *File {
// Sticker object represents a WebP image, so-called sticker.
type Sticker struct {
File
Width int `json:"width"`
Height int `json:"height"`
Animated bool `json:"is_animated"`
Video bool `json:"is_video"`
Thumbnail *Photo `json:"thumb"`
Emoji string `json:"emoji"`
SetName string `json:"set_name"`
MaskPosition *MaskPosition `json:"mask_position"`
PremiumAnimation *File `json:"premium_animation"`
Type StickerSetType `json:"type"`
Width int `json:"width"`
Height int `json:"height"`
Animated bool `json:"is_animated"`
Video bool `json:"is_video"`
Thumbnail *Photo `json:"thumbnail"`
Emoji string `json:"emoji"`
SetName string `json:"set_name"`
PremiumAnimation *File `json:"premium_animation"`
MaskPosition *MaskPosition `json:"mask_position"`
CustomEmoji string `json:"custom_emoji_id"`
Repaint bool `json:"needs_repainting"`
}
func (s *Sticker) MediaType() string {

@ -0,0 +1,44 @@
package telebot
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestAlbumSetCaption(t *testing.T) {
tests := []struct {
name string
media Inputtable
}{
{
name: "photo",
media: &Photo{Caption: "wrong_caption"},
},
{
name: "animation",
media: &Animation{Caption: "wrong_caption"},
},
{
name: "video",
media: &Video{Caption: "wrong_caption"},
},
{
name: "audio",
media: &Audio{Caption: "wrong_caption"},
},
{
name: "document",
media: &Document{Caption: "wrong_caption"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var a Album
a = append(a, tt.media)
a = append(a, &Photo{Caption: "random_caption"})
a.SetCaption("correct_caption")
assert.Equal(t, "correct_caption", a[0].InputMedia().Caption)
assert.Equal(t, "random_caption", a[1].InputMedia().Caption)
})
}
}

@ -10,6 +10,9 @@ import (
type Message struct {
ID int `json:"message_id"`
// (Optional) Unique identifier of a message thread to which the message belongs; for supergroups only
ThreadID int `json:"message_thread_id"`
// For message sent to channels, Sender will be nil
Sender *User `json:"from"`
@ -43,6 +46,9 @@ type Message struct {
// For forwarded messages, unixtime of the original message.
OriginalUnixtime int `json:"forward_date"`
// For information about the original message for forwarded messages.
Origin *MessageOrigin `json:"forward_origin"`
// Message is a channel post that was automatically forwarded to the connected discussion group.
AutomaticForward bool `json:"is_automatic_forward"`
@ -53,12 +59,29 @@ type Message struct {
// itself is a reply.
ReplyTo *Message `json:"reply_to_message"`
// (Optional) For replies to a story, the original story
Story *Story `json:"story"`
// (Optional) Information about the message that is being replied to,
// which may come from another chat or forum topic.
ExternalReplyInfo *ExternalReplyInfo `json:"external_reply"`
// (Optional) For replies that quote part of the original message,
// the quoted part of the message.
Quote *TextQuote `json:"quote"`
// Shows through which bot the message was sent.
Via *User `json:"via_bot"`
// For replies to a story, the original story.
ReplyToStory *Story `json:"reply_to_story"`
// (Optional) Time of last edit in Unix.
LastEdit int64 `json:"edit_date"`
// (Optional) True, if the message is sent to a forum topic.
TopicMessage bool `json:"is_topic_message"`
// (Optional) Message can't be forwarded.
Protected bool `json:"has_protected_content,omitempty"`
@ -81,6 +104,10 @@ type Message struct {
// etc. that appear in the text.
Entities Entities `json:"entities,omitempty"`
// (Optional) PreviewOptions used for link preview generation for the message,
// if it is a text message and link preview options were changed.
PreviewOptions *PreviewOptions `json:"link_preview_options,omitempty"`
// Some messages containing media, may as well have a caption.
Caption string `json:"caption,omitempty"`
@ -109,7 +136,7 @@ type Message struct {
// For a video, information about it.
Video *Video `json:"video"`
// For a animation, information about it.
// For an animation, information about it.
Animation *Animation `json:"animation"`
// For a contact, contact information itself.
@ -130,6 +157,18 @@ type Message struct {
// For a dice, information about it.
Dice *Dice `json:"dice"`
// (Optional) The message is a scheduled giveaway message.
Giveaway *Giveaway `json:"giveaway"`
// (Optional) A giveaway with public winners was completed.
GiveawayWinners *GiveawayWinners `json:"giveaway_winners"`
// (Optional) Service message: a scheduled giveaway was created.
GiveawayCreated *GiveawayCreated `json:"giveaway_created"`
// (Optional) Service message: a giveaway without public winners was completed.
GiveawayCompleted *GiveawayCompleted `json:"giveaway_completed"`
// For a service message, represents a user,
// that just got added to chat, this message came from.
//
@ -223,6 +262,12 @@ type Message struct {
// Message is a service message about a successful payment.
Payment *Payment `json:"successful_payment"`
// For a service message, a user was shared with the bot.
UserShared *RecipientShared `json:"users_shared,omitempty"`
// For a service message, a chat was shared with the bot.
ChatShared *RecipientShared `json:"chat_shared,omitempty"`
// The domain name of the website on which the user has logged in.
ConnectedWebsite string `json:"connected_website,omitempty"`
@ -250,6 +295,37 @@ type Message struct {
// Inline keyboard attached to the message.
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
// Service message: user boosted the chat.
BoostAdded *BoostAdded `json:"boost_added"`
// If the sender of the message boosted the chat, the number of boosts
// added by the user.
SenderBoostCount int `json:"sender_boost_count"`
// Service message: forum topic created
TopicCreated *Topic `json:"forum_topic_created,omitempty"`
// Service message: forum topic closed
TopicClosed *struct{} `json:"forum_topic_closed,omitempty"`
// Service message: forum topic reopened
TopicReopened *Topic `json:"forum_topic_reopened,omitempty"`
// Service message: forum topic deleted
TopicEdited *Topic `json:"forum_topic_edited,omitempty"`
// Service message: general forum topic hidden
GeneralTopicHidden *struct{} `json:"general_topic_hidden,omitempty"`
// Service message: general forum topic unhidden
GeneralTopicUnhidden *struct{} `json:"general_topic_unhidden,omitempty"`
// Service message: represents spoiler information about the message.
HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"`
// Service message: the user allowed the bot added to the attachment menu to write messages
WriteAccessAllowed *WriteAccessAllowed `json:"write_access_allowed,omitempty"`
}
// MessageEntity object represents "special" parts of text messages,
@ -300,9 +376,10 @@ const (
EntityTextLink EntityType = "text_link"
EntitySpoiler EntityType = "spoiler"
EntityCustomEmoji EntityType = "custom_emoji"
EntityBlockquote EntityType = "blockquote"
)
// Entities is used to set message's text entities as a send option.
// Entities are used to set message's text entities as a send option.
type Entities []MessageEntity
// ProximityAlert sent whenever a user in the chat triggers
@ -318,6 +395,11 @@ type AutoDeleteTimer struct {
Unixtime int `json:"message_auto_delete_time"`
}
// Inaccessible shows whether the message is InaccessibleMessage object.
func (m *Message) Inaccessible() bool {
return m.Sender == nil
}
// MessageSig satisfies Editable interface (see Editable.)
func (m *Message) MessageSig() (string, int64) {
return strconv.Itoa(m.ID), m.Chat.ID
@ -365,7 +447,6 @@ func (m *Message) FromChannel() bool {
// Service messages are automatically sent messages, which
// typically occur on some global action. For instance, when
// anyone leaves the chat or chat title changes.
//
func (m *Message) IsService() bool {
fact := false
@ -386,7 +467,6 @@ func (m *Message) IsService() bool {
//
// It's safer than manually slicing Text because Telegram uses
// UTF-16 indices whereas Go string are []byte.
//
func (m *Message) EntityText(e MessageEntity) string {
text := m.Text
if text == "" {
@ -427,3 +507,215 @@ func (m *Message) Media() Media {
return nil
}
}
// MessageReaction object represents a change of a reaction on a message performed by a user.
type MessageReaction struct {
// The chat containing the message the user reacted to.
Chat *Chat `json:"chat"`
// Unique identifier of the message inside the chat.
MessageID int `json:"message_id"`
// (Optional) The user that changed the reaction,
// if the user isn't anonymous
User *User `json:"user"`
// (Optional) The chat on behalf of which the reaction was changed,
// if the user is anonymous.
ActorChat *Chat `json:"actor_chat"`
// Date of the change in Unix time.
DateUnixtime int64 `json:"date"`
// Previous list of reaction types that were set by the user.
OldReaction []Reaction `json:"old_reaction"`
// New list of reaction types that have been set by the user.
NewReaction []Reaction `json:"new_reaction"`
}
func (mu *MessageReaction) Time() time.Time {
return time.Unix(mu.DateUnixtime, 0)
}
// MessageReactionCount represents reaction changes on a message with
// anonymous reactions.
type MessageReactionCount struct {
// The chat containing the message.
Chat *Chat `json:"chat"`
// Unique message identifier inside the chat.
MessageID int `json:"message_id"`
// Date of the change in Unix time.
DateUnixtime int64 `json:"date"`
// List of reactions that are present on the message.
Reactions *ReactionCount `json:"reactions"`
}
// Time returns the moment of change in local time.
func (mc *MessageReactionCount) Time() time.Time {
return time.Unix(mc.DateUnixtime, 0)
}
// TextQuote contains information about the quoted part of a message that is
// replied to by the given message.
type TextQuote struct {
// Text of the quoted part of a message that is replied to by the given message.
Text string `json:"text"`
// (Optional) Special entities that appear in the quote.
// Currently, only bold, italic, underline, strikethrough, spoiler,
// and custom_emoji entities are kept in quotes.
Entities []MessageEntity `json:"entities"`
// Approximate quote position in the original message in UTF-16 code units
// as specified by the sender.
Position int `json:"position"`
// (Optional) True, if the quote was chosen manually by the message sender.
// Otherwise, the quote was added automatically by the server.
Manual bool `json:"is_manual"`
}
// MessageOrigin a message reference that has been sent originally by a known user.
type MessageOrigin struct {
// Type of the message origin, always “channel”.
Type string `json:"type"`
// Date the message was sent originally in Unix time.
DateUnixtime int64 `json:"date"`
// User that sent the message originally.
Sender *User `json:"sender_user,omitempty"`
// Name of the user that sent the message originally.
SenderUsername string `json:"sender_user_name,omitempty"`
// Chat that sent the message originally.
SenderChat *Chat `json:"sender_chat,omitempty"`
// Channel chat to which the message was originally sent.
Chat *Chat `json:"chat,omitempty"`
// Unique message identifier inside the chat.
MessageID int `json:"message_id,omitempty"`
// (Optional) For messages originally sent by an anonymous chat administrator,
// original message author signature.
Signature string `json:"author_signature,omitempty"`
}
// Time returns the moment of message that was sent originally in local time.
func (mo *MessageOrigin) Time() time.Time {
return time.Unix(mo.DateUnixtime, 0)
}
// ExternalReplyInfo contains information about a message that is being replied to,
// which may come from another chat or forum topic.
type ExternalReplyInfo struct {
// Origin of the message replied to by the given message.
Origin *MessageOrigin `json:"origin"`
// (Optional) Chat the original message belongs to.
// Available only if the chat is a supergroup or a channel.
Chat *Chat `json:"chat"`
// (Optional) Unique message identifier inside the original chat.
// Available only if the original chat is a supergroup or a channel.
MessageID int `json:"message_id"`
// (Optional) ReactionOptions used for link preview generation for the original message,
// if it is a text message.
PreviewOptions *PreviewOptions `json:"link_preview_options"`
// (Optional) Message is an animation, information about the animation.
Animation *Animation `json:"animation"`
// (Optional) Message is an audio file, information about the file.
Audio *Audio `json:"audio"`
// (Optional) Message is a general file, information about the file.
Document *Document `json:"document"`
// (Optional) Message is a photo, available sizes of the photo.
Photo []Photo `json:"photo"`
// (Optional) Message is a sticker, information about the sticker.
Sticker *Sticker `json:"sticker"`
// (Optional) Message is a forwarded story.
Story *Story `json:"story"`
// (Optional) Message is a video, information about the video.
Video *Video `json:"video"`
// (Optional) Message is a video note, information about the video message.
Note *VideoNote `json:"video_note"`
// (Optional) Message is a voice message, information about the file.
Voice *Voice `json:"voice"`
// (Optional) True, if the message media is covered by a spoiler animation.
HasMediaSpoiler bool `json:"has_media_spoiler"`
// (Optional) Message is a shared contact, information about the contact.
Contact *Contact `json:"contact"`
// (Optional) Message is a dice with random value.
Dice *Dice `json:"dice"`
//( Optional) Message is a game, information about the game.
Game *Game `json:"game"`
// (Optional) Message is a venue, information about the venue.
Venue *Venue `json:"venue"`
// (Optional) Message is a native poll, information about the poll.
Poll *Poll `json:"poll"`
// (Optional) Message is a shared location, information about the location.
Location *Location `json:"location"`
// (Optional) Message is an invoice for a payment, information about the invoice.
Invoice *Invoice `json:"invoice"`
// (Optional) Message is a scheduled giveaway, information about the giveaway.
Giveaway *Giveaway `json:"giveaway"`
// (Optional) A giveaway with public winners was completed.
GiveawayWinners *GiveawayWinners `json:"giveaway_winners"`
}
// ReplyParams describes reply parameters for the message that is being sent.
type ReplyParams struct {
// Identifier of the message that will be replied to in the current chat,
// or in the chat chat_id if it is specified.
MessageID int `json:"message_id"`
// (Optional) If the message to be replied to is from a different chat,
// unique identifier for the chat or username of the channel.
ChatID int64 `json:"chat_id"`
// Optional. Pass True if the message should be sent even if the specified message
// to be replied to is not found; can be used only for replies in the
// same chat and forum topic.
AllowWithoutReply bool `json:"allow_sending_without_reply"`
// (Optional) Quoted part of the message to be replied to; 0-1024 characters after
// entities parsing. The quote must be an exact substring of the message to be replied to,
// including bold, italic, underline, strikethrough, spoiler, and custom_emoji entities.
// The message will fail to send if the quote isn't found in the original message.
Quote string `json:"quote"`
// (Optional) Mode for parsing entities in the quote.
QuoteParseMode ParseMode `json:"quote_parse_mode"`
// (Optional) A JSON-serialized list of special entities that appear in the quote.
// It can be specified instead of quote_parse_mode.
QuoteEntities []MessageEntity `json:"quote_entities"`
// (Optional) Position of the quote in the original message in UTF-16 code units.
QuotePosition int `json:"quote_position"`
}

@ -4,6 +4,15 @@ package telebot
// which get called before the endpoint group or specific handler.
type MiddlewareFunc func(HandlerFunc) HandlerFunc
func appendMiddleware(a, b []MiddlewareFunc) []MiddlewareFunc {
if len(a) == 0 {
return b
}
m := make([]MiddlewareFunc, 0, len(a)+len(b))
return append(m, append(a, b...)...)
}
func applyMiddleware(h HandlerFunc, m ...MiddlewareFunc) HandlerFunc {
for i := len(m) - 1; i >= 0; i-- {
h = m[i](h)
@ -25,5 +34,5 @@ func (g *Group) Use(middleware ...MiddlewareFunc) {
// Handle adds endpoint handler to the bot, combining group's middleware
// with the optional given middleware.
func (g *Group) Handle(endpoint interface{}, h HandlerFunc, m ...MiddlewareFunc) {
g.b.Handle(endpoint, h, append(g.middleware, m...)...)
g.b.Handle(endpoint, h, appendMiddleware(g.middleware, m)...)
}

@ -32,26 +32,28 @@ func IgnoreVia() tele.MiddlewareFunc {
}
}
type RecoverFunc = func(error, tele.Context)
// Recover returns a middleware that recovers a panic happened in
// the handler.
func Recover(onError ...func(error)) tele.MiddlewareFunc {
func Recover(onError ...RecoverFunc) tele.MiddlewareFunc {
return func(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
var f func(error)
var f RecoverFunc
if len(onError) > 0 {
f = onError[0]
} else {
f = func(err error) {
c.Bot().OnError(err, nil)
f = func(err error, c tele.Context) {
c.Bot().OnError(err, c)
}
}
defer func() {
if r := recover(); r != nil {
if err, ok := r.(error); ok {
f(err)
f(err, c)
} else if s, ok := r.(string); ok {
f(errors.New(s))
f(errors.New(s), c)
}
}
}()

@ -12,7 +12,7 @@ import (
var b, _ = tele.NewBot(tele.Settings{Offline: true})
func TestRecover(t *testing.T) {
onError := func(err error) {
onError := func(err error, c tele.Context) {
require.Error(t, err, "recover test")
}

@ -11,7 +11,6 @@ import (
// flags instead.
//
// Supported options are defined as iota-constants.
//
type Option int
const (
@ -54,7 +53,6 @@ func Placeholder(text string) *SendOptions {
// Despite its power, SendOptions is rather inconvenient to use all
// the way through bot logic, so you might want to consider storing
// and re-using it somewhere or be using Option flags instead.
//
type SendOptions struct {
// If the message is a reply, original message.
ReplyTo *Message
@ -77,8 +75,17 @@ type SendOptions struct {
// AllowWithoutReply allows sending messages not a as reply if the replied-to message has already been deleted.
AllowWithoutReply bool
// Protected protects the contents of the sent message from forwarding and saving
// Protected protects the contents of sent message from forwarding and saving.
Protected bool
// ThreadID supports sending messages to a thread.
ThreadID int
// HasSpoiler marks the message as containing a spoiler.
HasSpoiler bool
// ReplyParams Describes the message to reply to
ReplyParams *ReplyParams
}
func (og *SendOptions) copy() *SendOptions {
@ -100,6 +107,8 @@ func extractOptions(how []interface{}) *SendOptions {
if opt != nil {
opts.ReplyMarkup = opt.copy()
}
case *ReplyParams:
opts.ReplyParams = opt
case Option:
switch opt {
case NoPreview:
@ -189,6 +198,14 @@ func (b *Bot) embedSendOptions(params map[string]string, opt *SendOptions) {
if opt.Protected {
params["protect_content"] = "true"
}
if opt.ThreadID != 0 {
params["message_thread_id"] = strconv.Itoa(opt.ThreadID)
}
if opt.HasSpoiler {
params["spoiler"] = "true"
}
}
func processButtons(keys [][]InlineButton) {
@ -211,3 +228,45 @@ func processButtons(keys [][]InlineButton) {
}
}
}
// PreviewOptions describes the options used for link preview generation.
type PreviewOptions struct {
// (Optional) True, if the link preview is disabled.
Disabled bool `json:"is_disabled"`
// (Optional) URL to use for the link preview. If empty, then the first URL
// found in the message text will be used.
URL string `json:"url"`
// (Optional) True, if the media in the link preview is supposed to be shrunk;
// ignored if the URL isn't explicitly specified or media size change.
// isn't supported for the preview.
SmallMedia bool `json:"prefer_small_media"`
// (Optional) True, if the media in the link preview is supposed to be enlarged;
// ignored if the URL isn't explicitly specified or media size change.
// isn't supported for the preview.
LargeMedia bool `json:"prefer_large_media"`
// (Optional) True, if the link preview must be shown above the message text;
// otherwise, the link preview will be shown below the message text.
AboveText bool `json:"show_above_text"`
}
func embedMessages(params map[string]string, msgs []Editable) {
ids := make([]string, 0, len(msgs))
_, chatID := msgs[0].MessageSig()
for _, msg := range msgs {
msgID, _ := msg.MessageSig()
ids = append(ids, msgID)
}
data, err := json.Marshal(ids)
if err != nil {
return
}
params["message_ids"] = string(data)
params["chat_id"] = strconv.FormatInt(chatID, 10)
}

@ -49,6 +49,7 @@ type PollOption struct {
type PollAnswer struct {
PollID string `json:"poll_id"`
Sender *User `json:"user"`
Chat *Chat `json:"voter_chat"`
Options []int `json:"option_ids"`
}

@ -2,12 +2,32 @@ package telebot
import "time"
var AllowedUpdates = []string{
"message",
"edited_message",
"channel_post",
"edited_channel_post",
"message_reaction",
"message_reaction_count",
"inline_query",
"chosen_inline_result",
"callback_query",
"shipping_query",
"pre_checkout_query",
"poll",
"poll_answer",
"my_chat_member",
"chat_member",
"chat_join_request",
"chat_boost",
"removed_chat_boost",
}
// Poller is a provider of Updates.
//
// All pollers must implement Poll(), which accepts bot
// pointer and subscription channel and start polling
// synchronously straight away.
//
type Poller interface {
// Poll is supposed to take the bot object
// subscription channel and start polling
@ -70,7 +90,6 @@ func (p *LongPoller) Poll(b *Bot, dest chan Update, stop chan struct{}) {
// handling, banning or whatever.
//
// For heavy middleware, use increased capacity.
//
type MiddlewarePoller struct {
Capacity int // Default: 1
Poller Poller

@ -0,0 +1,67 @@
package telebot
import (
"encoding/json"
)
// Reaction describes the type of reaction.
// Describes an instance of ReactionTypeCustomEmoji and ReactionTypeEmoji.
type Reaction struct {
// Type of the reaction, always “emoji”
Type string `json:"type"`
// Reaction emoji.
Emoji string `json:"emoji,omitempty"`
// Custom emoji identifier.
CustomEmoji string `json:"custom_emoji_id,omitempty"`
}
// ReactionCount represents a reaction added to a message along
// with the number of times it was added.
type ReactionCount struct {
// Type of the reaction.
Type Reaction `json:"type"`
// Number of times the reaction was added.
Count int `json:"total_count"`
}
// ReactionOptions represents an object of reaction options.
type ReactionOptions struct {
// List of reaction types to set on the message.
Reactions []Reaction `json:"reaction"`
// Pass True to set the reaction with a big animation.
Big bool `json:"is_big"`
}
// React changes the chosen reactions on a message. Service messages can't be
// reacted to. Automatically forwarded messages from a channel to its discussion group have
// the same available reactions as messages in the channel.
func (b *Bot) React(to Recipient, msg Editable, opts ...ReactionOptions) error {
if to == nil {
return ErrBadRecipient
}
msgID, _ := msg.MessageSig()
params := map[string]string{
"chat_id": to.Recipient(),
"message_id": msgID,
}
if len(opts) > 0 {
opt := opts[0]
if len(opt.Reactions) > 0 {
data, _ := json.Marshal(opt.Reactions)
params["reaction"] = string(data)
}
if opt.Big {
params["is_big"] = "true"
}
}
_, err := b.Raw("setMessageReaction", params)
return err
}

@ -0,0 +1,87 @@
package react
import (
tele "gopkg.in/telebot.v3"
)
type Reaction = tele.Reaction
func React(r ...Reaction) tele.ReactionOptions {
return tele.ReactionOptions{Reactions: r}
}
// Currently available emojis.
var (
ThumbUp = Reaction{Emoji: "👍"}
ThumbDown = Reaction{Emoji: "👎"}
Heart = Reaction{Emoji: "❤"}
Fire = Reaction{Emoji: "🔥"}
HeartEyes = Reaction{Emoji: "😍"}
ClappingHands = Reaction{Emoji: "👏"}
GrinningFace = Reaction{Emoji: "😁"}
ThinkingFace = Reaction{Emoji: "🤔"}
ExplodingHead = Reaction{Emoji: "🤯"}
ScreamingFace = Reaction{Emoji: "😱"}
SwearingFace = Reaction{Emoji: "🤬"}
CryingFace = Reaction{Emoji: "😢"}
PartyPopper = Reaction{Emoji: "🎉"}
StarStruck = Reaction{Emoji: "🤩"}
VomitingFace = Reaction{Emoji: "🤮"}
PileOfPoo = Reaction{Emoji: "💩"}
PrayingHands = Reaction{Emoji: "🙏"}
OkHand = Reaction{Emoji: "👌"}
DoveOfPeace = Reaction{Emoji: "🕊"}
ClownFace = Reaction{Emoji: "🤡"}
YawningFace = Reaction{Emoji: "🥱"}
WoozyFace = Reaction{Emoji: "🥴"}
Whale = Reaction{Emoji: "🐳"}
HeartOnFire = Reaction{Emoji: "❤‍🔥"}
MoonFace = Reaction{Emoji: "🌚"}
HotDog = Reaction{Emoji: "🌭"}
HundredPoints = Reaction{Emoji: "💯"}
RollingOnTheFloorLaughing = Reaction{Emoji: "🤣"}
Lightning = Reaction{Emoji: "⚡"}
Banana = Reaction{Emoji: "🍌"}
Trophy = Reaction{Emoji: "🏆"}
BrokenHeart = Reaction{Emoji: "💔"}
FaceWithRaisedEyebrow = Reaction{Emoji: "🤨"}
NeutralFace = Reaction{Emoji: "😐"}
Strawberry = Reaction{Emoji: "🍓"}
Champagne = Reaction{Emoji: "🍾"}
KissMark = Reaction{Emoji: "💋"}
MiddleFinger = Reaction{Emoji: "🖕"}
EvilFace = Reaction{Emoji: "😈"}
SleepingFace = Reaction{Emoji: "😴"}
LoudlyCryingFace = Reaction{Emoji: "😭"}
NerdFace = Reaction{Emoji: "🤓"}
Ghost = Reaction{Emoji: "👻"}
Engineer = Reaction{Emoji: "👨‍💻"}
Eyes = Reaction{Emoji: "👀"}
JackOLantern = Reaction{Emoji: "🎃"}
NoMonkey = Reaction{Emoji: "🙈"}
SmilingFaceWithHalo = Reaction{Emoji: "😇"}
FearfulFace = Reaction{Emoji: "😨"}
Handshake = Reaction{Emoji: "🤝"}
WritingHand = Reaction{Emoji: "✍"}
HuggingFace = Reaction{Emoji: "🤗"}
Brain = Reaction{Emoji: "🫡"}
SantaClaus = Reaction{Emoji: "🎅"}
ChristmasTree = Reaction{Emoji: "🎄"}
Snowman = Reaction{Emoji: "☃"}
NailPolish = Reaction{Emoji: "💅"}
ZanyFace = Reaction{Emoji: "🤪"}
Moai = Reaction{Emoji: "🗿"}
Cool = Reaction{Emoji: "🆒"}
HeartWithArrow = Reaction{Emoji: "💘"}
HearMonkey = Reaction{Emoji: "🙉"}
Unicorn = Reaction{Emoji: "🦄"}
FaceBlowingKiss = Reaction{Emoji: "😘"}
Pill = Reaction{Emoji: "💊"}
SpeaklessMonkey = Reaction{Emoji: "🙊"}
Sunglasses = Reaction{Emoji: "😎"}
AlienMonster = Reaction{Emoji: "👾"}
ManShrugging = Reaction{Emoji: "🤷‍♂️"}
PersonShrugging = Reaction{Emoji: "🤷"}
WomanShrugging = Reaction{Emoji: "🤷‍♀️"}
PoutingFace = Reaction{Emoji: "😡"}
)

@ -18,7 +18,6 @@ type Recipient interface {
// This is pretty cool, since it lets bots implement
// custom Sendables for complex kind of media or
// chat objects spanning across multiple messages.
//
type Sendable interface {
Send(*Bot, Recipient, *SendOptions) (*Message, error)
}
@ -116,6 +115,7 @@ func (d *Document) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error
func (s *Sticker) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
"emoji": s.Emoji,
}
b.embedSendOptions(params, opt)
@ -402,7 +402,7 @@ func (g *Game) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
func thumbnailToFilemap(thumb *Photo) map[string]File {
if thumb != nil {
return map[string]File{"thumb": thumb.File}
return map[string]File{"thumbnail": thumb.File}
}
return nil
}

@ -0,0 +1,306 @@
package telebot
import (
"encoding/json"
"errors"
"fmt"
"strconv"
)
type (
StickerSetType = string
StickerSetFormat = string
MaskFeature = string
)
const (
StickerRegular StickerSetType = "regular"
StickerMask StickerSetType = "mask"
StickerCustomEmoji StickerSetType = "custom_emoji"
)
const (
StickerStatic StickerSetFormat = "static"
StickerAnimated StickerSetFormat = "animated"
StickerVideo StickerSetFormat = "video"
)
const (
MaskForehead MaskFeature = "forehead"
MaskEyes MaskFeature = "eyes"
MaskMouth MaskFeature = "mouth"
MaskChin MaskFeature = "chin"
)
// StickerSet represents a sticker set.
type StickerSet struct {
Type StickerSetType `json:"sticker_type"`
Format StickerSetFormat `json:"sticker_format"`
Name string `json:"name"`
Title string `json:"title"`
Animated bool `json:"is_animated"`
Video bool `json:"is_video"`
Stickers []Sticker `json:"stickers"`
Thumbnail *Photo `json:"thumbnail"`
Emojis string `json:"emojis"`
ContainsMasks bool `json:"contains_masks"` // FIXME: can be removed
MaskPosition *MaskPosition `json:"mask_position"`
Repaint bool `json:"needs_repainting"`
// Input is a field used in createNewStickerSet method to specify a list
// of pre-defined stickers of type InputSticker to add to the set.
Input []InputSticker
}
type InputSticker struct {
File
Sticker string `json:"sticker"`
MaskPosition *MaskPosition `json:"mask_position"`
Emojis []string `json:"emoji_list"`
Keywords []string `json:"keywords"`
}
// MaskPosition describes the position on faces where
// a mask should be placed by default.
type MaskPosition struct {
Feature MaskFeature `json:"point"`
XShift float32 `json:"x_shift"`
YShift float32 `json:"y_shift"`
Scale float32 `json:"scale"`
}
// UploadSticker uploads a sticker file for later use.
func (b *Bot) UploadSticker(to Recipient, format StickerSetFormat, f File) (*File, error) {
params := map[string]string{
"user_id": to.Recipient(),
"sticker_format": format,
}
data, err := b.sendFiles("uploadStickerFile", map[string]File{"0": f}, params)
if err != nil {
return nil, err
}
var resp struct {
Result File
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return &resp.Result, nil
}
// StickerSet returns a sticker set on success.
func (b *Bot) StickerSet(name string) (*StickerSet, error) {
data, err := b.Raw("getStickerSet", map[string]string{"name": name})
if err != nil {
return nil, err
}
var resp struct {
Result *StickerSet
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
}
// CreateStickerSet creates a new sticker set.
func (b *Bot) CreateStickerSet(of Recipient, set *StickerSet) error {
files := make(map[string]File)
for i, s := range set.Input {
repr := s.File.process(strconv.Itoa(i), files)
if repr == "" {
return fmt.Errorf("telebot: sticker #%d does not exist", i+1)
}
set.Input[i].Sticker = repr
}
data, _ := json.Marshal(set.Input)
params := map[string]string{
"user_id": of.Recipient(),
"name": set.Name,
"title": set.Title,
"sticker_format": set.Format,
"stickers": string(data),
}
if set.Type != "" {
params["sticker_type"] = set.Type
}
if set.Repaint {
params["needs_repainting"] = "true"
}
_, err := b.sendFiles("createNewStickerSet", files, params)
return err
}
// AddStickerToSet adds a new sticker to the existing sticker set.
func (b *Bot) AddStickerToSet(of Recipient, name string, sticker InputSticker) error {
files := make(map[string]File)
repr := sticker.File.process("0", files)
if repr == "" {
return errors.New("telebot: sticker does not exist")
}
sticker.Sticker = repr
data, _ := json.Marshal(sticker)
params := map[string]string{
"user_id": of.Recipient(),
"name": name,
"sticker": string(data),
}
_, err := b.sendFiles("addStickerToSet", files, params)
return err
}
// SetStickerPosition moves a sticker in set to a specific position.
func (b *Bot) SetStickerPosition(sticker string, position int) error {
params := map[string]string{
"sticker": sticker,
"position": strconv.Itoa(position),
}
_, err := b.Raw("setStickerPositionInSet", params)
return err
}
// DeleteSticker deletes a sticker from a set created by the bot.
func (b *Bot) DeleteSticker(sticker string) error {
_, err := b.Raw("deleteStickerFromSet", map[string]string{"sticker": sticker})
return err
}
// SetStickerSetThumb sets a thumbnail of the sticker set.
// Animated thumbnails can be set for animated sticker sets only.
//
// Thumbnail must be a PNG image, up to 128 kilobytes in size
// and have width and height exactly 100px, or a TGS animation
// up to 32 kilobytes in size.
//
// Animated sticker set thumbnail can't be uploaded via HTTP URL.
func (b *Bot) SetStickerSetThumb(of Recipient, set *StickerSet) error {
if set.Thumbnail == nil {
return errors.New("telebot: thumbnail is required")
}
files := make(map[string]File)
repr := set.Thumbnail.File.process("thumb", files)
if repr == "" {
return errors.New("telebot: thumbnail does not exist")
}
params := map[string]string{
"user_id": of.Recipient(),
"name": set.Name,
"thumbnail": repr,
}
_, err := b.sendFiles("setStickerSetThumbnail", files, params)
return err
}
// SetStickerSetTitle sets the title of a created sticker set.
func (b *Bot) SetStickerSetTitle(s StickerSet) error {
params := map[string]string{
"name": s.Name,
"title": s.Title,
}
_, err := b.Raw("setStickerSetTitle", params)
return err
}
// DeleteStickerSet deletes a sticker set that was created by the bot.
func (b *Bot) DeleteStickerSet(name string) error {
params := map[string]string{"name": name}
_, err := b.Raw("deleteStickerSet", params)
return err
}
// SetStickerEmojis changes the list of emoji assigned to a regular or custom emoji sticker.
func (b *Bot) SetStickerEmojis(sticker string, emojis []string) error {
data, err := json.Marshal(emojis)
if err != nil {
return err
}
params := map[string]string{
"sticker": sticker,
"emoji_list": string(data),
}
_, err = b.Raw("setStickerEmojiList", params)
return err
}
// SetStickerKeywords changes search keywords assigned to a regular or custom emoji sticker.
func (b *Bot) SetStickerKeywords(sticker string, keywords []string) error {
mk, err := json.Marshal(keywords)
if err != nil {
return err
}
params := map[string]string{
"sticker": sticker,
"keywords": string(mk),
}
_, err = b.Raw("setStickerKeywords", params)
return err
}
// SetStickerMaskPosition changes the mask position of a mask sticker.
func (b *Bot) SetStickerMaskPosition(sticker string, mask MaskPosition) error {
data, err := json.Marshal(mask)
if err != nil {
return err
}
params := map[string]string{
"sticker": sticker,
"mask_position": string(data),
}
_, err = b.Raw("setStickerMaskPosition", params)
return err
}
// CustomEmojiStickers returns the information about custom emoji stickers by their ids.
func (b *Bot) CustomEmojiStickers(ids []string) ([]Sticker, error) {
data, _ := json.Marshal(ids)
params := map[string]string{
"custom_emoji_ids": string(data),
}
data, err := b.Raw("getCustomEmojiStickers", params)
if err != nil {
return nil, err
}
var resp struct {
Result []Sticker
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
}
// SetCustomEmojiStickerSetThumb sets the thumbnail of a custom emoji sticker set.
func (b *Bot) SetCustomEmojiStickerSetThumb(name, id string) error {
params := map[string]string{
"name": name,
"custom_emoji_id": id,
}
_, err := b.Raw("setCustomEmojiStickerSetThumbnail", params)
return err
}

@ -0,0 +1,61 @@
package telebot
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestStickerSet(t *testing.T) {
if b == nil {
t.Skip("Cached bot instance is bad (probably wrong or empty TELEBOT_SECRET)")
}
if userID == 0 {
t.Skip("USER_ID is required for StickerSet methods test")
}
input := []InputSticker{
{
File: FromURL("https://placehold.co/512/000000/FFFFFF/png"),
Emojis: []string{"🤖"},
Keywords: []string{"telebot", "robot", "bot"},
},
{
File: FromURL("https://placehold.co/512/000000/999999/png"),
Emojis: []string{"🤖"},
Keywords: []string{"telebot", "robot", "bot"},
},
}
original := &StickerSet{
Name: fmt.Sprintf("telebot_%d_by_%s", time.Now().Unix(), b.Me.Username),
Type: StickerRegular,
Format: StickerStatic,
Title: "Telebot Stickers",
Input: input[:1],
}
// 1
err := b.CreateStickerSet(user, original)
require.NoError(t, err)
// 2
err = b.AddStickerToSet(user, original.Name, input[1])
require.NoError(t, err)
original.Thumbnail = &Photo{File: thumb}
err = b.SetStickerSetThumb(user, original)
require.NoError(t, err)
set, err := b.StickerSet(original.Name)
require.NoError(t, err)
require.Equal(t, original.Name, set.Name)
require.Equal(t, len(input), len(set.Stickers))
_, err = b.Send(user, &set.Stickers[0])
require.NoError(t, err)
_, err = b.Send(user, &set.Stickers[1])
require.NoError(t, err)
}

@ -1,212 +0,0 @@
package telebot
import (
"encoding/json"
"strconv"
)
type StickerSetType = string
const (
StickerRegular = "regular"
StickerMask = "mask"
StickerCustomEmoji = "custom_emoji"
)
// StickerSet represents a sticker set.
type StickerSet struct {
Type StickerSetType `json:"sticker_type"`
Name string `json:"name"`
Title string `json:"title"`
Animated bool `json:"is_animated"`
Video bool `json:"is_video"`
Stickers []Sticker `json:"stickers"`
Thumbnail *Photo `json:"thumb"`
PNG *File `json:"png_sticker"`
TGS *File `json:"tgs_sticker"`
WebM *File `json:"webm_sticker"`
Emojis string `json:"emojis"`
ContainsMasks bool `json:"contains_masks"` // FIXME: can be removed
MaskPosition *MaskPosition `json:"mask_position"`
}
// MaskPosition describes the position on faces where
// a mask should be placed by default.
type MaskPosition struct {
Feature MaskFeature `json:"point"`
XShift float32 `json:"x_shift"`
YShift float32 `json:"y_shift"`
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{
"png_sticker": *png,
}
params := map[string]string{
"user_id": to.Recipient(),
}
data, err := b.sendFiles("uploadStickerFile", files, params)
if err != nil {
return nil, err
}
var resp struct {
Result File
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return &resp.Result, nil
}
// StickerSet returns a sticker set on success.
func (b *Bot) StickerSet(name string) (*StickerSet, error) {
data, err := b.Raw("getStickerSet", map[string]string{"name": name})
if err != nil {
return nil, err
}
var resp struct {
Result *StickerSet
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
}
// CreateStickerSet creates a new sticker set.
func (b *Bot) CreateStickerSet(to Recipient, s StickerSet) error {
files := make(map[string]File)
if s.PNG != nil {
files["png_sticker"] = *s.PNG
}
if s.TGS != nil {
files["tgs_sticker"] = *s.TGS
}
if s.WebM != nil {
files["webm_sticker"] = *s.WebM
}
params := map[string]string{
"user_id": to.Recipient(),
"sticker_type": s.Type,
"name": s.Name,
"title": s.Title,
"emojis": s.Emojis,
"contains_masks": strconv.FormatBool(s.ContainsMasks),
}
if s.MaskPosition != nil {
data, _ := json.Marshal(&s.MaskPosition)
params["mask_position"] = string(data)
}
_, err := b.sendFiles("createNewStickerSet", files, params)
return err
}
// AddSticker adds a new sticker to the existing sticker set.
func (b *Bot) AddSticker(to Recipient, s StickerSet) error {
files := make(map[string]File)
if s.PNG != nil {
files["png_sticker"] = *s.PNG
} else if s.TGS != nil {
files["tgs_sticker"] = *s.TGS
} else if s.WebM != nil {
files["webm_sticker"] = *s.WebM
}
params := map[string]string{
"user_id": to.Recipient(),
"name": s.Name,
"emojis": s.Emojis,
}
if s.MaskPosition != nil {
data, _ := json.Marshal(&s.MaskPosition)
params["mask_position"] = string(data)
}
_, err := b.sendFiles("addStickerToSet", files, params)
return err
}
// SetStickerPosition moves a sticker in set to a specific position.
func (b *Bot) SetStickerPosition(sticker string, position int) error {
params := map[string]string{
"sticker": sticker,
"position": strconv.Itoa(position),
}
_, err := b.Raw("setStickerPositionInSet", params)
return err
}
// DeleteSticker deletes a sticker from a set created by the bot.
func (b *Bot) DeleteSticker(sticker string) error {
_, err := b.Raw("deleteStickerFromSet", map[string]string{"sticker": sticker})
return err
}
// SetStickerSetThumb sets a thumbnail of the sticker set.
// Animated thumbnails can be set for animated sticker sets only.
//
// Thumbnail must be a PNG image, up to 128 kilobytes in size
// and have width and height exactly 100px, or a TGS animation
// up to 32 kilobytes in size.
//
// Animated sticker set thumbnail can't be uploaded via HTTP URL.
//
func (b *Bot) SetStickerSetThumb(to Recipient, s StickerSet) error {
files := make(map[string]File)
if s.PNG != nil {
files["thumb"] = *s.PNG
} else if s.TGS != nil {
files["thumb"] = *s.TGS
}
params := map[string]string{
"name": s.Name,
"user_id": to.Recipient(),
}
_, err := b.sendFiles("setStickerSetThumb", files, params)
return err
}
// CustomEmojiStickers returns the information about custom emoji stickers by their ids.
func (b *Bot) CustomEmojiStickers(ids []string) ([]Sticker, error) {
data, _ := json.Marshal(ids)
params := map[string]string{
"custom_emoji_ids": string(data),
}
data, err := b.Raw("getCustomEmojiStickers", params)
if err != nil {
return nil, err
}
var resp struct {
Result []Sticker
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
}

@ -2,29 +2,28 @@
//
// Example:
//
// package main
// package main
//
// import (
// "time"
// tele "gopkg.in/telebot.v3"
// )
// import (
// "time"
// tele "gopkg.in/telebot.v3"
// )
//
// func main() {
// b, err := tele.NewBot(tele.Settings{
// Token: "...",
// Poller: &tele.LongPoller{Timeout: 10 * time.Second},
// })
// if err != nil {
// return
// }
//
// b.Handle("/start", func(c tele.Context) error {
// return c.Send("Hello world!")
// })
//
// b.Start()
// func main() {
// b, err := tele.NewBot(tele.Settings{
// Token: "...",
// Poller: &tele.LongPoller{Timeout: 10 * time.Second},
// })
// if err != nil {
// return
// }
//
// b.Handle("/start", func(c tele.Context) error {
// return c.Send("Hello world!")
// })
//
// b.Start()
// }
package telebot
import "errors"
@ -43,35 +42,43 @@ const DefaultApiURL = "https://api.telegram.org"
//
// For convenience, all Telebot-provided endpoints start with
// an "alert" character \a.
//
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"
OnChannelPost = "\achannel_post"
OnEditedChannelPost = "\aedited_channel_post"
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"
OnTopicCreated = "\atopic_created"
OnTopicReopened = "\atopic_reopened"
OnTopicClosed = "\atopic_closed"
OnTopicEdited = "\atopic_edited"
OnGeneralTopicHidden = "\ageneral_topic_hidden"
OnGeneralTopicUnhidden = "\ageneral_topic_unhidden"
OnWriteAccessAllowed = "\awrite_access_allowed"
OnAddedToGroup = "\aadded_to_group"
OnUserJoined = "\auser_joined"
OnUserLeft = "\auser_left"
OnUserShared = "\auser_shared"
OnChatShared = "\achat_shared"
OnNewGroupTitle = "\anew_chat_title"
OnNewGroupPhoto = "\anew_chat_photo"
OnGroupPhotoDeleted = "\achat_photo_deleted"
@ -102,6 +109,9 @@ const (
OnVideoChatEnded = "\avideo_chat_ended"
OnVideoChatParticipants = "\avideo_chat_participants_invited"
OnVideoChatScheduled = "\avideo_chat_scheduled"
OnBoost = "\aboost_updated"
OnBoostRemoved = "\aboost_removed"
)
// ChatAction is a client-side status indicating bot activity.
@ -131,6 +141,13 @@ const (
ModeHTML ParseMode = "HTML"
)
// M is a shortcut for map[string]interface{}. Use it for passing
// arguments to the layout functions.
// M is a shortcut for map[string]interface{}.
// Useful for passing arguments to the layout functions.
type M = map[string]interface{}
// Flag returns a pointer to the given bool.
// Useful for passing the three-state flags to a Bot API.
// For example, see ReplyRecipient type.
func Flag(b bool) *bool {
return &b
}

@ -0,0 +1,184 @@
package telebot
import (
"encoding/json"
"strconv"
)
type Topic struct {
Name string `json:"name"`
IconColor int `json:"icon_color"`
IconCustomEmoji string `json:"icon_custom_emoji_id"`
ThreadID int `json:"message_thread_id"`
}
// CreateTopic creates a topic in a forum supergroup chat.
func (b *Bot) CreateTopic(chat *Chat, topic *Topic) (*Topic, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
"name": topic.Name,
}
if topic.IconColor != 0 {
params["icon_color"] = strconv.Itoa(topic.IconColor)
}
if topic.IconCustomEmoji != "" {
params["icon_custom_emoji_id"] = topic.IconCustomEmoji
}
data, err := b.Raw("createForumTopic", params)
if err != nil {
return nil, err
}
var resp struct {
Result *Topic
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, err
}
// EditTopic edits name and icon of a topic in a forum supergroup chat.
func (b *Bot) EditTopic(chat *Chat, topic *Topic) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
"message_thread_id": topic.ThreadID,
}
if topic.Name != "" {
params["name"] = topic.Name
}
if topic.IconCustomEmoji != "" {
params["icon_custom_emoji_id"] = topic.IconCustomEmoji
}
_, err := b.Raw("editForumTopic", params)
return err
}
// CloseTopic closes an open topic in a forum supergroup chat.
func (b *Bot) CloseTopic(chat *Chat, topic *Topic) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
"message_thread_id": topic.ThreadID,
}
_, err := b.Raw("closeForumTopic", params)
return err
}
// ReopenTopic reopens a closed topic in a forum supergroup chat.
func (b *Bot) ReopenTopic(chat *Chat, topic *Topic) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
"message_thread_id": topic.ThreadID,
}
_, err := b.Raw("reopenForumTopic", params)
return err
}
// DeleteTopic deletes a forum topic along with all its messages in a forum supergroup chat.
func (b *Bot) DeleteTopic(chat *Chat, topic *Topic) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
"message_thread_id": topic.ThreadID,
}
_, err := b.Raw("deleteForumTopic", params)
return err
}
// UnpinAllTopicMessages clears the list of pinned messages in a forum topic. The bot must be an administrator in the chat for this to work and must have the can_pin_messages administrator right in the supergroup.
func (b *Bot) UnpinAllTopicMessages(chat *Chat, topic *Topic) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
"message_thread_id": topic.ThreadID,
}
_, err := b.Raw("unpinAllForumTopicMessages", params)
return err
}
// TopicIconStickers gets custom emoji stickers, which can be used as a forum topic icon by any user.
func (b *Bot) TopicIconStickers() ([]Sticker, error) {
params := map[string]string{}
data, err := b.Raw("getForumTopicIconStickers", params)
if err != nil {
return nil, err
}
var resp struct {
Result []Sticker
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
}
// EditGeneralTopic edits name of the 'General' topic in a forum supergroup chat.
func (b *Bot) EditGeneralTopic(chat *Chat, topic *Topic) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
"name": topic.Name,
}
_, err := b.Raw("editGeneralForumTopic", params)
return err
}
// CloseGeneralTopic closes an open 'General' topic in a forum supergroup chat.
func (b *Bot) CloseGeneralTopic(chat *Chat) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
}
_, err := b.Raw("closeGeneralForumTopic", params)
return err
}
// ReopenGeneralTopic reopens a closed 'General' topic in a forum supergroup chat.
func (b *Bot) ReopenGeneralTopic(chat *Chat) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
}
_, err := b.Raw("reopenGeneralForumTopic", params)
return err
}
// HideGeneralTopic hides the 'General' topic in a forum supergroup chat.
func (b *Bot) HideGeneralTopic(chat *Chat) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
}
_, err := b.Raw("hideGeneralForumTopic", params)
return err
}
// UnhideGeneralTopic unhides the 'General' topic in a forum supergroup chat.
func (b *Bot) UnhideGeneralTopic(chat *Chat) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
}
_, err := b.Raw("unhideGeneralForumTopic", params)
return err
}
// UnpinAllGeneralTopicMessages clears the list of pinned messages in a General forum topic.
// The bot must be an administrator in the chat for this to work and must have the
// can_pin_messages administrator right in the supergroup.
func (b *Bot) UnpinAllGeneralTopicMessages(chat *Chat) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
}
_, err := b.Raw("unpinAllGeneralForumTopicMessages", params)
return err
}

@ -6,20 +6,24 @@ import "strings"
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"`
Message *Message `json:"message,omitempty"`
EditedMessage *Message `json:"edited_message,omitempty"`
ChannelPost *Message `json:"channel_post,omitempty"`
EditedChannelPost *Message `json:"edited_channel_post,omitempty"`
MessageReaction *MessageReaction `json:"message_reaction"`
MessageReactionCount *MessageReactionCount `json:"message_reaction_count"`
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"`
Boost *BoostUpdated `json:"chat_boost"`
BoostRemoved *BoostRemoved `json:"removed_chat_boost"`
}
// ProcessUpdate processes a single incoming update.
@ -99,6 +103,35 @@ func (b *Bot) ProcessUpdate(u Update) {
return
}
if m.TopicCreated != nil {
b.handle(OnTopicCreated, c)
return
}
if m.TopicReopened != nil {
b.handle(OnTopicReopened, c)
return
}
if m.TopicClosed != nil {
b.handle(OnTopicClosed, c)
return
}
if m.TopicEdited != nil {
b.handle(OnTopicEdited, c)
return
}
if m.GeneralTopicHidden != nil {
b.handle(OnGeneralTopicHidden, c)
return
}
if m.GeneralTopicUnhidden != nil {
b.handle(OnGeneralTopicUnhidden, c)
return
}
if m.WriteAccessAllowed != nil {
b.handle(OnWriteAccessAllowed, 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 {
@ -110,7 +143,6 @@ func (b *Bot) ProcessUpdate(u Update) {
b.handle(OnUserJoined, c)
return
}
if m.UsersJoined != nil {
for _, user := range m.UsersJoined {
m.UserJoined = &user
@ -118,22 +150,28 @@ func (b *Bot) ProcessUpdate(u Update) {
}
return
}
if m.UserLeft != nil {
b.handle(OnUserLeft, c)
return
}
if m.UserShared != nil {
b.handle(OnUserShared, c)
return
}
if m.ChatShared != nil {
b.handle(OnChatShared, 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
@ -143,12 +181,10 @@ func (b *Bot) ProcessUpdate(u Update) {
b.handle(OnGroupCreated, c)
return
}
if m.SuperGroupCreated {
b.handle(OnSuperGroupCreated, c)
return
}
if m.ChannelCreated {
b.handle(OnChannelCreated, c)
return
@ -164,17 +200,14 @@ func (b *Bot) ProcessUpdate(u Update) {
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
@ -182,13 +215,13 @@ func (b *Bot) ProcessUpdate(u Update) {
if m.WebAppData != nil {
b.handle(OnWebApp, c)
return
}
if m.ProximityAlert != nil {
b.handle(OnProximityAlert, c)
return
}
if m.AutoDeleteTimer != nil {
b.handle(OnAutoDeleteTimer, c)
return
@ -279,6 +312,16 @@ func (b *Bot) ProcessUpdate(u Update) {
b.handle(OnChatJoinRequest, c)
return
}
if u.Boost != nil {
b.handle(OnBoost, c)
return
}
if u.BoostRemoved != nil {
b.handle(OnBoostRemoved, c)
return
}
}
func (b *Bot) handle(end string, c Context) bool {

@ -2,26 +2,28 @@ package telebot
import "time"
// VideoChatStarted represents a service message about a video chat
// started in the chat.
type VideoChatStarted struct{}
type (
// VideoChatStarted represents a service message about a video chat
// started in the chat.
VideoChatStarted struct{}
// VideoChatEnded represents a service message about a video chat
// ended in the chat.
type VideoChatEnded struct {
Duration int `json:"duration"` // in seconds
}
// VideoChatEnded represents a service message about a video chat
// ended in the chat.
VideoChatEnded struct {
Duration int `json:"duration"` // in seconds
}
// VideoChatParticipants represents a service message about new
// members invited to a video chat
type VideoChatParticipants struct {
Users []User `json:"users"`
}
// VideoChatParticipants represents a service message about new
// members invited to a video chat
VideoChatParticipants struct {
Users []User `json:"users"`
}
// VideoChatScheduled represents a service message about a video chat scheduled in the chat.
type VideoChatScheduled struct {
Unixtime int64 `json:"start_date"`
}
// VideoChatScheduled represents a service message about a video chat scheduled in the chat.
VideoChatScheduled struct {
Unixtime int64 `json:"start_date"`
}
)
// StartsAt returns the point when the video chat is supposed to be started by a chat administrator.
func (v *VideoChatScheduled) StartsAt() time.Time {

@ -16,3 +16,11 @@ type WebAppData struct {
Data string `json:"data"`
Text string `json:"button_text"`
}
// WebAppAccessAllowed represents a service message about a user allowing
// a bot to write messages after adding the bot to the attachment menu or launching a Web App from a link.
type WriteAccessAllowed struct {
WebAppName string `json:"web_app_name,omitempty"`
FromRequest bool `json:"from_request,omitempty"`
FromAttachmentMenu bool `json:"from_attachment_menu,omitempty"`
}

@ -119,7 +119,7 @@ func (h *Webhook) getParams() map[string]string {
func (h *Webhook) Poll(b *Bot, dest chan Update, stop chan struct{}) {
if err := b.SetWebhook(h); err != nil {
b.debug(err)
b.OnError(err, nil)
close(stop)
return
}

Loading…
Cancel
Save