Compare commits

...

41 Commits

Author SHA1 Message Date
Demian 4d9811176e layout: fix relative paths for New 6 days ago
Demian cc61a965b4 api: add missing methods 2 months ago
Demian 7ee7bd3a21 update: fix m.Origin check 2 months ago
demget 898c3e18f9
api: implement interface (#724)
* api: implement interface

* bot,context: implement ProcessContext, NewContext

* middleware: fix Recover
2 months ago
Nikolay Pavlovich f1007b7c08
Added forward handler (#697)
* Added forward handler

* added OnReply event
2 months ago
Demian 4e8d423603 payments: rename 2 months ago
Олегсей 0442831ecc
implement bot api 7.7 (#728) 2 months ago
Demian 7655e7a9ca bot: implement SendPaid 2 months ago
Demian 9c7a18be08 context: fix payment support 2 months ago
Олегсей 39d57c70bd
implement bot api 7.6 (#729)
Co-authored-by: demget <30910794+demget@users.noreply.github.com>
2 months ago
Demian a02de5490f react: add default reaction type, fix custom_emoji_id naming 2 months ago
Megum1n 08f2e85c39
Add required type field to reactions (#716)
* Add required type field to reactions

* Formatting
2 months ago
zhongqi b19cf78b1f feat: set default menu button 2 months ago
Олегсей e8d6ed91d9
implement bot api 7.8 (#727) 2 months ago
David Shekunts 14ff5ae5d9 fixed reactions count bug 2 months ago
JunJi e90e62d100
add callback_game and pay for InlineButton (#680)
* add callback_game and pay for InlineButton

* Add new type CallbackGame

---------

Co-authored-by: Sniper_Ji <5735962+sniperusopp01@user.noreply.gitee.com>
2 months ago
mhrlife 226075b9d0 Add ignore setWebhook option 2 months ago
Asadbek 8dd54ae1f6
fix: params[spoiler] > params[has_spoiler] (#690) 2 months ago
Taras f1b0fab4df inline: add omitempty to QueryResponseButton::Start 2 months ago
Maxim Nurmukhametov af64372b8e fix missing Caption on Photo unmarshalling 2 months ago
Demian 8b69e47820 telebot: refactor bot api 7.5 2 months ago
Demian 6713166829 telebot: refactor bot api 7.4 2 months ago
Олегсей 1c5b5de314 implement bot api 7.5 (#700)
* BotAPI7.5

* bot, stars: refactor
2 months ago
Олегсей d67653533a implement bot api 7.4 (#698) 2 months ago
Demian c8882690a2 telebot: refactor bot api 7.3 2 months ago
Олегсей c37b2b97b3 implement bot api 7.3 (#699)
* BotAPI7.3

* implement class ChatFullInfo

* chat: removed ChatFullInfo media: added LiveForever

---------

Co-authored-by: demget <30910794+demget@users.noreply.github.com>
2 months ago
Demian ca0a8527e0 telebot: refactor bot api 7.2 2 months ago
Олегсей ffbf081045 implement bot api 7.2 (#706)
* Integration with Business Accounts

* Working on Behalf of Business Accounts

* Information about Business Accounts

* Mixed-Format Sticker Packs

* Request Chat Improvements

* Other Changes

* business: satisfy change request
2 months ago
Demian 864bef4e4d options: fix default parse mode embedding 2 months ago
Demian 2c98c59344 layout: implement nested locales 2 months ago
Demian a8acf84301 react: refactor 4 months ago
Demian 10fbde9b66 boost: refactor 4 months ago
Demian 1acc928a92 middleware: fix tests 4 months ago
Demian c9cec214a9 bot: rename DeleteMessages 4 months ago
Demian c4fce10013 bot: rename ForwardMessages and CopyMessages 4 months ago
Demian 47227931ec bot: refactor Trigger 4 months 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
4 months ago
Nash-Well 77f77a6840
bot: implement Trigger function (#687)
* bot: trigger init

* bot: add execution of registered handlers in trigger func
4 months ago
Demian a26ba9f0bd middleware: add context arg to the Recover 6 months ago
Demian af50a953b5 message: remove InaccessibleMessage completely 6 months ago
nash e16cc083f7 telebot,update: change OnBoostRemoved register 6 months ago

489
api.go

@ -1,377 +1,118 @@
package telebot
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
"strconv"
"strings"
"time"
)
// Raw lets you call any method of Bot API manually.
// It also handles API errors, so you only need to unwrap
// result field from json data.
func (b *Bot) Raw(method string, payload interface{}) ([]byte, error) {
url := b.URL + "/bot" + b.Token + "/" + method
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
return nil, err
}
// 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.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
b.stopMu.RLock()
stopCh := b.stopClient
b.stopMu.RUnlock()
select {
case <-stopCh:
cancel()
case <-ctx.Done():
}
}()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
if err != nil {
return nil, wrapError(err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := b.client.Do(req)
if err != nil {
return nil, wrapError(err)
}
resp.Close = true
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, wrapError(err)
}
if b.verbose {
verbose(method, payload, data)
}
// returning data as well
return data, extractOk(data)
}
func (b *Bot) sendFiles(method string, files map[string]File, params map[string]string) ([]byte, error) {
rawFiles := make(map[string]interface{})
for name, f := range files {
switch {
case f.InCloud():
params[name] = f.FileID
case f.FileURL != "":
params[name] = f.FileURL
case f.OnDisk():
rawFiles[name] = f.FileLocal
case f.FileReader != nil:
rawFiles[name] = f.FileReader
default:
return nil, fmt.Errorf("telebot: file for field %s doesn't exist", name)
}
}
if len(rawFiles) == 0 {
return b.Raw(method, params)
}
pipeReader, pipeWriter := io.Pipe()
writer := multipart.NewWriter(pipeWriter)
go func() {
defer pipeWriter.Close()
for field, file := range rawFiles {
if err := addFileToWriter(writer, files[field].fileName, field, file); err != nil {
pipeWriter.CloseWithError(err)
return
}
}
for field, value := range params {
if err := writer.WriteField(field, value); err != nil {
pipeWriter.CloseWithError(err)
return
}
}
if err := writer.Close(); err != nil {
pipeWriter.CloseWithError(err)
return
}
}()
url := b.URL + "/bot" + b.Token + "/" + method
resp, err := b.client.Post(url, writer.FormDataContentType(), pipeReader)
if err != nil {
err = wrapError(err)
pipeReader.CloseWithError(err)
return nil, err
}
resp.Close = true
defer resp.Body.Close()
if resp.StatusCode == http.StatusInternalServerError {
return nil, ErrInternal
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, wrapError(err)
}
return data, extractOk(data)
}
func addFileToWriter(writer *multipart.Writer, filename, field string, file interface{}) error {
var reader io.Reader
if r, ok := file.(io.Reader); ok {
reader = r
} else if path, ok := file.(string); ok {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
reader = f
} else {
return fmt.Errorf("telebot: file for field %v should be io.ReadCloser or string", field)
}
part, err := writer.CreateFormFile(field, filename)
if err != nil {
return err
}
_, err = io.Copy(part, reader)
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(),
"text": text,
}
b.embedSendOptions(params, opt)
data, err := b.Raw("sendMessage", params)
if err != nil {
return nil, err
}
return extractMessage(data)
}
func (b *Bot) sendMedia(media Media, params map[string]string, files map[string]File) (*Message, error) {
kind := media.MediaType()
what := "send" + strings.Title(kind)
if kind == "videoNote" {
kind = "video_note"
}
sendFiles := map[string]File{kind: *media.MediaFile()}
for k, v := range files {
sendFiles[k] = v
}
data, err := b.sendFiles(what, sendFiles, params)
if err != nil {
return nil, err
}
return extractMessage(data)
}
func (b *Bot) getMe() (*User, error) {
data, err := b.Raw("getMe", nil)
if err != nil {
return nil, err
}
var resp struct {
Result *User
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
}
func (b *Bot) getUpdates(offset, limit int, timeout time.Duration, allowed []string) ([]Update, error) {
params := map[string]string{
"offset": strconv.Itoa(offset),
"timeout": strconv.Itoa(int(timeout / time.Second)),
}
data, _ := json.Marshal(allowed)
params["allowed_updates"] = string(data)
if limit != 0 {
params["limit"] = strconv.Itoa(limit)
}
data, err := b.Raw("getUpdates", params)
if err != nil {
return nil, err
}
var resp struct {
Result []Update
}
if err := json.Unmarshal(data, &resp); err != nil {
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.
func extractOk(data []byte) error {
var e struct {
Ok bool `json:"ok"`
Code int `json:"error_code"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
}
if json.NewDecoder(bytes.NewReader(data)).Decode(&e) != nil {
return nil // FIXME
}
if e.Ok {
return nil
}
err := Err(e.Description)
switch err {
case nil:
case ErrGroupMigrated:
migratedTo, ok := e.Parameters["migrate_to_chat_id"]
if !ok {
return NewError(e.Code, e.Description)
}
return GroupError{
err: err.(*Error),
MigratedTo: int64(migratedTo.(float64)),
}
default:
return err
}
switch e.Code {
case http.StatusTooManyRequests:
retryAfter, ok := e.Parameters["retry_after"]
if !ok {
return NewError(e.Code, e.Description)
}
err = FloodError{
err: NewError(e.Code, e.Description),
RetryAfter: int(retryAfter.(float64)),
}
default:
err = fmt.Errorf("telegram: %s (%d)", e.Description, e.Code)
}
return err
}
// extractMessage extracts common Message result from given data.
// Should be called after extractOk or b.Raw() to handle possible errors.
func extractMessage(data []byte) (*Message, error) {
var resp struct {
Result *Message
}
if err := json.Unmarshal(data, &resp); err != nil {
var resp struct {
Result bool
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
if resp.Result {
return nil, ErrTrueResult
}
return nil, wrapError(err)
}
return resp.Result, nil
}
func (b *Bot) forwardCopyMessages(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
}
func verbose(method string, payload interface{}, data []byte) {
body, _ := json.Marshal(payload)
body = bytes.ReplaceAll(body, []byte(`\"`), []byte(`"`))
body = bytes.ReplaceAll(body, []byte(`"{`), []byte(`{`))
body = bytes.ReplaceAll(body, []byte(`}"`), []byte(`}`))
indent := func(b []byte) string {
var buf bytes.Buffer
json.Indent(&buf, b, "", " ")
return buf.String()
}
log.Printf(
"[verbose] telebot: sent request\nMethod: %v\nParams: %v\nResponse: %v",
method, indent(body), indent(data),
)
import "io"
// API is the interface that wraps all basic methods for interacting
// with Telegram Bot API.
type API interface {
Raw(method string, payload interface{}) ([]byte, error)
Accept(query *PreCheckoutQuery, errorMessage ...string) error
AddStickerToSet(of Recipient, name string, sticker InputSticker) error
AdminsOf(chat *Chat) ([]ChatMember, error)
Answer(query *Query, resp *QueryResponse) error
AnswerWebApp(query *Query, r Result) (*WebAppMessage, error)
ApproveJoinRequest(chat Recipient, user *User) error
Ban(chat *Chat, member *ChatMember, revokeMessages ...bool) error
BanSenderChat(chat *Chat, sender Recipient) error
BusinessConnection(id string) (*BusinessConnection, error)
ChatByID(id int64) (*Chat, error)
ChatByUsername(name string) (*Chat, error)
ChatMemberOf(chat, user Recipient) (*ChatMember, error)
Close() (bool, error)
CloseGeneralTopic(chat *Chat) error
CloseTopic(chat *Chat, topic *Topic) error
Commands(opts ...interface{}) ([]Command, error)
Copy(to Recipient, msg Editable, opts ...interface{}) (*Message, error)
CopyMany(to Recipient, msgs []Editable, opts ...*SendOptions) ([]Message, error)
CreateInviteLink(chat Recipient, link *ChatInviteLink) (*ChatInviteLink, error)
CreateInvoiceLink(i Invoice) (string, error)
CreateStickerSet(of Recipient, set *StickerSet) error
CreateTopic(chat *Chat, topic *Topic) (*Topic, error)
CustomEmojiStickers(ids []string) ([]Sticker, error)
DeclineJoinRequest(chat Recipient, user *User) error
DefaultRights(forChannels bool) (*Rights, error)
Delete(msg Editable) error
DeleteCommands(opts ...interface{}) error
DeleteGroupPhoto(chat *Chat) error
DeleteGroupStickerSet(chat *Chat) error
DeleteMany(msgs []Editable) error
DeleteSticker(sticker string) error
DeleteStickerSet(name string) error
DeleteTopic(chat *Chat, topic *Topic) error
Download(file *File, localFilename string) error
Edit(msg Editable, what interface{}, opts ...interface{}) (*Message, error)
EditCaption(msg Editable, caption string, opts ...interface{}) (*Message, error)
EditGeneralTopic(chat *Chat, topic *Topic) error
EditInviteLink(chat Recipient, link *ChatInviteLink) (*ChatInviteLink, error)
EditMedia(msg Editable, media Inputtable, opts ...interface{}) (*Message, error)
EditReplyMarkup(msg Editable, markup *ReplyMarkup) (*Message, error)
EditTopic(chat *Chat, topic *Topic) error
File(file *File) (io.ReadCloser, error)
FileByID(fileID string) (File, error)
Forward(to Recipient, msg Editable, opts ...interface{}) (*Message, error)
ForwardMany(to Recipient, msgs []Editable, opts ...*SendOptions) ([]Message, error)
GameScores(user Recipient, msg Editable) ([]GameHighScore, error)
HideGeneralTopic(chat *Chat) error
InviteLink(chat *Chat) (string, error)
Leave(chat Recipient) error
Len(chat *Chat) (int, error)
Logout() (bool, error)
MenuButton(chat *User) (*MenuButton, error)
MyDescription(language string) (*BotInfo, error)
MyName(language string) (*BotInfo, error)
MyShortDescription(language string) (*BotInfo, error)
Notify(to Recipient, action ChatAction, threadID ...int) error
Pin(msg Editable, opts ...interface{}) error
ProfilePhotosOf(user *User) ([]Photo, error)
Promote(chat *Chat, member *ChatMember) error
React(to Recipient, msg Editable, r Reactions) error
RefundStars(to Recipient, chargeID string) error
RemoveWebhook(dropPending ...bool) error
ReopenGeneralTopic(chat *Chat) error
ReopenTopic(chat *Chat, topic *Topic) error
ReplaceStickerInSet(of Recipient, stickerSet, oldSticker string, sticker InputSticker) (bool, error)
Reply(to *Message, what interface{}, opts ...interface{}) (*Message, error)
Respond(c *Callback, resp ...*CallbackResponse) error
Restrict(chat *Chat, member *ChatMember) error
RevokeInviteLink(chat Recipient, link string) (*ChatInviteLink, error)
Send(to Recipient, what interface{}, opts ...interface{}) (*Message, error)
SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message, error)
SendPaid(to Recipient, stars int, a PaidAlbum, opts ...interface{}) (*Message, error)
SetAdminTitle(chat *Chat, user *User, title string) error
SetCommands(opts ...interface{}) error
SetCustomEmojiStickerSetThumb(name, id string) error
SetDefaultRights(rights Rights, forChannels bool) error
SetGameScore(user Recipient, msg Editable, score GameHighScore) (*Message, error)
SetGroupDescription(chat *Chat, description string) error
SetGroupPermissions(chat *Chat, perms Rights) error
SetGroupStickerSet(chat *Chat, setName string) error
SetGroupTitle(chat *Chat, title string) error
SetMenuButton(chat *User, mb interface{}) error
SetMyDescription(desc, language string) error
SetMyName(name, language string) error
SetMyShortDescription(desc, language string) error
SetStickerEmojis(sticker string, emojis []string) error
SetStickerKeywords(sticker string, keywords []string) error
SetStickerMaskPosition(sticker string, mask MaskPosition) error
SetStickerPosition(sticker string, position int) error
SetStickerSetThumb(of Recipient, set *StickerSet) error
SetStickerSetTitle(s StickerSet) error
SetWebhook(w *Webhook) error
Ship(query *ShippingQuery, what ...interface{}) error
StarTransactions(offset, limit int) ([]StarTransaction, error)
StickerSet(name string) (*StickerSet, error)
StopLiveLocation(msg Editable, opts ...interface{}) (*Message, error)
StopPoll(msg Editable, opts ...interface{}) (*Poll, error)
TopicIconStickers() ([]Sticker, error)
Unban(chat *Chat, user *User, forBanned ...bool) error
UnbanSenderChat(chat *Chat, sender Recipient) error
UnhideGeneralTopic(chat *Chat) error
Unpin(chat Recipient, messageID ...int) error
UnpinAll(chat Recipient) error
UnpinAllGeneralTopicMessages(chat *Chat) error
UnpinAllTopicMessages(chat *Chat, topic *Topic) error
UploadSticker(to Recipient, format StickerSetFormat, f File) (*File, error)
UserBoosts(chat, user Recipient) ([]Boost, error)
Webhook() (*Webhook, error)
}

@ -36,9 +36,9 @@ func (c *Boost) ExpirationDate() time.Time {
type BoostSourceType = string
const (
BoostPremium = "premium"
BoostGiftCode = "gift_code"
BoostGiveaway = "giveaway"
BoostPremium BoostSourceType = "premium"
BoostGiftCode BoostSourceType = "gift_code"
BoostGiveaway BoostSourceType = "giveaway"
)
// BoostSource describes the source of a chat boost.
@ -102,10 +102,12 @@ func (b *Bot) UserBoosts(chat, user Recipient) ([]Boost, error) {
}
var resp struct {
Result []Boost `json:"boosts"`
Result struct {
Boosts []Boost `json:"boosts"`
}
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
return resp.Result.Boosts, nil
}

162
bot.go

@ -175,22 +175,33 @@ var (
//
// 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 = 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
@ -255,10 +266,7 @@ func (b *Bot) NewMarkup() *ReplyMarkup {
// NewContext returns a new native context object,
// field by the passed update.
func (b *Bot) NewContext(u Update) Context {
return &nativeContext{
b: b,
u: u,
}
return NewContext(b, u)
}
// Send accepts 2+ arguments, starting with destination chat, followed by
@ -280,7 +288,7 @@ func (b *Bot) Send(to Recipient, what interface{}, opts ...interface{}) (*Messag
return nil, ErrBadRecipient
}
sendOpts := extractOptions(opts)
sendOpts := b.extractOptions(opts)
switch object := what.(type) {
case string:
@ -292,6 +300,53 @@ func (b *Bot) Send(to Recipient, what interface{}, opts ...interface{}) (*Messag
}
}
// SendPaid sends multiple instances of paid media as a single message.
// To include the caption, make sure the first PaidInputtable of an album has it.
func (b *Bot) SendPaid(to Recipient, stars int, a PaidAlbum, opts ...interface{}) (*Message, error) {
if to == nil {
return nil, ErrBadRecipient
}
params := map[string]string{
"chat_id": to.Recipient(),
"star_count": strconv.Itoa(stars),
}
sendOpts := b.extractOptions(opts)
media := make([]string, len(a))
files := make(map[string]File)
for i, x := range a {
repr := x.MediaFile().process(strconv.Itoa(i), files)
if repr == "" {
return nil, fmt.Errorf("telebot: paid media entry #%d does not exist", i)
}
im := x.InputMedia()
im.Media = repr
if i == 0 {
params["caption"] = im.Caption
if im.CaptionAbove {
params["show_caption_above_media"] = "true"
}
}
data, _ := json.Marshal(im)
media[i] = string(data)
}
params["media"] = "[" + strings.Join(media, ",") + "]"
b.embedSendOptions(params, sendOpts)
data, err := b.sendFiles("sendPaidMedia", files, params)
if err != nil {
return nil, err
}
return extractMessage(data)
}
// 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.
@ -300,7 +355,7 @@ func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message,
return nil, ErrBadRecipient
}
sendOpts := extractOptions(opts)
sendOpts := b.extractOptions(opts)
media := make([]string, len(a))
files := make(map[string]File)
@ -366,7 +421,7 @@ func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message,
// Reply behaves just like Send() with an exception of "reply-to" indicator.
// This function will panic upon nil Message.
func (b *Bot) Reply(to *Message, what interface{}, opts ...interface{}) (*Message, error) {
sendOpts := extractOptions(opts)
sendOpts := b.extractOptions(opts)
if sendOpts == nil {
sendOpts = &SendOptions{}
}
@ -389,7 +444,7 @@ func (b *Bot) Forward(to Recipient, msg Editable, opts ...interface{}) (*Message
"message_id": msgID,
}
sendOpts := extractOptions(opts)
sendOpts := b.extractOptions(opts)
b.embedSendOptions(params, sendOpts)
data, err := b.Raw("forwardMessage", params)
@ -400,21 +455,21 @@ func (b *Bot) Forward(to Recipient, msg Editable, opts ...interface{}) (*Message
return extractMessage(data)
}
// ForwardMessages method forwards multiple messages of any kind.
// 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) ForwardMessages(to Recipient, msgs []Editable, opts ...*SendOptions) ([]Message, error) {
func (b *Bot) ForwardMany(to Recipient, msgs []Editable, opts ...*SendOptions) ([]Message, error) {
if to == nil {
return nil, ErrBadRecipient
}
return b.forwardCopyMessages(to, msgs, "forwardMessages", opts...)
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.
func (b *Bot) Copy(to Recipient, msg Editable, options ...interface{}) (*Message, error) {
func (b *Bot) Copy(to Recipient, msg Editable, opts ...interface{}) (*Message, error) {
if to == nil {
return nil, ErrBadRecipient
}
@ -426,7 +481,7 @@ func (b *Bot) Copy(to Recipient, msg Editable, options ...interface{}) (*Message
"message_id": msgID,
}
sendOpts := extractOptions(options)
sendOpts := b.extractOptions(opts)
b.embedSendOptions(params, sendOpts)
data, err := b.Raw("copyMessage", params)
@ -437,18 +492,18 @@ func (b *Bot) Copy(to Recipient, msg Editable, options ...interface{}) (*Message
return extractMessage(data)
}
// CopyMessages this method makes a copy of messages of any kind.
// 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) CopyMessages(to Recipient, msgs []Editable, opts ...*SendOptions) ([]Message, error) {
func (b *Bot) CopyMany(to Recipient, msgs []Editable, opts ...*SendOptions) ([]Message, error) {
if to == nil {
return nil, ErrBadRecipient
}
return b.forwardCopyMessages(to, msgs, "copyMessages", opts...)
return b.forwardCopyMany(to, msgs, "copyMessages", opts...)
}
// Edit is magic, it lets you change already sent message.
@ -494,6 +549,9 @@ func (b *Bot) Edit(msg Editable, what interface{}, opts ...interface{}) (*Messag
if v.AlertRadius != 0 {
params["proximity_alert_radius"] = strconv.Itoa(v.AlertRadius)
}
if v.LivePeriod != 0 {
params["live_period"] = strconv.Itoa(v.LivePeriod)
}
default:
return nil, ErrUnsupportedWhat
}
@ -507,7 +565,7 @@ func (b *Bot) Edit(msg Editable, what interface{}, opts ...interface{}) (*Messag
params["message_id"] = msgID
}
sendOpts := extractOptions(opts)
sendOpts := b.extractOptions(opts)
b.embedSendOptions(params, sendOpts)
data, err := b.Raw(method, params)
@ -571,7 +629,7 @@ func (b *Bot) EditCaption(msg Editable, caption string, opts ...interface{}) (*M
params["message_id"] = msgID
}
sendOpts := extractOptions(opts)
sendOpts := b.extractOptions(opts)
b.embedSendOptions(params, sendOpts)
data, err := b.Raw("editMessageCaption", params)
@ -635,7 +693,7 @@ func (b *Bot) EditMedia(msg Editable, media Inputtable, opts ...interface{}) (*M
msgID, chatID := msg.MessageSig()
params := make(map[string]string)
sendOpts := extractOptions(opts)
sendOpts := b.extractOptions(opts)
b.embedSendOptions(params, sendOpts)
im := media.InputMedia()
@ -693,11 +751,10 @@ func (b *Bot) Delete(msg Editable) error {
return err
}
// DeleteMessages deletes multiple messages simultaneously.
// DeleteMany deletes multiple messages simultaneously.
// If some of the specified messages can't be found, they are skipped.
func (b *Bot) DeleteMessages(msgs []Editable) error {
func (b *Bot) DeleteMany(msgs []Editable) error {
params := make(map[string]string)
embedMessages(params, msgs)
_, err := b.Raw("deleteMessages", params)
@ -937,7 +994,7 @@ func (b *Bot) StopLiveLocation(msg Editable, opts ...interface{}) (*Message, err
"message_id": msgID,
}
sendOpts := extractOptions(opts)
sendOpts := b.extractOptions(opts)
b.embedSendOptions(params, sendOpts)
data, err := b.Raw("stopMessageLiveLocation", params)
@ -961,7 +1018,7 @@ func (b *Bot) StopPoll(msg Editable, opts ...interface{}) (*Poll, error) {
"message_id": msgID,
}
sendOpts := extractOptions(opts)
sendOpts := b.extractOptions(opts)
b.embedSendOptions(params, sendOpts)
data, err := b.Raw("stopPoll", params)
@ -1000,7 +1057,7 @@ func (b *Bot) Pin(msg Editable, opts ...interface{}) error {
"message_id": msgID,
}
sendOpts := extractOptions(opts)
sendOpts := b.extractOptions(opts)
b.embedSendOptions(params, sendOpts)
_, err := b.Raw("pinChatMessage", params)
@ -1136,8 +1193,11 @@ func (b *Bot) MenuButton(chat *User) (*MenuButton, error) {
// - 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(),
params := map[string]interface{}{}
// chat_id is optional
if chat != nil {
params["chat_id"] = chat.Recipient()
}
switch v := mb.(type) {
@ -1242,6 +1302,28 @@ func (b *Bot) MyShortDescription(language string) (*BotInfo, error) {
return b.botInfo(language, "getMyShortDescription")
}
func (b *Bot) StarTransactions(offset, limit int) ([]StarTransaction, error) {
params := map[string]int{
"offset": offset,
"limit": limit,
}
data, err := b.Raw("getStarTransactions", params)
if err != nil {
return nil, err
}
var resp struct {
Result struct {
Transactions []StarTransaction `json:"transactions"`
}
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result.Transactions, nil
}
func (b *Bot) botInfo(language, key string) (*BotInfo, error) {
params := map[string]string{
"language_code": language,
@ -1260,3 +1342,13 @@ func (b *Bot) botInfo(language, key string) (*BotInfo, error) {
}
return resp.Result, nil
}
func extractEndpoint(endpoint interface{}) string {
switch end := endpoint.(type) {
case string:
return end
case CallbackEndpoint:
return end.CallbackUnique()
}
return ""
}

@ -0,0 +1,377 @@
package telebot
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
"strconv"
"strings"
"time"
)
// Raw lets you call any method of Bot API manually.
// It also handles API errors, so you only need to unwrap
// result field from json data.
func (b *Bot) Raw(method string, payload interface{}) ([]byte, error) {
url := b.URL + "/bot" + b.Token + "/" + method
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
return nil, err
}
// 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.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
b.stopMu.RLock()
stopCh := b.stopClient
b.stopMu.RUnlock()
select {
case <-stopCh:
cancel()
case <-ctx.Done():
}
}()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
if err != nil {
return nil, wrapError(err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := b.client.Do(req)
if err != nil {
return nil, wrapError(err)
}
resp.Close = true
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, wrapError(err)
}
if b.verbose {
verbose(method, payload, data)
}
// returning data as well
return data, extractOk(data)
}
func (b *Bot) sendFiles(method string, files map[string]File, params map[string]string) ([]byte, error) {
rawFiles := make(map[string]interface{})
for name, f := range files {
switch {
case f.InCloud():
params[name] = f.FileID
case f.FileURL != "":
params[name] = f.FileURL
case f.OnDisk():
rawFiles[name] = f.FileLocal
case f.FileReader != nil:
rawFiles[name] = f.FileReader
default:
return nil, fmt.Errorf("telebot: file for field %s doesn't exist", name)
}
}
if len(rawFiles) == 0 {
return b.Raw(method, params)
}
pipeReader, pipeWriter := io.Pipe()
writer := multipart.NewWriter(pipeWriter)
go func() {
defer pipeWriter.Close()
for field, file := range rawFiles {
if err := addFileToWriter(writer, files[field].fileName, field, file); err != nil {
pipeWriter.CloseWithError(err)
return
}
}
for field, value := range params {
if err := writer.WriteField(field, value); err != nil {
pipeWriter.CloseWithError(err)
return
}
}
if err := writer.Close(); err != nil {
pipeWriter.CloseWithError(err)
return
}
}()
url := b.URL + "/bot" + b.Token + "/" + method
resp, err := b.client.Post(url, writer.FormDataContentType(), pipeReader)
if err != nil {
err = wrapError(err)
pipeReader.CloseWithError(err)
return nil, err
}
resp.Close = true
defer resp.Body.Close()
if resp.StatusCode == http.StatusInternalServerError {
return nil, ErrInternal
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, wrapError(err)
}
return data, extractOk(data)
}
func addFileToWriter(writer *multipart.Writer, filename, field string, file interface{}) error {
var reader io.Reader
if r, ok := file.(io.Reader); ok {
reader = r
} else if path, ok := file.(string); ok {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
reader = f
} else {
return fmt.Errorf("telebot: file for field %v should be io.ReadCloser or string", field)
}
part, err := writer.CreateFormFile(field, filename)
if err != nil {
return err
}
_, err = io.Copy(part, reader)
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(),
"text": text,
}
b.embedSendOptions(params, opt)
data, err := b.Raw("sendMessage", params)
if err != nil {
return nil, err
}
return extractMessage(data)
}
func (b *Bot) sendMedia(media Media, params map[string]string, files map[string]File) (*Message, error) {
kind := media.MediaType()
what := "send" + strings.Title(kind)
if kind == "videoNote" {
kind = "video_note"
}
sendFiles := map[string]File{kind: *media.MediaFile()}
for k, v := range files {
sendFiles[k] = v
}
data, err := b.sendFiles(what, sendFiles, params)
if err != nil {
return nil, err
}
return extractMessage(data)
}
func (b *Bot) getMe() (*User, error) {
data, err := b.Raw("getMe", nil)
if err != nil {
return nil, err
}
var resp struct {
Result *User
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
}
func (b *Bot) getUpdates(offset, limit int, timeout time.Duration, allowed []string) ([]Update, error) {
params := map[string]string{
"offset": strconv.Itoa(offset),
"timeout": strconv.Itoa(int(timeout / time.Second)),
}
data, _ := json.Marshal(allowed)
params["allowed_updates"] = string(data)
if limit != 0 {
params["limit"] = strconv.Itoa(limit)
}
data, err := b.Raw("getUpdates", params)
if err != nil {
return nil, err
}
var resp struct {
Result []Update
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
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.
func extractOk(data []byte) error {
var e struct {
Ok bool `json:"ok"`
Code int `json:"error_code"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
}
if json.NewDecoder(bytes.NewReader(data)).Decode(&e) != nil {
return nil // FIXME
}
if e.Ok {
return nil
}
err := Err(e.Description)
switch err {
case nil:
case ErrGroupMigrated:
migratedTo, ok := e.Parameters["migrate_to_chat_id"]
if !ok {
return NewError(e.Code, e.Description)
}
return GroupError{
err: err.(*Error),
MigratedTo: int64(migratedTo.(float64)),
}
default:
return err
}
switch e.Code {
case http.StatusTooManyRequests:
retryAfter, ok := e.Parameters["retry_after"]
if !ok {
return NewError(e.Code, e.Description)
}
err = FloodError{
err: NewError(e.Code, e.Description),
RetryAfter: int(retryAfter.(float64)),
}
default:
err = fmt.Errorf("telegram: %s (%d)", e.Description, e.Code)
}
return err
}
// extractMessage extracts common Message result from given data.
// Should be called after extractOk or b.Raw() to handle possible errors.
func extractMessage(data []byte) (*Message, error) {
var resp struct {
Result *Message
}
if err := json.Unmarshal(data, &resp); err != nil {
var resp struct {
Result bool
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
if resp.Result {
return nil, ErrTrueResult
}
return nil, wrapError(err)
}
return resp.Result, nil
}
func verbose(method string, payload interface{}, data []byte) {
body, _ := json.Marshal(payload)
body = bytes.ReplaceAll(body, []byte(`\"`), []byte(`"`))
body = bytes.ReplaceAll(body, []byte(`"{`), []byte(`{`))
body = bytes.ReplaceAll(body, []byte(`}"`), []byte(`}`))
indent := func(b []byte) string {
var buf bytes.Buffer
json.Indent(&buf, b, "", " ")
return buf.String()
}
log.Printf(
"[verbose] telebot: sent request\nMethod: %v\nParams: %v\nResponse: %v",
method, indent(body), indent(data),
)
}

@ -18,12 +18,15 @@ import (
var (
// required to test send and edit methods
token = os.Getenv("TELEBOT_SECRET")
b, _ = newTestBot() // cached bot instance to avoid getMe method flooding
chatID, _ = strconv.ParseInt(os.Getenv("CHAT_ID"), 10, 64)
userID, _ = strconv.ParseInt(os.Getenv("USER_ID"), 10, 64)
channelID, _ = strconv.ParseInt(os.Getenv("CHANNEL_ID"), 10, 64)
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
channel = &Chat{ID: channelID} // to channel recipient for some special cases
logo = FromURL("https://telegra.ph/file/c95b8fe46dd3df15d12e5.png")
thumb = FromURL("https://telegra.ph/file/fe28e378784b3a4e367fb.png")
@ -231,6 +234,10 @@ func TestBotProcessUpdate(t *testing.T) {
assert.NotNil(t, c.Message().Payment)
return nil
})
b.Handle(OnRefund, func(c Context) error {
assert.NotNil(t, c.Message().RefundedPayment)
return nil
})
b.Handle(OnAddedToGroup, func(c Context) error {
assert.NotNil(t, c.Message().GroupCreated)
return nil
@ -317,7 +324,7 @@ func TestBotProcessUpdate(t *testing.T) {
b.ProcessUpdate(Update{Message: &Message{Text: "/start@other_bot"}})
b.ProcessUpdate(Update{Message: &Message{Text: "hello"}})
b.ProcessUpdate(Update{Message: &Message{Text: "text"}})
b.ProcessUpdate(Update{Message: &Message{PinnedMessage: &InaccessibleMessage{Message: &Message{}}}})
b.ProcessUpdate(Update{Message: &Message{PinnedMessage: &Message{}}})
b.ProcessUpdate(Update{Message: &Message{Photo: &Photo{}}})
b.ProcessUpdate(Update{Message: &Message{Voice: &Voice{}}})
b.ProcessUpdate(Update{Message: &Message{Audio: &Audio{}}})
@ -331,6 +338,7 @@ func TestBotProcessUpdate(t *testing.T) {
b.ProcessUpdate(Update{Message: &Message{Venue: &Venue{}}})
b.ProcessUpdate(Update{Message: &Message{Invoice: &Invoice{}}})
b.ProcessUpdate(Update{Message: &Message{Payment: &Payment{}}})
b.ProcessUpdate(Update{Message: &Message{RefundedPayment: &RefundedPayment{}}})
b.ProcessUpdate(Update{Message: &Message{Dice: &Dice{}}})
b.ProcessUpdate(Update{Message: &Message{GroupCreated: true}})
b.ProcessUpdate(Update{Message: &Message{UserJoined: &User{ID: 1}}})
@ -342,7 +350,7 @@ func TestBotProcessUpdate(t *testing.T) {
b.ProcessUpdate(Update{Message: &Message{Chat: &Chat{ID: 1}, MigrateTo: 2}})
b.ProcessUpdate(Update{EditedMessage: &Message{Text: "edited"}})
b.ProcessUpdate(Update{ChannelPost: &Message{Text: "post"}})
b.ProcessUpdate(Update{ChannelPost: &Message{PinnedMessage: &InaccessibleMessage{Message: &Message{}}}})
b.ProcessUpdate(Update{ChannelPost: &Message{PinnedMessage: &Message{}}})
b.ProcessUpdate(Update{EditedChannelPost: &Message{Text: "edited post"}})
b.ProcessUpdate(Update{Callback: &Callback{MessageID: "inline", Data: "callback"}})
b.ProcessUpdate(Update{Callback: &Callback{Data: "callback"}})
@ -533,8 +541,25 @@ func TestBot(t *testing.T) {
assert.NotEmpty(t, msgs[0].AlbumID)
})
t.Run("SendPaid()", func(t *testing.T) {
_, err = b.SendPaid(nil, 0, nil)
assert.Equal(t, ErrBadRecipient, err)
_, err = b.SendPaid(channel, 0, nil)
assert.Error(t, err)
photo2 := *photo
photo2.Caption = ""
msg, err := b.SendPaid(channel, 1, PaidAlbum{photo, &photo2}, ModeHTML)
require.NoError(t, err)
require.NotNil(t, msg)
assert.Equal(t, 1, msg.PaidMedia.Stars)
assert.Equal(t, 2, len(msg.PaidMedia.PaidMedia))
})
t.Run("EditCaption()+ParseMode", func(t *testing.T) {
b.parseMode = ModeHTML
b.parseMode = "html"
edited, err := b.EditCaption(msg, "<b>new caption with html</b>")
require.NoError(t, err)
@ -554,14 +579,15 @@ func TestBot(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "new caption with markdown (V2)", edited.Caption)
assert.Equal(t, EntityItalic, edited.CaptionEntities[0].Type)
b.parseMode = ModeDefault
})
t.Run("Edit(what=Media)", func(t *testing.T) {
photo.Caption = "<code>new caption with html</code>"
edited, err := b.Edit(msg, photo)
require.NoError(t, err)
assert.Equal(t, edited.Photo.UniqueID, photo.UniqueID)
assert.Equal(t, EntityCode, edited.CaptionEntities[0].Type)
resp, err := http.Get("https://telegra.ph/file/274e5eb26f348b10bd8ee.mp4")
require.NoError(t, err)

@ -0,0 +1,106 @@
package telebot
import (
"encoding/json"
"time"
)
type BusinessConnection struct {
// Unique identifier of the business connection
ID string `json:"id"`
// Business account user that created the business connection
Sender *User `json:"user"`
// Identifier of a private chat with the user who created the business connection. This
// number may have more than 32 significant bits and some programming languages may
// have difficulty/silent defects in interpreting it. But it has at most 52 significant bits,
// so a 64-bit integer or double-precision float type are safe for storing this identifier.
UserChatID int64 `json:"user_chat_id"`
// Unixtime, use BusinessConnection.Time() to get time.Time.
Unixtime int64 `json:"date"`
// True, if the bot can act on behalf of the business account in chats that were active in the last 24 hours
CanReply bool `json:"can_reply"`
// True, if the connection is active
Enabled bool `json:"is_enabled"`
}
// Time returns the moment of business connection creation in local time.
func (b *BusinessConnection) Time() time.Time {
return time.Unix(b.Unixtime, 0)
}
type BusinessMessagesDeleted struct {
// Unique identifier of the business connection
BusinessConnectionID string `json:"business_connection_id"`
// Information about a chat in the business account. The bot
// may not have access to the chat or the corresponding user.
Chat *Chat `json:"chat"`
// The list of identifiers of deleted messages in the chat of the business account
MessageIDs []int `json:"message_ids"`
}
type BusinessIntro struct {
// (Optional)
// Title text of the business intro
Title string `json:"title"`
// Message text of the business intro
Message string `json:"message"`
// Sticker of the business intro
Sticker *Sticker `json:"sticker"`
}
type BusinessLocation struct {
// Address of the business
Address string `json:"address"`
// (Optional) Location of the business
Location *Location `json:"location"`
}
type BusinessOpeningHoursInterval struct {
// The minute's sequence number in a week, starting on Monday,
// marking the start of the time interval during which the business
// is open; 0 - 7 * 24 * 60
OpeningMinute int `json:"opening_minute"`
// The minute's sequence number in a week, starting on Monday,
// marking the start of the time interval during which the business
// is open; 0 - 7 * 24 * 60
ClosingMinute int `json:"closing_minute"`
}
type BusinessOpeningHours struct {
// Unique name of the time zone for which the opening hours are defined
Timezone string `json:"time_zone_name"`
// List of time intervals describing business opening hours
OpeningHours []BusinessOpeningHoursInterval `json:"opening_hours"`
}
// BusinessConnection returns the information about the connection of the bot with a business account.
func (b *Bot) BusinessConnection(id string) (*BusinessConnection, error) {
params := map[string]string{
"business_connection_id": id,
}
data, err := b.Raw("getBusinessConnection", params)
if err != nil {
return nil, err
}
var resp struct {
Result *BusinessConnection
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
}

@ -16,7 +16,7 @@ type Callback struct {
// Message will be set if the button that originated the query
// was attached to a message sent by a bot.
Message *InaccessibleMessage `json:"message"`
Message *Message `json:"message"`
// MessageID will be set if the button was attached to a message
// sent via the bot in inline mode.

@ -19,12 +19,14 @@ type User struct {
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"`
StatusCustomEmojiID string `json:"emoji_status_custom_emoji_id"`
// Returns only in getMe
CanJoinGroups bool `json:"can_join_groups"`
CanReadMessages bool `json:"can_read_all_group_messages"`
SupportsInline bool `json:"supports_inline_queries"`
CanConnectToBusiness bool `json:"can_connect_to_business"`
HasMainWebApp bool `json:"has_main_web_app"`
}
// Recipient returns user ID (see Recipient interface).
@ -57,7 +59,8 @@ type Chat struct {
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"`
CanSendPaidMedia bool `json:"can_send_paid_media"`
CustomEmojiStickerSet 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"`
@ -65,14 +68,20 @@ type Chat struct {
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"`
StatusCustomEmojiID string `json:"emoji_status_custom_emoji_id"`
EmojiExpirationUnixtime int64 `json:"emoji_status_expiration_date"`
BackgroundEmojiID string `json:"background_custom_emoji_id"`
BackgroundCustomEmojiID 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"`
ProfileBackgroundCustomEmojiID string `json:"profile_background_custom_emoji_id"`
HasVisibleHistory bool `json:"has_visible_history"`
UnrestrictBoosts int `json:"unrestrict_boost_count"`
MaxReactions int `json:"max_reaction_count"`
Birthdate Birthdate `json:"birthdate,omitempty"`
PersonalChat *Chat `json:"personal_chat,omitempty"`
BusinessIntro BusinessIntro `json:"business_intro,omitempty"`
BusinessLocation BusinessLocation `json:"business_location,omitempty"`
BusinessOpeningHours BusinessOpeningHours `json:"business_opening_hours,omitempty"`
}
// Recipient returns chat ID (see Recipient interface).
@ -165,6 +174,11 @@ type ChatMemberUpdate struct {
// join the chat; for joining by invite link events only.
InviteLink *ChatInviteLink `json:"invite_link"`
// (Optional) True, if the user joined the chat after sending
// a direct join request without using an invite link and being
// approved by an administrator
ViaJoinRequest bool `json:"via_join_request"`
// (Optional) True, if the user joined the chat via a chat folder invite link.
ViaFolderLink bool `json:"via_chat_folder_invite_link"`
}
@ -261,6 +275,74 @@ type Story struct {
Poster *Chat `json:"chat"`
}
type Birthdate struct {
// Day of the user's birth; 1-31
Day int `json:"day"`
// Month of the user's birth; 1-12
Month int `json:"month"`
// (Optional) Year of the user's birth
Year int `json:"year"`
}
type ChatBackground struct {
// Type of the background
Type BackgroundType `json:"type"`
}
type BackgroundType struct {
// Type of the background, always “fill”
Type string `json:"type"`
// The background fill
Fill BackgroundFill `json:"fill,omitempty"`
// Document with the wallpaper
Document Document `json:"document,omitempty"`
// Dimming of the background in dark themes, as a percentage; 0-100
DarkThemeDimming int `json:"dark_theme_dimming,omitempty"`
// Intensity of the pattern when it is shown above the filled background; 0-100
Intensity int `json:"intensity,omitempty"`
// (Optional) True, if the wallpaper is downscaled to fit in a 450x450
// square and then box-blurred with radius 12
Blurred bool `json:"is_blurred,omitempty"`
// (Optional) True, if the background moves slightly when the device is tilted
Moving bool `json:"is_moving,omitempty"`
// (Optional) True, if the background fill must be applied only to the pattern itself.
// All other pixels are black in this case. For dark themes only
Inverted bool `json:"is_inverted,omitempty"`
// Name of the chat theme, which is usually an emoji
ThemeName string `json:"theme_name,omitempty"`
}
type BackgroundFill struct {
// Type of the background fill.
Type string `json:"type"`
// The color of the background fill in the RGB24 format
SolidColor int `json:"color,omitempty"`
// Top color of the gradient in the RGB24 format
GradientTopColor int `json:"top_color,omitempty"`
// Bottom color of the gradient in the RGB24 format
GradientBottomColor int `json:"bottom_color,omitempty"`
// Clockwise rotation angle of the background fill in degrees; 0-359
GradientRotationAngle int `json:"rotation_angle,omitempty"`
// A list of the 3 or 4 base colors that are used to generate
// the freeform gradient in the RGB24 format
GradientColors []int `json:"colors,omitempty"`
}
// ExpireDate returns the moment of the link expiration in local time.
func (c *ChatInviteLink) ExpireDate() time.Time {
return time.Unix(c.ExpireUnixtime, 0)

@ -11,16 +11,19 @@ import (
// used to handle actual endpoints.
type HandlerFunc func(Context) error
// NewContext returns a new native context object,
// field by the passed update.
func NewContext(b API, u Update) Context {
return &nativeContext{
b: b,
u: u,
}
}
// Context wraps an update and represents the context of current event.
type Context interface {
// Bot returns the bot instance.
Bot() *Bot
// Boost returns the boost instance.
Boost() *BoostUpdated
// BoostRemoved returns the boost removed from a chat instance.
BoostRemoved() *BoostRemoved
Bot() API
// Update returns the original update.
Update() Update
@ -43,6 +46,9 @@ type Context interface {
// PreCheckoutQuery returns stored pre checkout query if such presented.
PreCheckoutQuery() *PreCheckoutQuery
// Payment returns payment instance.
Payment() *Payment
// Poll returns stored poll if such presented.
Poll() *Poll
@ -61,6 +67,12 @@ type Context interface {
// 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
@ -68,7 +80,6 @@ type Context interface {
// Chat returns the current chat, depending on the context type.
// Returns nil if chat is not presented.
Chat() *Chat
// Recipient combines both Sender and Chat functions. If there is no user
// the chat will be returned. The native context cannot be without sender,
// but it is useful in the case when the context created intentionally
@ -174,24 +185,16 @@ type Context interface {
// nativeContext is a native implementation of the Context interface.
// "context" is taken by context package, maybe there is a better name.
type nativeContext struct {
b *Bot
b API
u Update
lock sync.RWMutex
store map[string]interface{}
}
func (c *nativeContext) Bot() *Bot {
func (c *nativeContext) Bot() API {
return c.b
}
func (c *nativeContext) Boost() *BoostUpdated {
return c.u.Boost
}
func (c *nativeContext) BoostRemoved() *BoostRemoved {
return c.u.BoostRemoved
}
func (c *nativeContext) Update() Update {
return c.u
}
@ -201,12 +204,12 @@ func (c *nativeContext) Message() *Message {
case c.u.Message != nil:
return c.u.Message
case c.u.Callback != nil:
return c.u.Callback.Message.Message
return c.u.Callback.Message
case c.u.EditedMessage != nil:
return c.u.EditedMessage
case c.u.ChannelPost != nil:
if c.u.ChannelPost.PinnedMessage != nil {
return c.u.ChannelPost.PinnedMessage.Message
return c.u.ChannelPost.PinnedMessage
}
return c.u.ChannelPost
case c.u.EditedChannelPost != nil:
@ -236,6 +239,13 @@ func (c *nativeContext) PreCheckoutQuery() *PreCheckoutQuery {
return c.u.PreCheckoutQuery
}
func (c *nativeContext) Payment() *Payment {
if c.u.Message == nil {
return nil
}
return c.u.Message.Payment
}
func (c *nativeContext) ChatMember() *ChatMemberUpdate {
switch {
case c.u.ChatMember != nil:
@ -260,7 +270,11 @@ func (c *nativeContext) PollAnswer() *PollAnswer {
}
func (c *nativeContext) Migration() (int64, int64) {
return c.u.Message.MigrateFrom, c.u.Message.MigrateTo
m := c.u.Message
if m == nil {
return 0, 0
}
return m.MigrateFrom, m.MigrateTo
}
func (c *nativeContext) Topic() *Topic {
@ -279,6 +293,14 @@ func (c *nativeContext) Topic() *Topic {
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:
@ -301,9 +323,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 {
@ -354,7 +383,11 @@ func (c *nativeContext) Entities() Entities {
func (c *nativeContext) Data() string {
switch {
case c.u.Message != nil:
return c.u.Message.Payload
m := c.u.Message
if m.Payment != nil {
return m.Payment.Payload
}
return m.Payload
case c.u.Callback != nil:
return c.u.Callback.Data
case c.u.Query != nil:
@ -371,9 +404,12 @@ func (c *nativeContext) Data() string {
}
func (c *nativeContext) Args() []string {
m := c.u.Message
switch {
case c.u.Message != nil:
payload := strings.Trim(c.u.Message.Payload, " ")
case m != nil && m.Payment != nil:
return strings.Split(m.Payment.Payload, "|")
case m != nil:
payload := strings.Trim(m.Payload, " ")
if payload != "" {
return strings.Fields(payload)
}
@ -471,7 +507,9 @@ func (c *nativeContext) Delete() error {
func (c *nativeContext) DeleteAfter(d time.Duration) *time.Timer {
return time.AfterFunc(d, func() {
if err := c.Delete(); err != nil {
c.b.OnError(err, c)
if b, ok := c.b.(*Bot); ok {
b.OnError(err, c)
}
}
})
}
@ -523,6 +561,7 @@ func (c *nativeContext) Set(key string, value interface{}) {
if c.store == nil {
c.store = make(map[string]interface{})
}
c.store[key] = value
}

@ -20,6 +20,10 @@ type Game struct {
Animation *Animation `json:"animation"`
}
// CallbackGame is a placeholder, currently holds no information
type CallbackGame struct {
}
// GameHighScore object represents one row
// of the high scores table for a game.
type GameHighScore struct {
@ -40,7 +44,6 @@ type GameHighScore struct {
// plus two of their closest neighbors on each side.
// Will also return the top three users
// if the user and his neighbors are not among them.
//
func (b *Bot) GameScores(user Recipient, msg Editable) ([]GameHighScore, error) {
msgID, chatID := msg.MessageSig()
@ -73,7 +76,6 @@ func (b *Bot) GameScores(user Recipient, msg Editable) ([]GameHighScore, error)
//
// If the message was sent by the bot, returns the edited Message,
// otherwise returns nil and ErrTrueResult.
//
func (b *Bot) SetGameScore(user Recipient, msg Editable, score GameHighScore) (*Message, error) {
msgID, chatID := msg.MessageSig()

@ -80,7 +80,7 @@ type QueryResponseButton struct {
// (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"`
Start string `json:"start_parameter,omitempty"`
}
// SwitchInlineQuery represents an inline button that switches the current

@ -213,6 +213,9 @@ type GifResult struct {
// If Cache != "", it'll be used instead
Cache string `json:"gif_file_id,omitempty"`
// (Optional) Pass True, if the caption must be shown above the message media
CaptionAbove bool `json:"show_caption_above_media,omitempty"`
}
// LocationResult represents a location on a map.
@ -260,6 +263,9 @@ type Mpeg4GifResult struct {
// If Cache != "", it'll be used instead
Cache string `json:"mpeg4_file_id,omitempty"`
// (Optional) Pass True, if the caption must be shown above the message media
CaptionAbove bool `json:"show_caption_above_media,omitempty"`
}
// PhotoResult represents a link to a photo.
@ -290,6 +296,9 @@ type PhotoResult struct {
// If Cache != "", it'll be used instead
Cache string `json:"photo_file_id,omitempty"`
// (Optional) Pass True, if the caption must be shown above the message media
CaptionAbove bool `json:"show_caption_above_media,omitempty"`
}
// VenueResult represents a venue.
@ -351,6 +360,9 @@ type VideoResult struct {
// If Cache != "", it'll be used instead
Cache string `json:"video_file_id,omitempty"`
// (Optional) Pass True, if the caption must be shown above the message media
CaptionAbove bool `json:"show_caption_above_media,omitempty"`
}
// VoiceResult represents a link to a voice recording in an .ogg

@ -17,7 +17,7 @@ type InputTextMessageContent struct {
ParseMode string `json:"parse_mode,omitempty"`
// (Optional) Link preview generation options for the message.
PreviewOptions *PreviewOptions `json:"link_preview_options"`
PreviewOptions *PreviewOptions `json:"link_preview_options,omitempty"`
}
func (input *InputTextMessageContent) IsInputMessageContent() bool {

@ -1,6 +1,6 @@
settings:
token_env: TOKEN
parse_mode: html
parse_mode: HTML
long_poller: {}
commands:

@ -70,7 +70,11 @@ type (
// New parses the given layout file.
func New(path string, funcs ...template.FuncMap) (*Layout, error) {
return NewFromFS(os.DirFS("."), path, funcs...)
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return rawNew(data, funcs...)
}
// NewFromFS parses the layout from the given fs.FS. It allows to read layout
@ -80,7 +84,10 @@ func NewFromFS(fsys fs.FS, path string, funcs ...template.FuncMap) (*Layout, err
if err != nil {
return nil, err
}
return rawNew(data, funcs...)
}
func rawNew(data []byte, funcs ...template.FuncMap) (*Layout, error) {
lt := Layout{
ctxs: make(map[tele.Context]string),
funcs: make(template.FuncMap),

@ -28,7 +28,7 @@ func TestLayout(t *testing.T) {
pref := lt.Settings()
assert.Equal(t, "TEST", pref.Token)
assert.Equal(t, "html", pref.ParseMode)
assert.Equal(t, tele.ModeHTML, pref.ParseMode)
assert.Equal(t, &tele.LongPoller{}, pref.Poller)
assert.Equal(t, pref, ltfs.Settings())
@ -132,4 +132,17 @@ func TestLayout(t *testing.T) {
Description: "Some description",
PreviewURL: "https://preview.picture",
}))
assert.Equal(t,
"This is an article.",
lt.TextLocale("en", "article_message"),
)
assert.Equal(t,
lt.TextLocale("en", "nested.example", "an example"),
"This is an example.",
)
assert.Equal(t,
lt.TextLocale("en", "nested.another.example", "another example"),
"This is another example.",
)
}

@ -1 +1,8 @@
article_message: This is an article.
nested:
example: |-
This is {{ . }}.
another:
example: |-
This is {{ . }}.

@ -246,18 +246,23 @@ func (lt *Layout) parseLocales(dir string) error {
return err
}
var texts map[string]string
var texts map[string]interface{}
if err := yaml.Unmarshal(data, &texts); err != nil {
return err
}
v := viper.New()
if err := v.MergeConfigMap(texts); err != nil {
return err
}
name := fi.Name()
name = strings.TrimSuffix(name, filepath.Ext(name))
tmpl := template.New(name).Funcs(lt.funcs)
for key, text := range texts {
_, err = tmpl.New(key).Parse(strings.Trim(text, "\r\n"))
if err != nil {
for _, key := range v.AllKeys() {
text := strings.Trim(v.GetString(key), "\r\n")
if _, err := tmpl.New(key).Parse(text); err != nil {
return err
}
}

@ -255,13 +255,28 @@ type ReplyRecipient struct {
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
RequestTitle *bool `json:"request_title,omitempty"` // chat only, optional
RequestName *bool `json:"request_name,omitempty"` // user only, optional
RequestUsername *bool `json:"request_username,omitempty"` // user only, optional
RequestPhoto *bool `json:"request_photo,omitempty"` // user only, optional
}
// RecipientShared combines both UserShared and ChatShared objects.
type RecipientShared struct {
ID int32 `json:"request_id"`
ID int32 `json:"request_id"` // chat, users
ChatID int64 `json:"chat_id"` // chat only
Title string `json:"title"` // chat only
Username string `json:"username"` // chat only
Photo *Photo `json:"photo"` // chat only
Users []struct {
UserID int64 `json:"user_id"`
ChatID int64 `json:"chat_id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
Photo *Photo `json:"photo"`
} `json:"users"` // users only
}
// InlineButton represents a button displayed in the message.
@ -280,6 +295,8 @@ type InlineButton struct {
InlineQueryChosenChat *SwitchInlineQuery `json:"switch_inline_query_chosen_chat,omitempty"`
Login *Login `json:"login_url,omitempty"`
WebApp *WebApp `json:"web_app,omitempty"`
CallbackGame *CallbackGame `json:"callback_game,omitempty"`
Pay bool `json:"pay,omitempty"`
}
// MarshalJSON implements json.Marshaler interface.

@ -2,6 +2,7 @@ package telebot
import (
"encoding/json"
"math"
)
// Media is a generic type for all kinds of media that includes File.
@ -29,7 +30,8 @@ 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"`
CaptionAbove bool `json:"show_caption_above_media,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
}
// Inputtable is a generic type for all kinds of media you
@ -67,9 +69,12 @@ func (a Album) SetCaption(caption string) {
type Photo struct {
File
// (Optional)
Width int `json:"width"`
Height int `json:"height"`
Caption string `json:"caption,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
CaptionAbove bool `json:"show_caption_above_media,omitempty"`
}
type photoSize struct {
@ -92,7 +97,13 @@ func (p *Photo) InputMedia() InputMedia {
return InputMedia{
Type: p.MediaType(),
Caption: p.Caption,
HasSpoiler: p.HasSpoiler,
CaptionAbove: p.CaptionAbove,
}
}
func (p *Photo) Paid() bool {
return true
}
// UnmarshalJSON is custom unmarshaller required to abstract
@ -119,6 +130,7 @@ func (p *Photo) UnmarshalJSON(data []byte) error {
p.File = hq.File
p.Width = hq.Width
p.Height = hq.Height
p.Caption = hq.Caption
return nil
}
@ -155,6 +167,7 @@ func (a *Audio) InputMedia() InputMedia {
Title: a.Title,
Performer: a.Performer,
}
}
// Document object represents a general file (as opposed to Photo or Audio).
@ -201,6 +214,8 @@ type Video struct {
Streaming bool `json:"supports_streaming,omitempty"`
MIME string `json:"mime_type,omitempty"`
FileName string `json:"file_name,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
CaptionAbove bool `json:"show_caption_above_media,omitempty"`
}
func (v *Video) MediaType() string {
@ -220,9 +235,15 @@ func (v *Video) InputMedia() InputMedia {
Height: v.Height,
Duration: v.Duration,
Streaming: v.Streaming,
HasSpoiler: v.HasSpoiler,
CaptionAbove: v.CaptionAbove,
}
}
func (v *Video) Paid() bool {
return true
}
// Animation object represents a animation file.
type Animation struct {
File
@ -236,6 +257,8 @@ type Animation struct {
Thumbnail *Photo `json:"thumbnail,omitempty"`
MIME string `json:"mime_type,omitempty"`
FileName string `json:"file_name,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
CaptionAbove bool `json:"show_caption_above_media,omitempty"`
}
func (a *Animation) MediaType() string {
@ -254,6 +277,8 @@ func (a *Animation) InputMedia() InputMedia {
Width: a.Width,
Height: a.Height,
Duration: a.Duration,
HasSpoiler: a.HasSpoiler,
CaptionAbove: a.CaptionAbove,
}
}
@ -308,7 +333,7 @@ type Sticker struct {
SetName string `json:"set_name"`
PremiumAnimation *File `json:"premium_animation"`
MaskPosition *MaskPosition `json:"mask_position"`
CustomEmoji string `json:"custom_emoji_id"`
CustomEmojiID string `json:"custom_emoji_id"`
Repaint bool `json:"needs_repainting"`
}
@ -328,8 +353,13 @@ type Contact struct {
// (Optional)
LastName string `json:"last_name"`
UserID int64 `json:"user_id,omitempty"`
VCard string `json:"vcard,omitempty"`
}
// LiveForever is an alias for math.MaxInt32.
// Use it for LivePeriod of the Location.
const LiveForever = math.MaxInt32
// Location object represents geographic position.
type Location struct {
Lat float32 `json:"latitude"`
@ -341,6 +371,10 @@ type Location struct {
// Period in seconds for which the location will be updated
// (see Live Locations, should be between 60 and 86400.)
LivePeriod int `json:"live_period,omitempty"`
// (Optional) Unique identifier of the business connection
// on behalf of which the message to be edited was sent
BusinessConnectionID string `json:"business_connection_id,omitempty"`
}
// Venue object represents a venue location with name, address and
@ -375,3 +409,29 @@ var (
Slot = &Dice{Type: "🎰"}
Bowl = &Dice{Type: "🎳"}
)
// PaidInputtable is a generic type for all kinds of media you
// can put into an album that are paid.
type PaidInputtable interface {
Inputtable
// Paid shows if the media is paid.
Paid() bool
}
// PaidAlbum lets you group multiple paid media into a single message.
type PaidAlbum []PaidInputtable
type PaidMedias struct {
Stars int `json:"star_count"`
PaidMedia []PaidMedia `json:"paid_media"`
}
type PaidMedia struct {
Type string `json:"type"`
Photo *Photo `json:"photo"` // photo
Video *Video `json:"video"` // video
Width int `json:"width"` // preview only
Height int `json:"height"` // preview only
Duration int `json:"duration"` // preview only
}

@ -1,7 +1,6 @@
package telebot
import (
"encoding/json"
"strconv"
"time"
"unicode/utf16"
@ -65,7 +64,7 @@ type Message struct {
// (Optional) Information about the message that is being replied to,
// which may come from another chat or forum topic.
ExternalReplyInfo *ExternalReplyInfo `json:"external_reply"`
ExternalReply *ExternalReply `json:"external_reply"`
// (Optional) For replies that quote part of the original message,
// the quoted part of the message.
@ -86,6 +85,10 @@ type Message struct {
// (Optional) Message can't be forwarded.
Protected bool `json:"has_protected_content,omitempty"`
// (Optional) True, if the message was sent by an implicit action,
// for example, as an away or a greeting business message, or as a scheduled message
FromOffline bool `json:"is_from_offline,omitempty"`
// AlbumID is the unique identifier of a media message group
// this message belongs to.
AlbumID string `json:"media_group_id"`
@ -105,9 +108,12 @@ type Message struct {
// etc. that appear in the text.
Entities Entities `json:"entities,omitempty"`
// (Optional) ReactionOptions 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"`
// (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"`
// (Optional) Unique identifier of the message effect added to the message
EffectID string `json:"effect_id"`
// Some messages containing media, may as well have a caption.
Caption string `json:"caption,omitempty"`
@ -122,6 +128,9 @@ type Message struct {
// For a general file, information about it.
Document *Document `json:"document"`
// Message contains paid media; information about the paid media
PaidMedia PaidMedias `json:"paid_media"`
// For a photo, all available sizes (thumbnails).
Photo *Photo `json:"photo"`
@ -137,7 +146,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.
@ -170,6 +179,16 @@ type Message struct {
// (Optional) Service message: a giveaway without public winners was completed.
GiveawayCompleted *GiveawayCompleted `json:"giveaway_completed"`
// (Optional) Unique identifier of the business connection from which the message
// was received. If non-empty, the message belongs to a chat of the corresponding
// business account that is independent from any potential bot chat which might
// share the same identifier.
BusinessConnectionID string `json:"business_connection_id"`
// (Optional) The bot that actually sent the message on behalf of the business account.
// Available only for outgoing messages sent on behalf of the connected business account.
BusinessBot *User `json:"sender_business_bot"`
// For a service message, represents a user,
// that just got added to chat, this message came from.
//
@ -255,7 +274,7 @@ type Message struct {
// Specified message was pinned. Note that the Message object
// in this field will not contain further ReplyTo fields even
// if it is itself a reply.
PinnedMessage *InaccessibleMessage `json:"pinned_message"`
PinnedMessage *Message `json:"pinned_message"`
// Message is an invoice for a payment.
Invoice *Invoice `json:"invoice"`
@ -263,6 +282,9 @@ type Message struct {
// Message is a service message about a successful payment.
Payment *Payment `json:"successful_payment"`
// Message is a service message about a refunded payment, information about the payment.
RefundedPayment *RefundedPayment `json:"refunded_payment"`
// For a service message, a user was shared with the bot.
UserShared *RecipientShared `json:"users_shared,omitempty"`
@ -300,9 +322,12 @@ type Message struct {
// Service message: user boosted the chat.
BoostAdded *BoostAdded `json:"boost_added"`
// Service message: chat background set
ChatBackground ChatBackground `json:"chat_background_set"`
// If the sender of the message boosted the chat, the number of boosts
// added by the user.
SenderBoostCount int `json:"sender_boost_count"`
SenderBoosts int `json:"sender_boost_count"`
// Service message: forum topic created
TopicCreated *Topic `json:"forum_topic_created,omitempty"`
@ -325,6 +350,9 @@ type Message struct {
// Service message: represents spoiler information about the message.
HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"`
// (Optional) Pass True, if the caption must be shown above the message media
CaptionAbove bool `json:"show_caption_above_media"`
// Service message: the user allowed the bot added to the attachment menu to write messages
WriteAccessAllowed *WriteAccessAllowed `json:"write_access_allowed,omitempty"`
}
@ -353,7 +381,7 @@ type MessageEntity struct {
Language string `json:"language,omitempty"`
// (Optional) For EntityCustomEmoji entity type only.
CustomEmoji string `json:"custom_emoji_id"`
CustomEmojiID string `json:"custom_emoji_id"`
}
// EntityType is a MessageEntity type.
@ -378,9 +406,10 @@ const (
EntitySpoiler EntityType = "spoiler"
EntityCustomEmoji EntityType = "custom_emoji"
EntityBlockquote EntityType = "blockquote"
EntityEBlockquote EntityType = "expandable_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
@ -396,6 +425,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
@ -504,31 +538,6 @@ func (m *Message) Media() Media {
}
}
// InaccessibleMessage describes a message that was deleted or is otherwise
// inaccessible to the bot. An instance of MaybeInaccessibleMessage object.
type InaccessibleMessage struct {
// A message that can be inaccessible to the bot.
*Message
// Chat the message belonged to.
Chat *Chat `json:"chat"`
// Unique message identifier inside the chat.
MessageID int `json:"message_id"`
// Always 0. The field can be used to differentiate regular and
// inaccessible messages.
DateUnixtime int64 `json:"date"`
}
func (im *InaccessibleMessage) MessageSig() (string, int64) {
return strconv.Itoa(im.MessageID), im.Chat.ID
}
func (im *InaccessibleMessage) Time() time.Time {
return time.Unix(im.DateUnixtime, 0)
}
// 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.
@ -572,7 +581,7 @@ type MessageReactionCount struct {
DateUnixtime int64 `json:"date"`
// List of reactions that are present on the message.
Reactions *ReactionCount `json:"reactions"`
Reactions []*ReactionCount `json:"reactions"`
}
// Time returns the moment of change in local time.
@ -633,9 +642,9 @@ func (mo *MessageOrigin) Time() time.Time {
return time.Unix(mo.DateUnixtime, 0)
}
// ExternalReplyInfo contains information about a message that is being replied to,
// ExternalReply contains information about a message that is being replied to,
// which may come from another chat or forum topic.
type ExternalReplyInfo struct {
type ExternalReply struct {
// Origin of the message replied to by the given message.
Origin *MessageOrigin `json:"origin"`
@ -647,7 +656,7 @@ type ExternalReplyInfo struct {
// 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,
// (Optional) PreviewOptions used for link preview generation for the original message,
// if it is a text message.
PreviewOptions *PreviewOptions `json:"link_preview_options"`
@ -678,9 +687,6 @@ type ExternalReplyInfo struct {
// (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"`
@ -707,6 +713,12 @@ type ExternalReplyInfo struct {
// (Optional) A giveaway with public winners was completed.
GiveawayWinners *GiveawayWinners `json:"giveaway_winners"`
// Message contains paid media; information about the paid media
PaidMedia PaidMedias `json:"paid_media"`
// (Optional) True, if the message media is covered by a spoiler animation.
HasMediaSpoiler bool `json:"has_media_spoiler"`
}
// ReplyParams describes reply parameters for the message that is being sent.
@ -740,33 +752,3 @@ type ReplyParams struct {
// (Optional) Position of the quote in the original message in UTF-16 code units.
QuotePosition int `json:"quote_position"`
}
// 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
}

@ -2,6 +2,7 @@ package middleware
import (
"errors"
"log"
tele "gopkg.in/telebot.v3"
)
@ -32,26 +33,30 @@ 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 if b, ok := c.Bot().(*tele.Bot); ok {
f = b.OnError
} else {
f = func(err error) {
c.Bot().OnError(err, nil)
f = func(err error, _ tele.Context) {
log.Println("telebot/middleware/recover:", err)
}
}
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")
}

@ -86,6 +86,12 @@ type SendOptions struct {
// ReplyParams Describes the message to reply to
ReplyParams *ReplyParams
// Unique identifier of the business connection
BusinessConnectionID string
// Unique identifier of the message effect to be added to the message; for private chats only
EffectID string
}
func (og *SendOptions) copy() *SendOptions {
@ -96,8 +102,10 @@ func (og *SendOptions) copy() *SendOptions {
return &cp
}
func extractOptions(how []interface{}) *SendOptions {
opts := &SendOptions{}
func (b *Bot) extractOptions(how []interface{}) *SendOptions {
opts := &SendOptions{
ParseMode: b.parseMode,
}
for _, prop := range how {
switch opt := prop.(type) {
@ -150,10 +158,6 @@ func extractOptions(how []interface{}) *SendOptions {
}
func (b *Bot) embedSendOptions(params map[string]string, opt *SendOptions) {
if b.parseMode != ModeDefault {
params["parse_mode"] = b.parseMode
}
if opt == nil {
return
}
@ -204,7 +208,15 @@ func (b *Bot) embedSendOptions(params map[string]string, opt *SendOptions) {
}
if opt.HasSpoiler {
params["spoiler"] = "true"
params["has_spoiler"] = "true"
}
if opt.BusinessConnectionID != "" {
params["business_connection_id"] = opt.BusinessConnectionID
}
if opt.EffectID != "" {
params["message_effect_id"] = opt.EffectID
}
}

@ -6,6 +6,9 @@ import (
"strconv"
)
// Stars is a provider token for Telegram Stars.
const Stars = "XTR"
// ShippingQuery contains information about an incoming shipping query.
type ShippingQuery struct {
Sender *User `json:"from"`
@ -42,6 +45,14 @@ type Payment struct {
ProviderChargeID string `json:"provider_payment_charge_id"`
}
type RefundedPayment struct {
Currency string `json:"currency"`
Total int `json:"total_amount"`
Payload string `json:"invoice_payload"`
TelegramChargeID string `json:"telegram_payment_charge_id"`
ProviderChargeID string `json:"provider_payment_charge_id"`
}
// PreCheckoutQuery contains information about an incoming pre-checkout query.
type PreCheckoutQuery struct {
Sender *User `json:"from"`
@ -186,3 +197,18 @@ func (b *Bot) CreateInvoiceLink(i Invoice) (string, error) {
}
return resp.Result, nil
}
// RefundStars returns a successful payment in Telegram Stars.
func (b *Bot) RefundStars(to Recipient, chargeID string) error {
params := map[string]string{
"user_id": to.Recipient(),
"telegram_payment_charge_id": chargeID,
}
_, err := b.Raw("refundStarPayment", params)
if err != nil {
return err
}
return nil
}

@ -29,7 +29,9 @@ type Poll struct {
MultipleAnswers bool `json:"allows_multiple_answers,omitempty"`
Explanation string `json:"explanation,omitempty"`
ParseMode ParseMode `json:"explanation_parse_mode,omitempty"`
Entities []MessageEntity `json:"explanation_entities"`
Entities []MessageEntity `json:"explanation_entities,omitempty"`
QuestionParseMode string `json:"question_parse_mode,omitempty"`
QuestionEntities []MessageEntity `json:"question_entities,omitempty"`
// True by default, shouldn't be omitted.
Anonymous bool `json:"is_anonymous"`
@ -43,6 +45,8 @@ type Poll struct {
type PollOption struct {
Text string `json:"text"`
VoterCount int `json:"voter_count"`
ParseMode ParseMode `json:"text_parse_mode,omitempty"`
Entities []MessageEntity `json:"text_entities,omitempty"`
}
// PollAnswer represents an answer of a user in a non-anonymous poll.

@ -1,7 +1,13 @@
package telebot
// EmojiType defines emoji types.
type EmojiType = string
import (
"encoding/json"
)
const (
ReactionTypeEmoji = "emoji"
ReactionTypeCustomEmoji = "custom_emoji"
)
// Reaction describes the type of reaction.
// Describes an instance of ReactionTypeCustomEmoji and ReactionTypeEmoji.
@ -10,10 +16,10 @@ type Reaction struct {
Type string `json:"type"`
// Reaction emoji.
Emoji EmojiType `json:"emoji,omitempty"`
Emoji string `json:"emoji,omitempty"`
// Custom emoji identifier.
CustomEmoji string `json:"custom_emoji_id,omitempty"`
CustomEmojiID string `json:"custom_emoji_id,omitempty"`
}
// ReactionCount represents a reaction added to a message along
@ -26,11 +32,36 @@ type ReactionCount struct {
Count int `json:"total_count"`
}
// ReactionOptions represents an object of reaction options.
type ReactionOptions struct {
// Reactions represents an object of reaction options.
type Reactions 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, r Reactions) error {
if to == nil {
return ErrBadRecipient
}
msgID, _ := msg.MessageSig()
params := map[string]string{
"chat_id": to.Recipient(),
"message_id": msgID,
}
data, _ := json.Marshal(r.Reactions)
params["reaction"] = string(data)
if r.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.Reactions {
return tele.Reactions{Reactions: r}
}
// Currently available emojis.
var (
ThumbUp = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👍"}
ThumbDown = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👎"}
Heart = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "❤"}
Fire = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🔥"}
HeartEyes = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😍"}
ClappingHands = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👏"}
GrinningFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😁"}
ThinkingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤔"}
ExplodingHead = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤯"}
ScreamingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😱"}
SwearingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤬"}
CryingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😢"}
PartyPopper = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🎉"}
StarStruck = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤩"}
VomitingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤮"}
PileOfPoo = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💩"}
PrayingHands = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🙏"}
OkHand = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👌"}
DoveOfPeace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🕊"}
ClownFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤡"}
YawningFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🥱"}
WoozyFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🥴"}
Whale = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🐳"}
HeartOnFire = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "❤‍🔥"}
MoonFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🌚"}
HotDog = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🌭"}
HundredPoints = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💯"}
RollingOnTheFloorLaughing = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤣"}
Lightning = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "⚡"}
Banana = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🍌"}
Trophy = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🏆"}
BrokenHeart = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💔"}
FaceWithRaisedEyebrow = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤨"}
NeutralFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😐"}
Strawberry = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🍓"}
Champagne = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🍾"}
KissMark = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💋"}
MiddleFinger = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🖕"}
EvilFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😈"}
SleepingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😴"}
LoudlyCryingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😭"}
NerdFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤓"}
Ghost = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👻"}
Engineer = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👨‍💻"}
Eyes = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👀"}
JackOLantern = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🎃"}
NoMonkey = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🙈"}
SmilingFaceWithHalo = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😇"}
FearfulFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😨"}
Handshake = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤝"}
WritingHand = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "✍"}
HuggingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤗"}
Brain = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🫡"}
SantaClaus = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🎅"}
ChristmasTree = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🎄"}
Snowman = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "☃"}
NailPolish = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💅"}
ZanyFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤪"}
Moai = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🗿"}
Cool = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🆒"}
HeartWithArrow = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💘"}
HearMonkey = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🙉"}
Unicorn = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🦄"}
FaceBlowingKiss = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😘"}
Pill = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💊"}
SpeaklessMonkey = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🙊"}
Sunglasses = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😎"}
AlienMonster = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👾"}
ManShrugging = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤷‍♂️"}
PersonShrugging = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤷"}
WomanShrugging = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤷‍♀️"}
PoutingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😡"}
)

@ -1,81 +0,0 @@
package react
import "gopkg.in/telebot.v3"
type Reaction = telebot.Reaction
// 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: "😡"}
)

@ -352,12 +352,7 @@ func (p *Poll) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
}
b.embedSendOptions(params, opt)
var options []string
for _, o := range p.Options {
options = append(options, o.Text)
}
opts, _ := json.Marshal(options)
opts, _ := json.Marshal(p.Options)
params["options"] = string(opts)
data, err := b.Raw("sendPoll", params)

@ -0,0 +1,73 @@
package telebot
import "time"
type TransactionType = string
const (
TransactionTypeUser TransactionType = "user"
TransactionTypeFragment TransactionType = "fragment"
TransactionPartnerTelegramAds TransactionType = "telegram_ads"
TransactionTypeOther TransactionType = "other"
)
type RevenueState = string
const (
RevenueStatePending RevenueState = "pending"
RevenueStateSucceeded RevenueState = "succeeded"
RevenueStateFailed RevenueState = "failed"
)
type StarTransaction struct {
// Unique identifier of the transaction. Coincides with the identifier of the
// original transaction for refund transactions. Coincides with
// SuccessfulPayment.telegram_payment_charge_id for successful incoming
// payments from users.
ID string `json:"id"`
// Number of Telegram Stars transferred by the transaction
Amount int `json:"amount"`
// Date the transaction was created in Unix time
Unixtime int64 `json:"date"`
// (Optional) Source of an incoming transaction (e.g., a user purchasing goods
// or services, Fragment refunding a failed withdrawal). Only for incoming transactions
Source TransactionPartner `json:"source"`
// (Optional) Receiver of an outgoing transaction (e.g., a user for a purchase
// refund, Fragment for a withdrawal). Only for outgoing transactions
Receiver TransactionPartner `json:"receiver"`
}
type TransactionPartner struct {
// Type of the state
Type TransactionType `json:"type"`
User *User `json:"user,omitempty"`
Payload string `json:"invoice_payload"`
// (Optional) State of the transaction if the transaction is outgoing$$
Withdrawal RevenueWithdrawal `json:"withdrawal_state,omitempty"`
}
type RevenueWithdrawal struct {
// Type of the state
Type RevenueState `json:"type"`
// Date the withdrawal was completed in Unix time
Unixtime int `json:"date,omitempty"`
// An HTTPS URL that can be used to see transaction details
URL string `json:"url,omitempty"`
}
// Time returns the date of the transaction.
func (c *StarTransaction) Time() time.Time {
return time.Unix(c.Unixtime, 0)
}
// Time returns the date of the withdrawal.
func (s *RevenueWithdrawal) Time() time.Time {
return time.Unix(int64(s.Unixtime), 0)
}

@ -38,8 +38,6 @@ type StickerSet struct {
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"`
@ -55,6 +53,7 @@ type StickerSet struct {
type InputSticker struct {
File
Sticker string `json:"sticker"`
Format string `json:"format"`
MaskPosition *MaskPosition `json:"mask_position"`
Emojis []string `json:"emoji_list"`
Keywords []string `json:"keywords"`
@ -123,7 +122,6 @@ func (b *Bot) CreateStickerSet(of Recipient, set *StickerSet) error {
"user_id": of.Recipient(),
"name": set.Name,
"title": set.Title,
"sticker_format": set.Format,
"stickers": string(data),
}
if set.Type != "" {
@ -198,6 +196,7 @@ func (b *Bot) SetStickerSetThumb(of Recipient, set *StickerSet) error {
params := map[string]string{
"user_id": of.Recipient(),
"name": set.Name,
"format": set.Format,
"thumbnail": repr,
}
@ -304,3 +303,29 @@ func (b *Bot) SetCustomEmojiStickerSetThumb(name, id string) error {
_, err := b.Raw("setCustomEmojiStickerSetThumbnail", params)
return err
}
// ReplaceStickerInSet returns True on success, if existing sticker was replaced with a new one.
func (b *Bot) ReplaceStickerInSet(of Recipient, stickerSet, oldSticker string, sticker InputSticker) (bool, error) {
files := make(map[string]File)
repr := sticker.File.process("0", files)
if repr == "" {
return false, errors.New("telebot: sticker does not exist")
}
sticker.Sticker = repr
data, err := json.Marshal(sticker)
if err != nil {
return false, err
}
params := map[string]string{
"user_id": of.Recipient(),
"name": stickerSet,
"old_sticker": oldSticker,
"sticker": string(data),
}
_, err = b.sendFiles("replaceStickerInSet", files, params)
return true, err
}

@ -45,6 +45,8 @@ const DefaultApiURL = "https://api.telegram.org"
const (
// Basic message handlers.
OnText = "\atext"
OnForward = "\aforward"
OnReply = "\areply"
OnEdited = "\aedited"
OnPhoto = "\aphoto"
OnAudio = "\aaudio"
@ -60,6 +62,7 @@ const (
OnDice = "\adice"
OnInvoice = "\ainvoice"
OnPayment = "\apayment"
OnRefund = "\arefund"
OnGame = "\agame"
OnPoll = "\apoll"
OnPollAnswer = "\apoll_answer"
@ -111,7 +114,12 @@ const (
OnVideoChatScheduled = "\avideo_chat_scheduled"
OnBoost = "\aboost_updated"
onBoostRemoved = "\aboost_removed"
OnBoostRemoved = "\aboost_removed"
OnBusinessConnection = "\abusiness_connection"
OnBusinessMessage = "\abusiness_message"
OnEditedBusinessMessage = "\aedited_business_message"
OnDeletedBusinessMessages = "\adeleted_business_messages"
)
// ChatAction is a client-side status indicating bot activity.

@ -8,7 +8,7 @@ import (
type Topic struct {
Name string `json:"name"`
IconColor int `json:"icon_color"`
IconCustomEmoji string `json:"icon_custom_emoji_id"`
IconCustomEmojiID string `json:"icon_custom_emoji_id"`
ThreadID int `json:"message_thread_id"`
}
@ -22,8 +22,8 @@ func (b *Bot) CreateTopic(chat *Chat, topic *Topic) (*Topic, error) {
if topic.IconColor != 0 {
params["icon_color"] = strconv.Itoa(topic.IconColor)
}
if topic.IconCustomEmoji != "" {
params["icon_custom_emoji_id"] = topic.IconCustomEmoji
if topic.IconCustomEmojiID != "" {
params["icon_custom_emoji_id"] = topic.IconCustomEmojiID
}
data, err := b.Raw("createForumTopic", params)
@ -50,8 +50,8 @@ func (b *Bot) EditTopic(chat *Chat, topic *Topic) error {
if topic.Name != "" {
params["name"] = topic.Name
}
if topic.IconCustomEmoji != "" {
params["icon_custom_emoji_id"] = topic.IconCustomEmoji
if topic.IconCustomEmojiID != "" {
params["icon_custom_emoji_id"] = topic.IconCustomEmojiID
}
_, err := b.Raw("editForumTopic", params)

@ -24,12 +24,22 @@ type Update struct {
ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"`
Boost *BoostUpdated `json:"chat_boost"`
BoostRemoved *BoostRemoved `json:"removed_chat_boost"`
BusinessConnection *BusinessConnection `json:"business_connection"`
BusinessMessage *Message `json:"business_message"`
EditedBusinessMessage *Message `json:"edited_business_message"`
DeletedBusinessMessages *BusinessMessagesDeleted `json:"deleted_business_messages"`
}
// ProcessUpdate processes a single incoming update.
// A started bot calls this function automatically.
func (b *Bot) ProcessUpdate(u Update) {
c := b.NewContext(u)
b.ProcessContext(b.NewContext(u))
}
// ProcessContext processes the given context.
// A started bot calls this function automatically.
func (b *Bot) ProcessContext(c Context) {
u := c.Update()
if u.Message != nil {
m := u.Message
@ -39,6 +49,10 @@ func (b *Bot) ProcessUpdate(u Update) {
return
}
if m.Origin != nil {
b.handle(OnForward, c)
}
// Commands
if m.Text != "" {
// Filtering malicious messages
@ -66,6 +80,10 @@ func (b *Bot) ProcessUpdate(u Update) {
return
}
if m.ReplyTo != nil {
b.handle(OnReply, c)
}
b.handle(OnText, c)
return
}
@ -102,7 +120,10 @@ func (b *Bot) ProcessUpdate(u Update) {
b.handle(OnPayment, c)
return
}
if m.RefundedPayment != nil {
b.handle(OnRefund, c)
return
}
if m.TopicCreated != nil {
b.handle(OnTopicCreated, c)
return
@ -292,7 +313,6 @@ func (b *Bot) ProcessUpdate(u Update) {
b.handle(OnPoll, c)
return
}
if u.PollAnswer != nil {
b.handle(OnPollAnswer, c)
return
@ -302,12 +322,10 @@ func (b *Bot) ProcessUpdate(u Update) {
b.handle(OnMyChatMember, c)
return
}
if u.ChatMember != nil {
b.handle(OnChatMember, c)
return
}
if u.ChatJoinRequest != nil {
b.handle(OnChatJoinRequest, c)
return
@ -317,9 +335,25 @@ func (b *Bot) ProcessUpdate(u Update) {
b.handle(OnBoost, c)
return
}
if u.BoostRemoved != nil {
b.handle(onBoostRemoved, c)
b.handle(OnBoostRemoved, c)
return
}
if u.BusinessConnection != nil {
b.handle(OnBusinessConnection, c)
return
}
if u.BusinessMessage != nil {
b.handle(OnBusinessMessage, c)
return
}
if u.EditedBusinessMessage != nil {
b.handle(OnEditedBusinessMessage, c)
return
}
if u.DeletedBusinessMessages != nil {
b.handle(OnDeletedBusinessMessages, c)
return
}
}

@ -38,6 +38,8 @@ type WebhookEndpoint struct {
// You can also leave the Listen field empty. In this case it is up to the caller to
// add the Webhook to a http-mux.
//
// If you want to ignore the automatic setWebhook call, you can set IgnoreSetWebhook to true.
//
type Webhook struct {
Listen string `json:"url"`
MaxConnections int `json:"max_connections"`
@ -45,6 +47,7 @@ type Webhook struct {
IP string `json:"ip_address"`
DropUpdates bool `json:"drop_pending_updates"`
SecretToken string `json:"secret_token"`
IgnoreSetWebhook bool `json:"ignore_set_web_hook"`
// (WebhookInfo)
HasCustomCert bool `json:"has_custom_certificate"`
@ -118,11 +121,14 @@ func (h *Webhook) getParams() map[string]string {
}
func (h *Webhook) Poll(b *Bot, dest chan Update, stop chan struct{}) {
// by default, the set webhook method will be called, to ignore it, set IgnoreSetWebhook to true
if !h.IgnoreSetWebhook {
if err := b.SetWebhook(h); err != nil {
b.OnError(err, nil)
close(stop)
return
}
}
// store the variables so the HTTP-handler can use 'em
h.dest = dest

Loading…
Cancel
Save