Merge branch 'tucnak:v3' into v3

pull/452/head
zry98 2 years ago committed by GitHub
commit 6cc14872e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,13 +0,0 @@
language: go
go:
- 1.13.x
install:
- go get -t -v
script:
- go test -coverprofile=coverage.txt -covermode=atomic
after_success:
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash <(curl -s https://codecov.io/bash); fi

@ -1,17 +1,18 @@
# Telebot
>"I never knew creating Telegram bots could be so _sexy_!"
[![GoDoc](https://godoc.org/gopkg.in/tucnak/telebot.v3?status.svg)](https://godoc.org/gopkg.in/tucnak/telebot.v3)
[![Travis](https://travis-ci.org/tucnak/telebot.svg?branch=v3)](https://travis-ci.org/tucnak/telebot)
[![codecov.io](https://codecov.io/gh/tucnak/telebot/coverage.svg?branch=develop)](https://codecov.io/gh/tucnak/telebot)
[![GoDoc](https://godoc.org/gopkg.in/telebot.v3?status.svg)](https://godoc.org/gopkg.in/telebot.v3)
[![codecov.io](https://codecov.io/gh/tucnak/telebot/coverage.svg?branch=v3)](https://codecov.io/gh/tucnak/telebot)
[![Discuss on Telegram](https://img.shields.io/badge/telegram-discuss-0088cc.svg)](https://t.me/go_telebot)
```bash
go get -u gopkg.in/tucnak/telebot.v3
go get -u gopkg.in/telebot.v3
```
* [Overview](#overview)
* [Getting Started](#getting-started)
- [Context](#context)
- [Middleware](#middleware)
- [Poller](#poller)
- [Commands](#commands)
- [Files](#files)
@ -27,7 +28,7 @@ go get -u gopkg.in/tucnak/telebot.v3
Telebot is a bot framework for [Telegram Bot API](https://core.telegram.org/bots/api).
This package provides the best of its kind API for command routing, inline query requests and keyboards, as well
as callbacks. Actually, I went a couple steps further, so instead of making a 1:1 API wrapper I chose to focus on
the beauty of API and performance. Some of the strong sides of telebot are:
the beauty of API and performance. Some strong sides of Telebot are:
* Real concise API
* Command routing
@ -35,39 +36,38 @@ the beauty of API and performance. Some of the strong sides of telebot are:
* Transparent File API
* Effortless bot callbacks
All the methods of telebot API are _extremely_ easy to memorize and get used to. Also, consider Telebot a
All the methods of Telebot API are _extremely_ easy to memorize and get used to. Also, consider Telebot a
highload-ready solution. I'll test and benchmark the most popular actions and if necessary, optimize
against them without sacrificing API quality.
# Getting Started
Let's take a look at the minimal telebot setup:
Let's take a look at the minimal Telebot setup:
```go
package main
import (
"log"
"os"
"time"
tele "gopkg.in/tucnak/telebot.v3"
tele "gopkg.in/telebot.v3"
)
func main() {
b, err := tele.NewBot(tele.Settings{
// You can also set custom API URL.
// If field is empty it equals to "https://api.telegram.org".
URL: "http://195.129.111.17:8012",
Token: "TOKEN_HERE",
pref := tele.Settings{
Token: os.Getenv("TOKEN"),
Poller: &tele.LongPoller{Timeout: 10 * time.Second},
})
}
b, err := tele.NewBot(pref)
if err != nil {
log.Fatal(err)
return
}
b.Handle("/hello", func(m tele.Context) error {
return m.Send("Hello")
b.Handle("/hello", func(c tele.Context) error {
return c.Send("Hello!")
})
b.Start()
@ -77,40 +77,97 @@ func main() {
Simple, innit? Telebot's routing system takes care of delivering updates
to their endpoints, so in order to get to handle any meaningful event,
all you got to do is just plug your function to one of the Telebot-provided
all you got to do is just plug your function into one of the Telebot-provided
endpoints. You can find the full list
[here](https://godoc.org/gopkg.in/tucnak/telebot.v3#pkg-constants).
There are dozens of supported endpoints (see package consts). Let me know
if you'd like to see some endpoint or endpoint ideas implemented. This system
is completely extensible, so I can introduce them without breaking
backwards compatibility.
## Context
Context is a special type that wraps a huge update structure and represents
the context of the current event. It provides several helpers, which allow
getting, for example, the chat that this update had been sent in, no matter
what kind of update this is.
```go
b, _ := tele.NewBot(settings)
b.Handle(tele.OnText, func(c tele.Context) error {
// All the text messages that weren't
// captured by existing handlers.
var (
user = c.Sender()
text = c.Text()
)
b.Handle(tele.OnText, func(m *tele.Message) {
// all the text messages that weren't
// captured by existing handlers
// Use full-fledged bot's functions
// only if you need a result:
msg, err := b.Send(user, text)
if err != nil {
return err
}
// Instead, prefer a context short-hand:
return c.Send(text)
})
b.Handle(tele.OnPhoto, func(m *tele.Message) {
// photos only
b.Handle(tele.OnChannelPost, func(c tele.Context) error {
// Channel posts only.
msg := c.Message()
})
b.Handle(tele.OnChannelPost, func (m *tele.Message) {
// channel posts only
b.Handle(tele.OnPhoto, func(c tele.Context) error {
// Photos only.
photo := c.Message().Photo
})
b.Handle(tele.OnQuery, func (q *tele.Query) {
// incoming inline queries
b.Handle(tele.OnQuery, func(c tele.Context) error {
// Incoming inline queries.
return c.Answer(...)
})
```
There's dozens of supported endpoints (see package consts). Let me know
if you'd like to see some endpoint or endpoint idea implemented. This system
is completely extensible, so I can introduce them without breaking
backwards-compatibility.
## Middleware
Telebot has a simple and recognizable way to set up middleware — chained functions with access to `Context`, called before the handler execution.
Import a `middleware` package to get some basic out-of-box middleware
implementations:
```go
import "gopkg.in/telebot.v3/middleware"
```
```go
// Global-scoped middleware:
b.Use(middleware.Logger())
b.Use(middleware.AutoRespond())
// Group-scoped middleware:
adminOnly := b.Group(middleware.Whitelist(adminIDs...))
adminOnly.Handle("/ban", onBan)
adminOnly.Handle("/kick", onKick)
// Handler-scoped middleware:
b.Handle(tele.OnText, onText, middleware.IgnoreVia())
```
Custom middleware example:
```go
// AutoResponder automatically responds to every callback update.
func AutoResponder(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
if c.Callback() != nil {
defer c.Respond()
}
return next(c) // continue execution chain
}
}
```
## Poller
Telebot doesn't really care how you provide it with incoming updates, as long
as you set it up with a Poller, or call ProcessUpdate for each update (see
[examples/awslambdaechobot](examples/awslambdaechobot)):
as you set it up with a Poller, or call ProcessUpdate for each update:
```go
// Poller is a provider of Updates.
@ -129,59 +186,35 @@ type Poller interface {
}
```
Telegram Bot API supports long polling and webhook integration. Poller means you
can plug telebot into whatever existing bot infrastructure (load balancers?) you
need, if you need to. Another great thing about pollers is that you can chain
them, making some sort of middleware:
```go
poller := &tele.LongPoller{Timeout: 15 * time.Second}
spamProtected := tele.NewMiddlewarePoller(poller, func(upd *tele.Update) bool {
if upd.Message == nil {
return true
}
if strings.Contains(upd.Message.Text, "spam") {
return false
}
return true
})
bot, _ := tele.NewBot(tele.Settings{
// ...
Poller: spamProtected,
})
// graceful shutdown
time.AfterFunc(N * time.Second, b.Stop)
// blocks until shutdown
bot.Start()
fmt.Println(poller.LastUpdateID) // 134237
```
## Commands
When handling commands, Telebot supports both direct (`/command`) and group-like
syntax (`/command@botname`) and will never deliver messages addressed to some
other bot, even if [privacy mode](https://core.telegram.org/bots#privacy-mode) is off.
For simplified deep-linking, telebot also extracts payload:
For simplified deep-linking, Telebot also extracts payload:
```go
// Command: /start <PAYLOAD>
b.Handle("/start", func(m *tele.Message) {
if !m.Private() {
return
}
b.Handle("/start", func(c tele.Context) error {
fmt.Println(c.Message().Payload) // <PAYLOAD>
})
```
fmt.Println(m.Payload) // <PAYLOAD>
For multiple arguments use:
```go
// Command: /tags <tag1> <tag2> <...>
b.Handle("/tags", func(c tele.Context) error {
tags := c.Args() // list of arguments splitted by a space
for _, tag := range tags {
// iterate through passed arguments
}
})
```
## Files
>Telegram allows files up to 20 MB in size.
>Telegram allows files up to 50 MB in size.
Telebot allows to both upload (from disk / by URL) and download (from Telegram)
and files in bot's scope. Also, sending any kind of media with a File created
Telebot allows to both upload (from disk or by URL) and download (from Telegram)
files in bot's scope. Also, sending any kind of media with a File created
from disk will upload the file to Telegram automatically:
```go
a := &tele.Audio{File: tele.FromDisk("file.ogg")}
@ -189,16 +222,16 @@ a := &tele.Audio{File: tele.FromDisk("file.ogg")}
fmt.Println(a.OnDisk()) // true
fmt.Println(a.InCloud()) // false
// Will upload the file from disk and send it to recipient
bot.Send(recipient, a)
// Will upload the file from disk and send it to the recipient
b.Send(recipient, a)
// Next time you'll be sending this very *Audio, Telebot won't
// re-upload the same file but rather utilize its Telegram FileID
bot.Send(otherRecipient, a)
b.Send(otherRecipient, a)
fmt.Println(a.OnDisk()) // true
fmt.Println(a.InCloud()) // true
fmt.Println(a.FileID) // <telegram file id: ABC-DEF1234ghIkl-zyx57W2v1u123ew11>
fmt.Println(a.FileID) // <Telegram file ID>
```
You might want to save certain `File`s in order to avoid re-uploading. Feel free
@ -208,7 +241,7 @@ data will ever be lost.
## Sendable
Send is undoubtedly the most important method in Telebot. `Send()` accepts a
`Recipient` (could be user, group or a channel) and a `Sendable`. Other types other than
the telebot-provided media types (`Photo`, `Audio`, `Video`, etc.) are `Sendable`.
the Telebot-provided media types (`Photo`, `Audio`, `Video`, etc.) are `Sendable`.
If you create composite types of your own, and they satisfy the `Sendable` interface,
Telebot will be able to send them out.
@ -257,12 +290,12 @@ b.Send(user, "text", tele.Silent, tele.NoPreview)
```
Full list of supported option-flags you can find
[here](https://github.com/tucnak/telebot/blob/v3/options.go#L9).
[here](https://pkg.go.dev/gopkg.in/tucnak/telebot.v3#Option).
## Editable
If you want to edit some existing message, you don't really need to store the
original `*Message` object. In fact, upon edit, Telegram only requires `chat_id`
and `message_id`. So you don't really need the Message as the whole. Also you
and `message_id`. So you don't really need the Message as a whole. Also, you
might want to store references to certain messages in the database, so I thought
it made sense for *any* Go struct to be editable as a Telegram message, to implement
`Editable`:
@ -285,7 +318,7 @@ type Editable interface {
```
For example, `Message` type is Editable. Here is the implementation of `StoredMessage`
type, provided by telebot:
type, provided by Telebot:
```go
// StoredMessage is an example struct suitable for being
// stored in the database as-is or being embedded into
@ -327,70 +360,56 @@ bot.EditCaption(m, "new caption")
## Keyboards
Telebot supports both kinds of keyboards Telegram provides: reply and inline
keyboards. Any button can also act as an endpoints for `Handle()`.
In `v2.2` we're introducing a little more convenient way in building keyboards.
The main goal is to avoid a lot of boilerplate and to make code clearer.
keyboards. Any button can also act as endpoints for `Handle()`.
```go
func main() {
b, _ := tele.NewBot(tele.Settings{...})
var (
// Universal markup builders.
menu = &tele.ReplyMarkup{ResizeKeyboard: true}
selector = &tele.ReplyMarkup{}
var (
// Universal markup builders.
menu = &ReplyMarkup{ResizeReplyKeyboard: true}
selector = &ReplyMarkup{}
// Reply buttons.
btnHelp = menu.Text(" Help")
btnSettings = menu.Text("⚙ Settings")
// Inline buttons.
//
// Pressing it will cause the client to
// send the bot a callback.
//
// Make sure Unique stays unique as per button kind,
// as it has to be for callback routing to work.
//
btnPrev = selector.Data("⬅", "prev", ...)
btnNext = selector.Data("➡", "next", ...)
)
menu.Reply(
menu.Row(btnHelp),
menu.Row(btnSettings),
)
selector.Inline(
selector.Row(btnPrev, btnNext),
)
// Reply buttons.
btnHelp = menu.Text(" Help")
btnSettings = menu.Text("⚙ Settings")
// Command: /start <PAYLOAD>
b.Handle("/start", func(m *tele.Message) {
if !m.Private() {
return
}
// Inline buttons.
//
// Pressing it will cause the client to
// send the bot a callback.
//
// Make sure Unique stays unique as per button kind
// since it's required for callback routing to work.
//
btnPrev = selector.Data("⬅", "prev", ...)
btnNext = selector.Data("➡", "next", ...)
)
b.Send(m.Sender, "Hello!", menu)
})
menu.Reply(
menu.Row(btnHelp),
menu.Row(btnSettings),
)
selector.Inline(
selector.Row(btnPrev, btnNext),
)
// On reply button pressed (message)
b.Handle(&btnHelp, func(m *tele.Message) {...})
b.Handle("/start", func(c tele.Context) error {
return c.Send("Hello!", menu)
})
// On inline button pressed (callback)
b.Handle(&btnPrev, func(c *tele.Callback) {
// ...
// Always respond!
b.Respond(c, &tele.CallbackResponse{...})
})
// On reply button pressed (message)
b.Handle(&btnHelp, func(c tele.Context) error {
return c.Edit("Here is some help: ...")
})
b.Start()
}
// On inline button pressed (callback)
b.Handle(&btnPrev, func(c tele.Context) error {
return c.Respond()
})
```
You can use markup constructor for every type of possible buttons:
You can use markup constructor for every type of possible button:
```go
r := &ReplyMarkup{}
r := b.NewMarkup()
// Reply buttons:
r.Text("Hello!")
@ -410,11 +429,11 @@ r.Login("Login", &tele.Login{...})
## Inline mode
So if you want to handle incoming inline queries you better plug the `tele.OnQuery`
endpoint and then use the `Answer()` method to send a list of inline queries
back. I think at the time of writing, telebot supports all of the provided result
types (but not the cached ones). This is how it looks like:
back. I think at the time of writing, Telebot supports all of the provided result
types (but not the cached ones). This is what it looks like:
```go
b.Handle(tele.OnQuery, func(q *tele.Query) {
b.Handle(tele.OnQuery, func(c tele.Context) error {
urls := []string{
"http://photo.jpg",
"http://photo2.jpg",
@ -423,10 +442,8 @@ b.Handle(tele.OnQuery, func(q *tele.Query) {
results := make(tele.Results, len(urls)) // []tele.Result
for i, url := range urls {
result := &tele.PhotoResult{
URL: url,
// required for photos
ThumbURL: url,
URL: url,
ThumbURL: url, // required for photos
}
results[i] = result
@ -434,14 +451,10 @@ b.Handle(tele.OnQuery, func(q *tele.Query) {
results[i].SetResultID(strconv.Itoa(i))
}
err := b.Answer(q, &tele.QueryResponse{
return c.Answer(&tele.QueryResponse{
Results: results,
CacheTime: 60, // a minute
})
if err != nil {
log.Println(err)
}
})
```
@ -452,16 +465,16 @@ of `QueryResponse`.
# Contributing
1. Fork it
2. Clone develop: `git clone -b develop https://github.com/tucnak/telebot`
3. Create your feature branch: `git checkout -b new-feature`
2. Clone v3: `git clone -b v3 https://github.com/tucnak/telebot`
3. Create your feature branch: `git checkout -b v3-feature`
4. Make changes and add them: `git add .`
5. Commit: `git commit -m "Add some feature"`
6. Push: `git push origin new-feature`
5. Commit: `git commit -m "add some feature"`
6. Push: `git push origin v3-feature`
7. Pull request
# Donate
I do coding for fun but I also try to search for interesting solutions and
I do coding for fun, but I also try to search for interesting solutions and
optimize them as much as possible.
If you feel like it's a good piece of software, I wouldn't mind a tip!

@ -11,6 +11,9 @@ type ChatInviteLink struct {
// The invite link.
InviteLink string `json:"invite_link"`
// Invite link name.
Name string `json:"name"`
// The creator of the link.
Creator *User `json:"creator"`
@ -21,12 +24,19 @@ type ChatInviteLink struct {
IsRevoked bool `json:"is_revoked"`
// (Optional) Point in time when the link will expire,
// use ExpireDate() to get time.Time
// use ExpireDate() to get time.Time.
ExpireUnixtime int64 `json:"expire_date,omitempty"`
// (Optional) Maximum number of users that can be members of
// the chat simultaneously.
MemberLimit int `json:"member_limit,omitempty"`
// (Optional) True, if users joining the chat via the link need to
// be approved by chat administrators. If True, member_limit can't be specified.
JoinRequest bool `json:"creates_join_request"`
// (Optional) Number of pending join requests created using this link.
PendingCount int `json:"pending_join_request_count"`
}
// ExpireDate returns the moment of the link expiration in local time.
@ -42,7 +52,7 @@ type ChatMemberUpdate struct {
// Sender which user the action was triggered.
Sender *User `json:"from"`
// Unixtime, use Date() to get time.Time
// Unixtime, use Date() to get time.Time.
Unixtime int64 `json:"date"`
// Previous information about the chat member.

@ -3,6 +3,7 @@ package telebot
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
@ -12,8 +13,6 @@ import (
"strconv"
"strings"
"time"
"github.com/pkg/errors"
)
// Raw lets you call any method of Bot API manually.
@ -73,7 +72,7 @@ func (b *Bot) sendFiles(method string, files map[string]File, params map[string]
case f.FileReader != nil:
rawFiles[name] = f.FileReader
default:
return nil, errors.Errorf("telebot: file for field %s doesn't exist", name)
return nil, fmt.Errorf("telebot: file for field %s doesn't exist", name)
}
}
@ -83,11 +82,12 @@ func (b *Bot) sendFiles(method string, files map[string]File, params map[string]
pipeReader, pipeWriter := io.Pipe()
writer := multipart.NewWriter(pipeWriter)
go func() {
defer pipeWriter.Close()
for field, file := range rawFiles {
if err := addFileToWriter(writer, params["file_name"], field, file); err != nil {
if err := addFileToWriter(writer, files[field].fileName, field, file); err != nil {
pipeWriter.CloseWithError(err)
return
}
@ -139,7 +139,7 @@ func addFileToWriter(writer *multipart.Writer, filename, field string, file inte
defer f.Close()
reader = f
} else {
return errors.Errorf("telebot: file for field %v should be an io.ReadCloser or string", field)
return fmt.Errorf("telebot: file for field %v should be io.ReadCloser or string", field)
}
part, err := writer.CreateFormFile(field, filename)
@ -166,19 +166,20 @@ func (b *Bot) sendText(to Recipient, text string, opt *SendOptions) (*Message, e
return extractMessage(data)
}
func (b *Bot) sendObject(f *File, what string, params map[string]string, files map[string]File) (*Message, error) {
sendWhat := "send" + strings.Title(what)
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 what == "videoNote" {
what = "video_note"
if kind == "videoNote" {
kind = "video_note"
}
sendFiles := map[string]File{what: *f}
sendFiles := map[string]File{kind: *media.MediaFile()}
for k, v := range files {
sendFiles[k] = v
}
data, err := b.sendFiles(sendWhat, sendFiles, params)
data, err := b.sendFiles(what, sendFiles, params)
if err != nil {
return nil, err
}

@ -70,5 +70,5 @@ func TestRaw(t *testing.T) {
assert.EqualError(t, err, "telebot: "+io.ErrUnexpectedEOF.Error())
_, err = b.Raw("testUnknownError", nil)
assert.EqualError(t, err, "telegram unknown: unknown error (400)")
assert.EqualError(t, err, "telegram: unknown error (400)")
}

367
bot.go

@ -9,8 +9,6 @@ import (
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
)
// NewBot does try to build a Bot with token `token`, which
@ -31,6 +29,9 @@ func NewBot(pref Settings) (*Bot, error) {
if pref.Poller == nil {
pref.Poller = &LongPoller{}
}
if pref.OnError == nil {
pref.OnError = defaultOnError
}
bot := &Bot{
Token: pref.Token,
@ -134,6 +135,7 @@ type Update struct {
PollAnswer *PollAnswer `json:"poll_answer,omitempty"`
MyChatMember *ChatMemberUpdate `json:"my_chat_member,omitempty"`
ChatMember *ChatMemberUpdate `json:"chat_member,omitempty"`
ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"`
}
// Command represents a bot command.
@ -234,30 +236,20 @@ func (b *Bot) NewMarkup() *ReplyMarkup {
// NewContext returns a new native context object,
// field by the passed update.
func (b *Bot) NewContext(upd Update) Context {
func (b *Bot) NewContext(u Update) Context {
return &nativeContext{
b: b,
update: upd,
message: upd.Message,
callback: upd.Callback,
query: upd.Query,
inlineResult: upd.InlineResult,
shippingQuery: upd.ShippingQuery,
preCheckoutQuery: upd.PreCheckoutQuery,
poll: upd.Poll,
pollAnswer: upd.PollAnswer,
myChatMember: upd.MyChatMember,
chatMember: upd.ChatMember,
b: b,
u: u,
}
}
// ProcessUpdate processes a single incoming update.
// A started bot calls this function automatically.
func (b *Bot) ProcessUpdate(upd Update) {
c := b.NewContext(upd).(*nativeContext)
func (b *Bot) ProcessUpdate(u Update) {
c := b.NewContext(u)
if upd.Message != nil {
m := upd.Message
if u.Message != nil {
m := u.Message
if m.PinnedMessage != nil {
b.handle(OnPinned, c)
@ -274,8 +266,8 @@ func (b *Bot) ProcessUpdate(upd Update) {
match := cmdRx.FindAllStringSubmatch(m.Text, -1)
if match != nil {
// Syntax: "</command>@<bot> <payload>"
command, botName := match[0][1], match[0][3]
if botName != "" && !strings.EqualFold(b.Me.Username, botName) {
return
}
@ -299,11 +291,30 @@ func (b *Bot) ProcessUpdate(upd Update) {
return
}
if m.Contact != nil {
b.handle(OnContact, c)
return
}
if m.Location != nil {
b.handle(OnLocation, c)
return
}
if m.Venue != nil {
b.handle(OnVenue, c)
return
}
if m.Game != nil {
b.handle(OnGame, c)
return
}
if m.Dice != nil {
b.handle(OnDice, c)
return
}
if m.Invoice != nil {
b.handle(OnInvoice, c)
return
}
if m.Payment != nil {
b.handle(OnPayment, c)
return
@ -401,44 +412,38 @@ func (b *Bot) ProcessUpdate(upd Update) {
}
}
if upd.EditedMessage != nil {
c.message = upd.EditedMessage
if u.EditedMessage != nil {
b.handle(OnEdited, c)
return
}
if upd.ChannelPost != nil {
m := upd.ChannelPost
if u.ChannelPost != nil {
m := u.ChannelPost
if m.PinnedMessage != nil {
c.message = m.PinnedMessage
b.handle(OnPinned, c)
return
}
c.message = upd.ChannelPost
b.handle(OnChannelPost, c)
return
}
if upd.EditedChannelPost != nil {
c.message = upd.EditedChannelPost
if u.EditedChannelPost != nil {
b.handle(OnEditedChannelPost, c)
return
}
if upd.Callback != nil {
if upd.Callback.Data != "" {
if data := upd.Callback.Data; data[0] == '\f' {
match := cbackRx.FindAllStringSubmatch(data, -1)
if match != nil {
unique, payload := match[0][1], match[0][3]
if handler, ok := b.handlers["\f"+unique]; ok {
c.callback.Unique = unique
c.callback.Data = payload
b.runHandler(handler, c)
return
}
if u.Callback != nil {
if data := u.Callback.Data; data != "" && data[0] == '\f' {
match := cbackRx.FindAllStringSubmatch(data, -1)
if match != nil {
unique, payload := match[0][1], match[0][3]
if handler, ok := b.handlers["\f"+unique]; ok {
u.Callback.Unique = unique
u.Callback.Data = payload
b.runHandler(handler, c)
return
}
}
}
@ -447,45 +452,50 @@ func (b *Bot) ProcessUpdate(upd Update) {
return
}
if upd.Query != nil {
if u.Query != nil {
b.handle(OnQuery, c)
return
}
if upd.InlineResult != nil {
if u.InlineResult != nil {
b.handle(OnInlineResult, c)
return
}
if upd.ShippingQuery != nil {
if u.ShippingQuery != nil {
b.handle(OnShipping, c)
return
}
if upd.PreCheckoutQuery != nil {
if u.PreCheckoutQuery != nil {
b.handle(OnCheckout, c)
return
}
if upd.Poll != nil {
if u.Poll != nil {
b.handle(OnPoll, c)
return
}
if upd.PollAnswer != nil {
if u.PollAnswer != nil {
b.handle(OnPollAnswer, c)
return
}
if upd.MyChatMember != nil {
if u.MyChatMember != nil {
b.handle(OnMyChatMember, c)
return
}
if upd.ChatMember != nil {
if u.ChatMember != nil {
b.handle(OnChatMember, c)
return
}
if u.ChatJoinRequest != nil {
b.handle(OnChatJoinRequest, c)
return
}
}
func (b *Bot) handle(end string, c Context) bool {
@ -497,38 +507,36 @@ func (b *Bot) handle(end string, c Context) bool {
}
func (b *Bot) handleMedia(c Context) bool {
m := c.Message()
var (
m = c.Message()
fired = true
)
switch {
case m.Photo != nil:
b.handle(OnPhoto, c)
fired = b.handle(OnPhoto, c)
case m.Voice != nil:
b.handle(OnVoice, c)
fired = b.handle(OnVoice, c)
case m.Audio != nil:
b.handle(OnAudio, c)
fired = b.handle(OnAudio, c)
case m.Animation != nil:
b.handle(OnAnimation, c)
fired = b.handle(OnAnimation, c)
case m.Document != nil:
b.handle(OnDocument, c)
fired = b.handle(OnDocument, c)
case m.Sticker != nil:
b.handle(OnSticker, c)
fired = b.handle(OnSticker, c)
case m.Video != nil:
b.handle(OnVideo, c)
fired = b.handle(OnVideo, c)
case m.VideoNote != nil:
b.handle(OnVideoNote, c)
case m.Contact != nil:
b.handle(OnContact, c)
case m.Location != nil:
b.handle(OnLocation, c)
case m.Venue != nil:
b.handle(OnVenue, c)
case m.Game != nil:
b.handle(OnGame, c)
case m.Dice != nil:
b.handle(OnDice, c)
fired = b.handle(OnVideoNote, c)
default:
return false
}
if !fired {
return b.handle(OnMedia, c)
}
return true
}
@ -570,6 +578,7 @@ func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message,
return nil, ErrBadRecipient
}
sendOpts := extractOptions(opts)
media := make([]string, len(a))
files := make(map[string]File)
@ -589,44 +598,19 @@ func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message,
repr = "attach://" + strconv.Itoa(i)
files[strconv.Itoa(i)] = *file
default:
return nil, errors.Errorf("telebot: album entry #%d does not exist", i)
return nil, fmt.Errorf("telebot: album entry #%d does not exist", i)
}
switch y := x.(type) {
case *Photo:
data, _ = json.Marshal(struct {
Type string `json:"type"`
Media string `json:"media"`
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
}{
Type: "photo",
Media: repr,
Caption: y.Caption,
ParseMode: y.ParseMode,
})
case *Video:
data, _ = json.Marshal(struct {
Type string `json:"type"`
Caption string `json:"caption"`
Media string `json:"media"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Duration int `json:"duration,omitempty"`
SupportsStreaming bool `json:"supports_streaming,omitempty"`
}{
Type: "video",
Caption: y.Caption,
Media: repr,
Width: y.Width,
Height: y.Height,
Duration: y.Duration,
SupportsStreaming: y.SupportsStreaming,
})
default:
return nil, errors.Errorf("telebot: album entry #%d is not valid", i)
im := x.InputMedia()
im.Media = repr
if len(sendOpts.Entities) > 0 {
im.Entities = sendOpts.Entities
} else {
im.ParseMode = sendOpts.ParseMode
}
data, _ = json.Marshal(im)
media[i] = string(data)
}
@ -634,8 +618,6 @@ func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message,
"chat_id": to.Recipient(),
"media": "[" + strings.Join(media, ",") + "]",
}
sendOpts := extractOptions(opts)
b.embedSendOptions(params, sendOpts)
data, err := b.sendFiles("sendMediaGroup", files, params)
@ -652,12 +634,18 @@ func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message,
for attachName := range files {
i, _ := strconv.Atoi(attachName)
r := resp.Result[i]
var newID string
if resp.Result[i].Photo != nil {
newID = resp.Result[i].Photo.FileID
} else {
newID = resp.Result[i].Video.FileID
switch {
case r.Photo != nil:
newID = r.Photo.FileID
case r.Video != nil:
newID = r.Video.FileID
case r.Audio != nil:
newID = r.Audio.FileID
case r.Document != nil:
newID = r.Document.FileID
}
a[i].MediaFile().FileID = newID
@ -754,7 +742,7 @@ func (b *Bot) Edit(msg Editable, what interface{}, opts ...interface{}) (*Messag
switch v := what.(type) {
case *ReplyMarkup:
return b.EditReplyMarkup(msg, v)
case InputMedia:
case Inputtable:
return b.EditMedia(msg, v, opts...)
case string:
method = "editMessageText"
@ -874,14 +862,14 @@ func (b *Bot) EditCaption(msg Editable, caption string, opts ...interface{}) (*M
// b.EditMedia(m, &tele.Photo{File: tele.FromDisk("chicken.jpg")})
// b.EditMedia(m, &tele.Video{File: tele.FromURL("http://video.mp4")})
//
func (b *Bot) EditMedia(msg Editable, media InputMedia, opts ...interface{}) (*Message, error) {
func (b *Bot) EditMedia(msg Editable, media Inputtable, opts ...interface{}) (*Message, error) {
var (
repr string
thumb *Photo
file = media.MediaFile()
files = make(map[string]File)
thumb *Photo
thumbName = "thumb"
file = media.MediaFile()
files = make(map[string]File)
)
switch {
@ -900,76 +888,18 @@ func (b *Bot) EditMedia(msg Editable, media InputMedia, opts ...interface{}) (*M
repr = "attach://" + s
files[s] = *file
default:
return nil, errors.Errorf("telebot: can't edit media, it does not exist")
}
type FileJSON struct {
// All types.
Type string `json:"type"`
Caption string `json:"caption"`
Media string `json:"media"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
// Video.
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
SupportsStreaming bool `json:"supports_streaming,omitempty"`
// Video and audio.
Duration int `json:"duration,omitempty"`
// Document.
FileName string `json:"file_name"`
// Document, video and audio.
Thumbnail string `json:"thumb,omitempty"`
MIME string `json:"mime_type,omitempty"`
// Audio.
Title string `json:"title,omitempty"`
Performer string `json:"performer,omitempty"`
return nil, fmt.Errorf("telebot: cannot edit media, it does not exist")
}
result := &FileJSON{Media: repr}
switch m := media.(type) {
case *Photo:
result.Type = "photo"
result.Caption = m.Caption
case *Video:
result.Type = "video"
result.Caption = m.Caption
result.Width = m.Width
result.Height = m.Height
result.Duration = m.Duration
result.SupportsStreaming = m.SupportsStreaming
result.MIME = m.MIME
thumb = m.Thumbnail
case *Document:
result.Type = "document"
result.Caption = m.Caption
result.FileName = m.FileName
result.MIME = m.MIME
thumb = m.Thumbnail
case *Audio:
result.Type = "audio"
result.Caption = m.Caption
result.Duration = m.Duration
result.MIME = m.MIME
result.Title = m.Title
result.Performer = m.Performer
thumb = m.Thumbnail
case *Document:
thumb = m.Thumbnail
case *Animation:
result.Type = "animation"
result.Caption = m.Caption
result.Width = m.Width
result.Height = m.Height
result.Duration = m.Duration
result.MIME = m.MIME
result.FileName = m.FileName
thumb = m.Thumbnail
default:
return nil, errors.Errorf("telebot: media entry is not valid")
}
msgID, chatID := msg.MessageSig()
@ -978,15 +908,21 @@ func (b *Bot) EditMedia(msg Editable, media InputMedia, opts ...interface{}) (*M
sendOpts := extractOptions(opts)
b.embedSendOptions(params, sendOpts)
if sendOpts != nil {
result.ParseMode = params["parse_mode"]
im := media.InputMedia()
im.Media = repr
if len(sendOpts.Entities) > 0 {
im.Entities = sendOpts.Entities
} else {
im.ParseMode = sendOpts.ParseMode
}
if thumb != nil {
result.Thumbnail = "attach://" + thumbName
im.Thumbnail = "attach://" + thumbName
files[thumbName] = *thumb.MediaFile()
}
data, _ := json.Marshal(result)
data, _ := json.Marshal(im)
params["media"] = string(data)
if chatID == 0 { // if inline message
@ -1214,7 +1150,7 @@ func (b *Bot) File(file *File) (io.ReadCloser, error) {
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, errors.Errorf("telebot: expected status 200 but got %s", resp.Status)
return nil, fmt.Errorf("telebot: expected status 200 but got %s", resp.Status)
}
return resp.Body, nil
@ -1435,8 +1371,12 @@ func (b *Bot) UnpinAll(chat *Chat) error {
// current username of a user, group or channel, etc.
//
func (b *Bot) ChatByID(id int64) (*Chat, error) {
return b.ChatByUsername(strconv.FormatInt(id, 10))
}
func (b *Bot) ChatByUsername(name string) (*Chat, error) {
params := map[string]string{
"chat_id": strconv.FormatInt(id, 10),
"chat_id": name,
}
data, err := b.Raw("getChat", params)
@ -1574,70 +1514,3 @@ func (b *Bot) Close() (bool, error) {
return resp.Result, nil
}
// CreateInviteLink creates an additional invite link for a chat.
func (b *Bot) CreateInviteLink(chat *Chat, link *ChatInviteLink) (*ChatInviteLink, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
}
if link != nil {
params["expire_date"] = strconv.FormatInt(link.ExpireUnixtime, 10)
params["member_limit"] = strconv.Itoa(link.MemberLimit)
}
data, err := b.Raw("createChatInviteLink", params)
if err != nil {
return nil, err
}
var resp ChatInviteLink
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return &resp, nil
}
// EditInviteLink edits a non-primary invite link created by the bot.
func (b *Bot) EditInviteLink(chat *Chat, link *ChatInviteLink) (*ChatInviteLink, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
}
if link != nil {
params["invite_link"] = link.InviteLink
params["expire_date"] = strconv.FormatInt(link.ExpireUnixtime, 10)
params["member_limit"] = strconv.Itoa(link.MemberLimit)
}
data, err := b.Raw("editChatInviteLink", params)
if err != nil {
return nil, err
}
var resp ChatInviteLink
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return &resp, nil
}
// RevokeInviteLink revokes an invite link created by the bot.
func (b *Bot) RevokeInviteLink(chat *Chat, link string) (*ChatInviteLink, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
"link": link,
}
data, err := b.Raw("revokeChatInviteLink", params)
if err != nil {
return nil, err
}
var resp ChatInviteLink
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return &resp, nil
}

@ -2,6 +2,8 @@ package telebot
import (
"errors"
"io"
"io/ioutil"
"net/http"
"os"
"strconv"
@ -13,10 +15,6 @@ import (
"github.com/stretchr/testify/require"
)
const (
photoID = "AgACAgIAAxkDAAIBV16Ybpg7l2jPgMUiiLJ3WaQOUqTrAAJorjEbh2TBSPSOinaCHfydQO_pki4AAwEAAwIAA3kAA_NQAAIYBA"
)
var (
// required to test send and edit methods
token = os.Getenv("TELEBOT_SECRET")
@ -152,6 +150,12 @@ func TestBotProcessUpdate(t *testing.T) {
t.Fatal(err)
}
b.Handle(OnMedia, func(c Context) error {
assert.NotNil(t, c.Message().Photo)
return nil
})
b.ProcessUpdate(Update{Message: &Message{Photo: &Photo{}}})
b.Handle("/start", func(c Context) error {
assert.Equal(t, "/start", c.Text())
return nil
@ -212,6 +216,10 @@ func TestBotProcessUpdate(t *testing.T) {
assert.NotNil(t, c.Message().Venue)
return nil
})
b.Handle(OnDice, func(c Context) error {
assert.NotNil(t, c.Message().Dice)
return nil
})
b.Handle(OnInvoice, func(c Context) error {
assert.NotNil(t, c.Message().Invoice)
return nil
@ -378,7 +386,7 @@ func TestBot(t *testing.T) {
assert.Equal(t, ErrBadRecipient, err)
photo := &Photo{
File: File{FileID: photoID},
File: FromURL("https://telegra.ph/file/65c5237b040ebf80ec278.jpg"),
Caption: t.Name(),
}
var msg *Message
@ -397,7 +405,10 @@ func TestBot(t *testing.T) {
_, err = b.SendAlbum(to, nil)
assert.Error(t, err)
msgs, err := b.SendAlbum(to, Album{photo, photo})
photo2 := *photo
photo2.Caption = ""
msgs, err := b.SendAlbum(to, Album{photo, &photo2}, ModeHTML)
require.NoError(t, err)
assert.Len(t, msgs, 2)
assert.NotEmpty(t, msgs[0].AlbumID)
@ -419,12 +430,42 @@ func TestBot(t *testing.T) {
b.parseMode = ModeDefault
})
t.Run("Edit(what=InputMedia)", func(t *testing.T) {
t.Run("Edit(what=Media)", func(t *testing.T) {
edited, err := b.Edit(msg, photo)
require.NoError(t, err)
assert.Equal(t, edited.Photo.UniqueID, photo.UniqueID)
resp, err := http.Get("https://telegra.ph/file/274e5eb26f348b10bd8ee.mp4")
require.NoError(t, err)
defer resp.Body.Close()
file, err := ioutil.TempFile("", "")
require.NoError(t, err)
_, err = io.Copy(file, resp.Body)
require.NoError(t, err)
animation := &Animation{
File: FromDisk(file.Name()),
Caption: t.Name(),
FileName: "animation.gif",
}
msg, err := b.Send(msg.Chat, animation)
require.NoError(t, err)
if msg.Animation != nil {
assert.Equal(t, msg.Animation.FileID, animation.FileID)
} else {
assert.Equal(t, msg.Document.FileID, animation.FileID)
}
_, err = b.Edit(edited, animation)
require.NoError(t, err)
})
t.Run("Edit(what=Animation)", func(t *testing.T) {})
t.Run("Send(what=string)", func(t *testing.T) {
msg, err = b.Send(to, t.Name())
require.NoError(t, err)
@ -444,7 +485,7 @@ func TestBot(t *testing.T) {
fwd.ID += 1 // nonexistent message
_, err = b.Forward(to, fwd)
assert.Equal(t, ErrToForwardNotFound, err)
assert.Equal(t, ErrNotFoundToForward, err)
})
t.Run("Edit(what=string)", func(t *testing.T) {
@ -480,10 +521,10 @@ func TestBot(t *testing.T) {
edited, err = b.EditReplyMarkup(edited, nil)
require.NoError(t, err)
assert.Nil(t, edited.ReplyMarkup.InlineKeyboard)
assert.Nil(t, edited.ReplyMarkup)
_, err = b.Edit(edited, bad)
assert.Equal(t, ErrButtonDataInvalid, err)
assert.Equal(t, ErrBadButtonData, err)
})
t.Run("Edit(what=Location)", func(t *testing.T) {
@ -498,7 +539,7 @@ func TestBot(t *testing.T) {
assert.NotNil(t, edited.Location)
})
// should be the last
// Should be after the Edit tests.
t.Run("Delete()", func(t *testing.T) {
require.NoError(t, b.Delete(msg))
})
@ -556,4 +597,30 @@ func TestBot(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, orig2, cmds)
})
t.Run("CreateInviteLink", func(t *testing.T) {
inviteLink, err := b.CreateInviteLink(&Chat{ID: chatID}, nil)
assert.Nil(t, err)
assert.True(t, len(inviteLink.InviteLink) > 0)
})
t.Run("EditInviteLink", func(t *testing.T) {
inviteLink, err := b.CreateInviteLink(&Chat{ID: chatID}, nil)
assert.Nil(t, err)
assert.True(t, len(inviteLink.InviteLink) > 0)
response, err := b.EditInviteLink(&Chat{ID: chatID}, &ChatInviteLink{InviteLink: inviteLink.InviteLink})
assert.Nil(t, err)
assert.True(t, len(response.InviteLink) > 0)
})
t.Run("RevokeInviteLink", func(t *testing.T) {
inviteLink, err := b.CreateInviteLink(&Chat{ID: chatID}, nil)
assert.Nil(t, err)
assert.True(t, len(inviteLink.InviteLink) > 0)
response, err := b.RevokeInviteLink(&Chat{ID: chatID}, inviteLink.InviteLink)
assert.Nil(t, err)
assert.True(t, len(response.InviteLink) > 0)
})
}

@ -1,6 +1,10 @@
package telebot
import "strconv"
import (
"encoding/json"
"strconv"
"time"
)
// User object represents a Telegram user, bot.
type User struct {
@ -73,9 +77,6 @@ type ChatPhoto struct {
// Recipient returns chat ID (see Recipient interface).
func (c *Chat) Recipient() string {
if c.Type == ChatChannel && c.Username != "" {
return c.Username
}
return strconv.FormatInt(c.ID, 10)
}
@ -120,3 +121,146 @@ type ChatID int64
func (i ChatID) Recipient() string {
return strconv.FormatInt(int64(i), 10)
}
// ChatJoinRequest represents a join request sent to a chat.
type ChatJoinRequest struct {
// Chat to which the request was sent.
Chat *Chat `json:"chat"`
// Sender is the user that sent the join request.
Sender *User `json:"from"`
// Unixtime, use ChatJoinRequest.Time() to get time.Time.
Unixtime int64 `json:"date"`
// Bio of the user, optional.
Bio string `json:"bio"`
// InviteLink is the chat invite link that was used by
//the user to send the join request, optional.
InviteLink *ChatInviteLink `json:"invite_link"`
}
// Time returns the moment of chat join request sending in local time.
func (r ChatJoinRequest) Time() time.Time {
return time.Unix(r.Unixtime, 0)
}
// CreateInviteLink creates an additional invite link for a chat.
func (b *Bot) CreateInviteLink(chat Recipient, link *ChatInviteLink) (*ChatInviteLink, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
}
if link != nil {
params["name"] = link.Name
if link.ExpireUnixtime != 0 {
params["expire_date"] = strconv.FormatInt(link.ExpireUnixtime, 10)
}
if link.MemberLimit > 0 {
params["member_limit"] = strconv.Itoa(link.MemberLimit)
} else if link.JoinRequest {
params["creates_join_request"] = "true"
}
}
data, err := b.Raw("createChatInviteLink", params)
if err != nil {
return nil, err
}
var resp struct {
Result ChatInviteLink `json:"result"`
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return &resp.Result, nil
}
// EditInviteLink edits a non-primary invite link created by the bot.
func (b *Bot) EditInviteLink(chat Recipient, link *ChatInviteLink) (*ChatInviteLink, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
}
if link != nil {
params["invite_link"] = link.InviteLink
params["name"] = link.Name
if link.ExpireUnixtime != 0 {
params["expire_date"] = strconv.FormatInt(link.ExpireUnixtime, 10)
}
if link.MemberLimit > 0 {
params["member_limit"] = strconv.Itoa(link.MemberLimit)
} else if link.JoinRequest {
params["creates_join_request"] = "true"
}
}
data, err := b.Raw("editChatInviteLink", params)
if err != nil {
return nil, err
}
var resp struct {
Result ChatInviteLink `json:"result"`
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return &resp.Result, nil
}
// RevokeInviteLink revokes an invite link created by the bot.
func (b *Bot) RevokeInviteLink(chat Recipient, link string) (*ChatInviteLink, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
"invite_link": link,
}
data, err := b.Raw("revokeChatInviteLink", params)
if err != nil {
return nil, err
}
var resp struct {
Result ChatInviteLink `json:"result"`
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return &resp.Result, nil
}
// ApproveChatJoinRequest approves a chat join request.
func (b *Bot) ApproveChatJoinRequest(chat Recipient, user *User) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"user_id": user.Recipient(),
}
data, err := b.Raw("approveChatJoinRequest", params)
if err != nil {
return err
}
return extractOk(data)
}
// DeclineChatJoinRequest declines a chat join request.
func (b *Bot) DeclineChatJoinRequest(chat Recipient, user *User) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"user_id": user.Recipient(),
}
data, err := b.Raw("declineChatJoinRequest", params)
if err != nil {
return err
}
return extractOk(data)
}

@ -1,10 +1,10 @@
package telebot
import (
"errors"
"strings"
"sync"
"github.com/pkg/errors"
"time"
)
// HandlerFunc represents a handler function, which is
@ -46,6 +46,9 @@ type Context interface {
// ChatMember returns chat member changes.
ChatMember() *ChatMemberUpdate
// ChatJoinRequest returns cha
ChatJoinRequest() *ChatJoinRequest
// Migration returns both migration from and to chat IDs.
Migration() (int64, int64)
@ -117,6 +120,11 @@ type Context interface {
// See Delete from bot.go.
Delete() error
// DeleteAfter waits for the duration to elapse and then removes the
// message. It handles an error automatically using b.OnError callback.
// It returns a Timer that can be used to cancel the call using its Stop method.
DeleteAfter(d time.Duration) *time.Timer
// Notify updates the chat action for the current recipient.
// See Notify from bot.go.
Notify(action ChatAction) error
@ -147,20 +155,8 @@ 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
update Update
message *Message
callback *Callback
query *Query
inlineResult *InlineResult
shippingQuery *ShippingQuery
preCheckoutQuery *PreCheckoutQuery
poll *Poll
pollAnswer *PollAnswer
myChatMember *ChatMemberUpdate
chatMember *ChatMemberUpdate
b *Bot
u Update
lock sync.RWMutex
store map[string]interface{}
}
@ -170,79 +166,98 @@ func (c *nativeContext) Bot() *Bot {
}
func (c *nativeContext) Update() Update {
return c.update
return c.u
}
func (c *nativeContext) Message() *Message {
switch {
case c.message != nil:
return c.message
case c.callback != nil:
return c.callback.Message
case c.u.Message != nil:
return c.u.Message
case c.u.Callback != nil:
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
}
return c.u.ChannelPost
case c.u.EditedChannelPost != nil:
return c.u.EditedChannelPost
default:
return nil
}
}
func (c *nativeContext) Callback() *Callback {
return c.callback
return c.u.Callback
}
func (c *nativeContext) Query() *Query {
return c.query
return c.u.Query
}
func (c *nativeContext) InlineResult() *InlineResult {
return c.inlineResult
return c.u.InlineResult
}
func (c *nativeContext) ShippingQuery() *ShippingQuery {
return c.shippingQuery
return c.u.ShippingQuery
}
func (c *nativeContext) PreCheckoutQuery() *PreCheckoutQuery {
return c.preCheckoutQuery
return c.u.PreCheckoutQuery
}
func (c *nativeContext) ChatMember() *ChatMemberUpdate {
switch {
case c.chatMember != nil:
return c.chatMember
case c.myChatMember != nil:
return c.myChatMember
case c.u.ChatMember != nil:
return c.u.ChatMember
case c.u.MyChatMember != nil:
return c.u.MyChatMember
default:
return nil
}
}
func (c *nativeContext) ChatJoinRequest() *ChatJoinRequest {
return c.u.ChatJoinRequest
}
func (c *nativeContext) Poll() *Poll {
return c.poll
return c.u.Poll
}
func (c *nativeContext) PollAnswer() *PollAnswer {
return c.pollAnswer
return c.u.PollAnswer
}
func (c *nativeContext) Migration() (int64, int64) {
return c.message.MigrateFrom, c.message.MigrateTo
return c.u.Message.MigrateFrom, c.u.Message.MigrateTo
}
func (c *nativeContext) Sender() *User {
switch {
case c.message != nil:
return c.message.Sender
case c.callback != nil:
return c.callback.Sender
case c.query != nil:
return c.query.Sender
case c.inlineResult != nil:
return c.inlineResult.Sender
case c.shippingQuery != nil:
return c.shippingQuery.Sender
case c.preCheckoutQuery != nil:
return c.preCheckoutQuery.Sender
case c.pollAnswer != nil:
return c.pollAnswer.Sender
case c.u.Callback != nil:
return c.u.Callback.Sender
case c.Message() != nil:
return c.Message().Sender
case c.u.Query != nil:
return c.u.Query.Sender
case c.u.InlineResult != nil:
return c.u.InlineResult.Sender
case c.u.ShippingQuery != nil:
return c.u.ShippingQuery.Sender
case c.u.PreCheckoutQuery != nil:
return c.u.PreCheckoutQuery.Sender
case c.u.PollAnswer != nil:
return c.u.PollAnswer.Sender
case c.u.MyChatMember != nil:
return c.u.MyChatMember.Sender
case c.u.ChatMember != nil:
return c.u.ChatMember.Sender
case c.u.ChatJoinRequest != nil:
return c.u.ChatJoinRequest.Sender
default:
return nil
}
@ -250,14 +265,14 @@ func (c *nativeContext) Sender() *User {
func (c *nativeContext) Chat() *Chat {
switch {
case c.message != nil:
return c.message.Chat
case c.callback != nil && c.callback.Message != nil:
return c.callback.Message.Chat
case c.myChatMember != nil:
return c.myChatMember.Chat
case c.chatMember != nil:
return c.chatMember.Chat
case c.Message() != nil:
return c.Message().Chat
case c.u.MyChatMember != nil:
return c.u.MyChatMember.Chat
case c.u.ChatMember != nil:
return c.u.ChatMember.Chat
case c.u.ChatJoinRequest != nil:
return c.u.ChatJoinRequest.Chat
default:
return nil
}
@ -272,30 +287,30 @@ func (c *nativeContext) Recipient() Recipient {
}
func (c *nativeContext) Text() string {
switch {
case c.message != nil:
return c.message.Text
case c.callback != nil && c.callback.Message != nil:
return c.callback.Message.Text
default:
m := c.Message()
if m == nil {
return ""
}
if m.Caption != "" {
return m.Caption
}
return m.Text
}
func (c *nativeContext) Data() string {
switch {
case c.message != nil:
return c.message.Payload
case c.callback != nil:
return c.callback.Data
case c.query != nil:
return c.query.Text
case c.inlineResult != nil:
return c.inlineResult.Query
case c.shippingQuery != nil:
return c.shippingQuery.Payload
case c.preCheckoutQuery != nil:
return c.preCheckoutQuery.Payload
case c.u.Message != nil:
return c.u.Message.Payload
case c.u.Callback != nil:
return c.u.Callback.Data
case c.u.Query != nil:
return c.u.Query.Text
case c.u.InlineResult != nil:
return c.u.InlineResult.Query
case c.u.ShippingQuery != nil:
return c.u.ShippingQuery.Payload
case c.u.PreCheckoutQuery != nil:
return c.u.PreCheckoutQuery.Payload
default:
return ""
}
@ -303,17 +318,17 @@ func (c *nativeContext) Data() string {
func (c *nativeContext) Args() []string {
switch {
case c.message != nil:
payload := strings.Trim(c.message.Payload, " ")
case c.u.Message != nil:
payload := strings.Trim(c.u.Message.Payload, " ")
if payload != "" {
return strings.Split(payload, " ")
}
case c.callback != nil:
return strings.Split(c.callback.Data, "|")
case c.query != nil:
return strings.Split(c.query.Text, " ")
case c.inlineResult != nil:
return strings.Split(c.inlineResult.Query, " ")
case c.u.Callback != nil:
return strings.Split(c.u.Callback.Data, "|")
case c.u.Query != nil:
return strings.Split(c.u.Query.Text, " ")
case c.u.InlineResult != nil:
return strings.Split(c.u.InlineResult.Query, " ")
}
return nil
}
@ -352,24 +367,24 @@ func (c *nativeContext) ForwardTo(to Recipient, opts ...interface{}) error {
}
func (c *nativeContext) Edit(what interface{}, opts ...interface{}) error {
if c.inlineResult != nil {
_, err := c.b.Edit(c.inlineResult, what, opts...)
if c.u.InlineResult != nil {
_, err := c.b.Edit(c.u.InlineResult, what, opts...)
return err
}
if c.callback != nil {
_, err := c.b.Edit(c.callback, what, opts...)
if c.u.Callback != nil {
_, err := c.b.Edit(c.u.Callback, what, opts...)
return err
}
return ErrBadContext
}
func (c *nativeContext) EditCaption(caption string, opts ...interface{}) error {
if c.inlineResult != nil {
_, err := c.b.EditCaption(c.inlineResult, caption, opts...)
if c.u.InlineResult != nil {
_, err := c.b.EditCaption(c.u.InlineResult, caption, opts...)
return err
}
if c.callback != nil {
_, err := c.b.Edit(c.callback, caption, opts...)
if c.u.Callback != nil {
_, err := c.b.EditCaption(c.u.Callback, caption, opts...)
return err
}
return ErrBadContext
@ -399,36 +414,44 @@ func (c *nativeContext) Delete() error {
return c.b.Delete(msg)
}
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)
}
})
}
func (c *nativeContext) Notify(action ChatAction) error {
return c.b.Notify(c.Recipient(), action)
}
func (c *nativeContext) Ship(what ...interface{}) error {
if c.shippingQuery == nil {
if c.u.ShippingQuery == nil {
return errors.New("telebot: context shipping query is nil")
}
return c.b.Ship(c.shippingQuery, what...)
return c.b.Ship(c.u.ShippingQuery, what...)
}
func (c *nativeContext) Accept(errorMessage ...string) error {
if c.preCheckoutQuery == nil {
if c.u.PreCheckoutQuery == nil {
return errors.New("telebot: context pre checkout query is nil")
}
return c.b.Accept(c.preCheckoutQuery, errorMessage...)
return c.b.Accept(c.u.PreCheckoutQuery, errorMessage...)
}
func (c *nativeContext) Answer(resp *QueryResponse) error {
if c.query == nil {
if c.u.Query == nil {
return errors.New("telebot: context inline query is nil")
}
return c.b.Answer(c.query, resp)
return c.b.Answer(c.u.Query, resp)
}
func (c *nativeContext) Respond(resp ...*CallbackResponse) error {
if c.callback == nil {
if c.u.Callback == nil {
return errors.New("telebot: context callback is nil")
}
return c.b.Respond(c.callback, resp...)
return c.b.Respond(c.u.Callback, resp...)
}
func (c *nativeContext) Set(key string, value interface{}) {

@ -5,26 +5,32 @@ import (
"strings"
)
type APIError struct {
Code int
Description string
Message string
Parameters map[string]interface{}
}
type (
Error struct {
Code int
Description string
Message string
}
type FloodError struct {
*APIError
RetryAfter int
}
FloodError struct {
err *Error
RetryAfter int
}
GroupError struct {
err *Error
MigratedTo int64
}
)
// ʔ returns description of error.
// A tiny shortcut to make code clearer.
func (err *APIError) ʔ() string {
func (err *Error) ʔ() string {
return err.Description
}
// Error implements error interface.
func (err *APIError) Error() string {
func (err *Error) Error() string {
msg := err.Message
if msg == "" {
split := strings.Split(err.Description, ": ")
@ -37,10 +43,20 @@ func (err *APIError) Error() string {
return fmt.Sprintf("telegram: %s (%d)", msg, err.Code)
}
// NewAPIError returns new APIError instance with given description.
// Error implements error interface.
func (err FloodError) Error() string {
return err.err.Error()
}
// Error implements error interface.
func (err GroupError) Error() string {
return err.err.Error()
}
// NewError returns new Error instance with given description.
// First element of msgs is Description. The second is optional Message.
func NewAPIError(code int, msgs ...string) *APIError {
err := &APIError{Code: code}
func NewError(code int, msgs ...string) *Error {
err := &Error{Code: code}
if len(msgs) >= 1 {
err.Description = msgs[0]
}
@ -50,134 +66,168 @@ func NewAPIError(code int, msgs ...string) *APIError {
return err
}
// General errors
var (
// General errors
ErrUnauthorized = NewAPIError(401, "Unauthorized")
ErrNotStartedByUser = NewAPIError(403, "Forbidden: bot can't initiate conversation with a user")
ErrBlockedByUser = NewAPIError(401, "Forbidden: bot was blocked by the user")
ErrUserIsDeactivated = NewAPIError(401, "Forbidden: user is deactivated")
ErrNotFound = NewAPIError(404, "Not Found")
ErrInternal = NewAPIError(500, "Internal Server Error")
// Bad request errors
ErrTooLarge = NewAPIError(400, "Request Entity Too Large")
ErrMessageTooLong = NewAPIError(400, "Bad Request: message is too long")
ErrToForwardNotFound = NewAPIError(400, "Bad Request: message to forward not found")
ErrToReplyNotFound = NewAPIError(400, "Bad Request: reply message not found")
ErrToDeleteNotFound = NewAPIError(400, "Bad Request: message to delete not found")
ErrEmptyMessage = NewAPIError(400, "Bad Request: message must be non-empty")
ErrEmptyText = NewAPIError(400, "Bad Request: text is empty")
ErrEmptyChatID = NewAPIError(400, "Bad Request: chat_id is empty")
ErrChatNotFound = NewAPIError(400, "Bad Request: chat not found")
ErrMessageNotModified = NewAPIError(400, "Bad Request: message is not modified")
ErrSameMessageContent = NewAPIError(400, "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message")
ErrCantEditMessage = NewAPIError(400, "Bad Request: message can't be edited")
ErrButtonDataInvalid = NewAPIError(400, "Bad Request: BUTTON_DATA_INVALID")
ErrWrongTypeOfContent = NewAPIError(400, "Bad Request: wrong type of the web page content")
ErrBadURLContent = NewAPIError(400, "Bad Request: failed to get HTTP URL content")
ErrWrongFileID = NewAPIError(400, "Bad Request: wrong file identifier/HTTP URL specified")
ErrWrongFileIDSymbol = NewAPIError(400, "Bad Request: wrong remote file id specified: can't unserialize it. Wrong last symbol")
ErrWrongFileIDLength = NewAPIError(400, "Bad Request: wrong remote file id specified: Wrong string length")
ErrWrongFileIDCharacter = NewAPIError(400, "Bad Request: wrong remote file id specified: Wrong character in the string")
ErrWrongFileIDPadding = NewAPIError(400, "Bad Request: wrong remote file id specified: Wrong padding in the string")
ErrFailedImageProcess = NewAPIError(400, "Bad Request: IMAGE_PROCESS_FAILED", "Image process failed")
ErrInvalidStickerSet = NewAPIError(400, "Bad Request: STICKERSET_INVALID", "Stickerset is invalid")
ErrBadPollOptions = NewAPIError(400, "Bad Request: expected an Array of String as options")
ErrGroupMigrated = NewAPIError(400, "Bad Request: group chat was upgraded to a supergroup chat")
ErrTooLarge = NewError(400, "Request Entity Too Large")
ErrUnauthorized = NewError(401, "Unauthorized")
ErrNotFound = NewError(404, "Not Found")
ErrInternal = NewError(500, "Internal Server Error")
)
// No rights errors
ErrNoRightsToRestrict = NewAPIError(400, "Bad Request: not enough rights to restrict/unrestrict chat member")
ErrNoRightsToSend = NewAPIError(400, "Bad Request: have no rights to send a message")
ErrNoRightsToSendPhoto = NewAPIError(400, "Bad Request: not enough rights to send photos to the chat")
ErrNoRightsToSendStickers = NewAPIError(400, "Bad Request: not enough rights to send stickers to the chat")
ErrNoRightsToSendGifs = NewAPIError(400, "Bad Request: CHAT_SEND_GIFS_FORBIDDEN", "sending GIFS is not allowed in this chat")
ErrNoRightsToDelete = NewAPIError(400, "Bad Request: message can't be deleted")
ErrKickingChatOwner = NewAPIError(400, "Bad Request: can't remove chat owner")
// Bad request errors
var (
ErrBadButtonData = NewError(400, "Bad Request: BUTTON_DATA_INVALID")
ErrBadPollOptions = NewError(400, "Bad Request: expected an Array of String as options")
ErrBadURLContent = NewError(400, "Bad Request: failed to get HTTP URL content")
ErrCantEditMessage = NewError(400, "Bad Request: message can't be edited")
ErrCantRemoveOwner = NewError(400, "Bad Request: can't remove chat owner")
ErrCantUploadFile = NewError(400, "Bad Request: can't upload file by URL")
ErrCantUseMediaInAlbum = NewError(400, "Bad Request: can't use the media of the specified type in the album")
ErrChatAboutNotModified = NewError(400, "Bad Request: chat description is not modified")
ErrChatNotFound = NewError(400, "Bad Request: chat not found")
ErrEmptyChatID = NewError(400, "Bad Request: chat_id is empty")
ErrEmptyMessage = NewError(400, "Bad Request: message must be non-empty")
ErrEmptyText = NewError(400, "Bad Request: text is empty")
ErrFailedImageProcess = NewError(400, "Bad Request: IMAGE_PROCESS_FAILED", "Image process failed")
ErrGroupMigrated = NewError(400, "Bad Request: group chat was upgraded to a supergroup chat")
ErrMessageNotModified = NewError(400, "Bad Request: message is not modified")
ErrNoRightsToDelete = NewError(400, "Bad Request: message can't be deleted")
ErrNoRightsToRestrict = NewError(400, "Bad Request: not enough rights to restrict/unrestrict chat member")
ErrNoRightsToSend = NewError(400, "Bad Request: have no rights to send a message")
ErrNoRightsToSendGifs = NewError(400, "Bad Request: CHAT_SEND_GIFS_FORBIDDEN", "sending GIFS is not allowed in this chat")
ErrNoRightsToSendPhoto = NewError(400, "Bad Request: not enough rights to send photos to the chat")
ErrNoRightsToSendStickers = NewError(400, "Bad Request: not enough rights to send stickers to the chat")
ErrNotFoundToDelete = NewError(400, "Bad Request: message to delete not found")
ErrNotFoundToForward = NewError(400, "Bad Request: message to forward not found")
ErrNotFoundToReply = NewError(400, "Bad Request: reply message not found")
ErrQueryTooOld = NewError(400, "Bad Request: query is too old and response timeout expired or query ID is invalid")
ErrSameMessageContent = NewError(400, "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message")
ErrStickerEmojisInvalid = NewError(400, "Bad Request: invalid sticker emojis")
ErrStickerSetInvalid = NewError(400, "Bad Request: STICKERSET_INVALID", "Stickerset is invalid")
ErrStickerSetInvalidName = NewError(400, "Bad Request: invalid sticker set name is specified")
ErrStickerSetNameOccupied = NewError(400, "Bad Request: sticker set name is already occupied")
ErrTooLongMarkup = NewError(400, "Bad Request: reply markup is too long")
ErrTooLongMessage = NewError(400, "Bad Request: message is too long")
ErrUserIsAdmin = NewError(400, "Bad Request: user is an administrator of the chat")
ErrWrongFileID = NewError(400, "Bad Request: wrong file identifier/HTTP URL specified")
ErrWrongFileIDCharacter = NewError(400, "Bad Request: wrong remote file id specified: Wrong character in the string")
ErrWrongFileIDLength = NewError(400, "Bad Request: wrong remote file id specified: Wrong string length")
ErrWrongFileIDPadding = NewError(400, "Bad Request: wrong remote file id specified: Wrong padding in the string")
ErrWrongFileIDSymbol = NewError(400, "Bad Request: wrong remote file id specified: can't unserialize it. Wrong last symbol")
ErrWrongTypeOfContent = NewError(400, "Bad Request: wrong type of the web page content")
ErrWrongURL = NewError(400, "Bad Request: wrong HTTP URL specified")
)
// Super/groups errors
ErrBotKickedFromGroup = NewAPIError(403, "Forbidden: bot was kicked from the group chat")
ErrBotKickedFromSuperGroup = NewAPIError(403, "Forbidden: bot was kicked from the supergroup chat")
// Forbidden errors
var (
ErrBlockedByUser = NewError(403, "Forbidden: bot was blocked by the user")
ErrKickedFromGroup = NewError(403, "Forbidden: bot was kicked from the group chat")
ErrKickedFromSuperGroup = NewError(403, "Forbidden: bot was kicked from the supergroup chat")
ErrNotStartedByUser = NewError(403, "Forbidden: bot can't initiate conversation with a user")
ErrUserIsDeactivated = NewError(403, "Forbidden: user is deactivated")
)
// ErrByDescription returns APIError instance by given description.
func ErrByDescription(s string) error {
// Err returns Error instance by given description.
func Err(s string) error {
switch s {
case ErrTooLarge.ʔ():
return ErrTooLarge
case ErrUnauthorized.ʔ():
return ErrUnauthorized
case ErrNotStartedByUser.ʔ():
return ErrNotStartedByUser
case ErrNotFound.ʔ():
return ErrNotFound
case ErrUserIsDeactivated.ʔ():
return ErrUserIsDeactivated
case ErrToForwardNotFound.ʔ():
return ErrToForwardNotFound
case ErrToReplyNotFound.ʔ():
return ErrToReplyNotFound
case ErrMessageTooLong.ʔ():
return ErrMessageTooLong
case ErrBlockedByUser.ʔ():
return ErrBlockedByUser
case ErrToDeleteNotFound.ʔ():
return ErrToDeleteNotFound
case ErrInternal.ʔ():
return ErrInternal
case ErrBadButtonData.ʔ():
return ErrBadButtonData
case ErrBadPollOptions.ʔ():
return ErrBadPollOptions
case ErrBadURLContent.ʔ():
return ErrBadURLContent
case ErrCantEditMessage.ʔ():
return ErrCantEditMessage
case ErrCantRemoveOwner.ʔ():
return ErrCantRemoveOwner
case ErrCantUploadFile.ʔ():
return ErrCantUploadFile
case ErrCantUseMediaInAlbum.ʔ():
return ErrCantUseMediaInAlbum
case ErrChatAboutNotModified.ʔ():
return ErrChatAboutNotModified
case ErrChatNotFound.ʔ():
return ErrChatNotFound
case ErrEmptyChatID.ʔ():
return ErrEmptyChatID
case ErrEmptyMessage.ʔ():
return ErrEmptyMessage
case ErrEmptyText.ʔ():
return ErrEmptyText
case ErrEmptyChatID.ʔ():
return ErrEmptyChatID
case ErrChatNotFound.ʔ():
return ErrChatNotFound
case ErrFailedImageProcess.ʔ():
return ErrFailedImageProcess
case ErrGroupMigrated.ʔ():
return ErrGroupMigrated
case ErrMessageNotModified.ʔ():
return ErrMessageNotModified
case ErrSameMessageContent.ʔ():
return ErrSameMessageContent
case ErrCantEditMessage.ʔ():
return ErrCantEditMessage
case ErrButtonDataInvalid.ʔ():
return ErrButtonDataInvalid
case ErrBadPollOptions.ʔ():
return ErrBadPollOptions
case ErrNoRightsToDelete.ʔ():
return ErrNoRightsToDelete
case ErrNoRightsToRestrict.ʔ():
return ErrNoRightsToRestrict
case ErrNoRightsToSend.ʔ():
return ErrNoRightsToSend
case ErrNoRightsToSendGifs.ʔ():
return ErrNoRightsToSendGifs
case ErrNoRightsToSendPhoto.ʔ():
return ErrNoRightsToSendPhoto
case ErrNoRightsToSendStickers.ʔ():
return ErrNoRightsToSendStickers
case ErrNoRightsToSendGifs.ʔ():
return ErrNoRightsToSendGifs
case ErrNoRightsToDelete.ʔ():
return ErrNoRightsToDelete
case ErrKickingChatOwner.ʔ():
return ErrKickingChatOwner
case ErrBotKickedFromGroup.ʔ():
return ErrKickingChatOwner
case ErrBotKickedFromSuperGroup.ʔ():
return ErrBotKickedFromSuperGroup
case ErrWrongTypeOfContent.ʔ():
return ErrWrongTypeOfContent
case ErrBadURLContent.ʔ():
return ErrBadURLContent
case ErrWrongFileIDSymbol.ʔ():
return ErrWrongFileIDSymbol
case ErrWrongFileIDLength.ʔ():
return ErrWrongFileIDLength
case ErrWrongFileIDCharacter.ʔ():
return ErrWrongFileIDCharacter
case ErrNotFoundToDelete.ʔ():
return ErrNotFoundToDelete
case ErrNotFoundToForward.ʔ():
return ErrNotFoundToForward
case ErrNotFoundToReply.ʔ():
return ErrNotFoundToReply
case ErrQueryTooOld.ʔ():
return ErrQueryTooOld
case ErrSameMessageContent.ʔ():
return ErrSameMessageContent
case ErrStickerEmojisInvalid.ʔ():
return ErrStickerEmojisInvalid
case ErrStickerSetInvalid.ʔ():
return ErrStickerSetInvalid
case ErrStickerSetInvalidName.ʔ():
return ErrStickerSetInvalidName
case ErrStickerSetNameOccupied.ʔ():
return ErrStickerSetNameOccupied
case ErrTooLongMarkup.ʔ():
return ErrTooLongMarkup
case ErrTooLongMessage.ʔ():
return ErrTooLongMessage
case ErrUserIsAdmin.ʔ():
return ErrUserIsAdmin
case ErrWrongFileID.ʔ():
return ErrWrongFileID
case ErrTooLarge.ʔ():
return ErrTooLarge
case ErrWrongFileIDCharacter.ʔ():
return ErrWrongFileIDCharacter
case ErrWrongFileIDLength.ʔ():
return ErrWrongFileIDLength
case ErrWrongFileIDPadding.ʔ():
return ErrWrongFileIDPadding
case ErrFailedImageProcess.ʔ():
return ErrFailedImageProcess
case ErrInvalidStickerSet.ʔ():
return ErrInvalidStickerSet
case ErrGroupMigrated.ʔ():
return ErrGroupMigrated
case ErrWrongFileIDSymbol.ʔ():
return ErrWrongFileIDSymbol
case ErrWrongTypeOfContent.ʔ():
return ErrWrongTypeOfContent
case ErrWrongURL.ʔ():
return ErrWrongURL
case ErrBlockedByUser.ʔ():
return ErrBlockedByUser
case ErrKickedFromGroup.ʔ():
return ErrKickedFromGroup
case ErrKickedFromSuperGroup.ʔ():
return ErrKickedFromSuperGroup
case ErrNotStartedByUser.ʔ():
return ErrNotStartedByUser
case ErrUserIsDeactivated.ʔ():
return ErrUserIsDeactivated
default:
return nil
}

@ -9,20 +9,21 @@ import (
type File struct {
FileID string `json:"file_id"`
UniqueID string `json:"file_unique_id"`
FileName string `json:"file_name"`
FileSize int `json:"file_size"`
// file on telegram server https://core.telegram.org/bots/api#file
// FilePath is used for files on Telegram server.
FilePath string `json:"file_path"`
// file on local file system.
// FileLocal uis ed for files on local file system.
FileLocal string `json:"file_local"`
// file on the internet
// FileURL is used for file on the internet.
FileURL string `json:"file_url"`
// file backed with io.Reader
// FileReader is used for file backed with io.Reader.
FileReader io.Reader `json:"-"`
fileName string
}
// FromDisk constructs a new local (on-disk) file object.

@ -1,13 +1,13 @@
module gopkg.in/tucnak/telebot.v3
module gopkg.in/telebot.v3
go 1.13
require (
github.com/fatih/color v1.12.0 // indirect
github.com/goccy/go-yaml v1.9.1
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/pkg/errors v0.8.1
github.com/fatih/color v1.13.0 // indirect
github.com/goccy/go-yaml v1.9.5
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/spf13/cast v1.3.1
github.com/stretchr/testify v1.5.1
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
github.com/stretchr/testify v1.7.0
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
@ -12,17 +12,17 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/goccy/go-yaml v1.9.1 h1:mjQLyIjpKgFxCMbp1xXkYvyQHXB5hPsy0Dbjm+0L1zg=
github.com/goccy/go-yaml v1.9.1/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/goccy/go-yaml v1.9.5 h1:Eh/+3uk9kLxG4koCX6lRMAPS1OaMSAi+FJcya0INdB0=
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
@ -30,8 +30,8 @@ github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -40,8 +40,10 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -49,5 +51,7 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -3,8 +3,6 @@ package telebot
import (
"encoding/json"
"fmt"
"github.com/pkg/errors"
)
// Query is an incoming inline query. When the user sends
@ -86,7 +84,7 @@ type Result interface {
SetResultID(string)
SetParseMode(ParseMode)
SetContent(InputMessageContent)
SetReplyMarkup([][]InlineButton)
SetReplyMarkup(*ReplyMarkup)
Process(*Bot)
}
@ -134,7 +132,7 @@ func inferIQR(result Result) error {
case *StickerResult:
r.Type = "sticker"
default:
return errors.Errorf("telebot: result %v is not supported", result)
return fmt.Errorf("telebot: result %v is not supported", result)
}
return nil

@ -17,7 +17,7 @@ type ResultBase struct {
Content InputMessageContent `json:"input_message_content,omitempty"`
// Optional. Inline keyboard attached to the message.
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
}
// ResultID returns ResultBase.ID.
@ -41,8 +41,8 @@ func (r *ResultBase) SetContent(content InputMessageContent) {
}
// SetReplyMarkup sets ResultBase.ReplyMarkup.
func (r *ResultBase) SetReplyMarkup(keyboard [][]InlineButton) {
r.ReplyMarkup = &InlineKeyboardMarkup{InlineKeyboard: keyboard}
func (r *ResultBase) SetReplyMarkup(markup *ReplyMarkup) {
r.ReplyMarkup = markup
}
func (r *ResultBase) Process(b *Bot) {

@ -6,7 +6,7 @@ import (
"github.com/goccy/go-yaml"
"github.com/spf13/cast"
tele "gopkg.in/tucnak/telebot.v3"
tele "gopkg.in/telebot.v3"
)
// Config represents typed map interface related to the "config" section in layout.

@ -1,7 +1,7 @@
package layout
import (
tele "gopkg.in/tucnak/telebot.v3"
tele "gopkg.in/telebot.v3"
)
// DefaultLayout is a simplified layout instance with pre-defined locale by default.

@ -10,7 +10,7 @@ import (
"text/template"
"github.com/goccy/go-yaml"
tele "gopkg.in/tucnak/telebot.v3"
tele "gopkg.in/telebot.v3"
)
type (
@ -500,7 +500,7 @@ func (lt *Layout) ResultLocale(locale, k string, args ...interface{}) tele.Resul
if markup == nil {
log.Printf("telebot/layout: markup with name %s was not found\n", result.Markup)
} else {
r.SetReplyMarkup(markup.InlineKeyboard)
r.SetReplyMarkup(markup)
}
}

@ -6,7 +6,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
tele "gopkg.in/tucnak/telebot.v3"
tele "gopkg.in/telebot.v3"
)
func TestLayout(t *testing.T) {

@ -1,7 +1,7 @@
package layout
import (
tele "gopkg.in/tucnak/telebot.v3"
tele "gopkg.in/telebot.v3"
)
// LocaleFunc is the function used to fetch the locale of the recipient.

@ -9,7 +9,7 @@ import (
"text/template"
"github.com/goccy/go-yaml"
tele "gopkg.in/tucnak/telebot.v3"
tele "gopkg.in/telebot.v3"
)
type Settings struct {

@ -4,28 +4,52 @@ import (
"encoding/json"
)
// Album lets you group multiple media (so-called InputMedia)
// into a single message.
type Album []InputMedia
// Media is a generic type for all kinds of media that includes File.
type Media interface {
// MediaType returns string-represented media type.
MediaType() string
// InputMedia is a generic type for all kinds of media you
// can put into an album.
type InputMedia interface {
// As some files must be uploaded (instead of referencing)
// outer layers of Telebot require it.
// MediaFile returns a pointer to the media file.
MediaFile() *File
}
// InputMedia represents a composite InputMedia struct that is
// used by Telebot in sending and editing media methods.
type InputMedia struct {
Type string `json:"type"`
Media string `json:"media"`
Caption string `json:"caption"`
Thumbnail string `json:"thumb,omitempty"`
ParseMode string `json:"parse_mode,omitempty"`
Entities Entities `json:"caption_entities,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Duration int `json:"duration,omitempty"`
Title string `json:"title,omitempty"`
Performer string `json:"performer,omitempty"`
Streaming bool `json:"supports_streaming,omitempty"`
}
// Inputtable is a generic type for all kinds of media you
// can put into an album.
type Inputtable interface {
Media
// InputMedia returns already marshalled InputMedia type
// ready to be used in sending and editing media methods.
InputMedia() InputMedia
}
// Album lets you group multiple media into a single message.
type Album []Inputtable
// Photo object represents a single photo file.
type Photo struct {
File
Width int `json:"width"`
Height int `json:"height"`
// (Optional)
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
Width int `json:"width"`
Height int `json:"height"`
Caption string `json:"caption,omitempty"`
}
type photoSize struct {
@ -36,27 +60,36 @@ type photoSize struct {
Caption string `json:"caption,omitempty"`
}
// MediaFile returns &Photo.File
func (p *Photo) MediaType() string {
return "photo"
}
func (p *Photo) MediaFile() *File {
return &p.File
}
func (p *Photo) InputMedia() InputMedia {
return InputMedia{
Type: p.MediaType(),
Caption: p.Caption,
}
}
// UnmarshalJSON is custom unmarshaller required to abstract
// away the hassle of treating different thumbnail sizes.
// Instead, Telebot chooses the hi-res one and just sticks to it.
//
// I really do find it a beautiful solution.
func (p *Photo) UnmarshalJSON(jsonStr []byte) error {
func (p *Photo) UnmarshalJSON(data []byte) error {
var hq photoSize
if jsonStr[0] == '{' {
if err := json.Unmarshal(jsonStr, &hq); err != nil {
if data[0] == '{' {
if err := json.Unmarshal(data, &hq); err != nil {
return err
}
} else {
var sizes []photoSize
if err := json.Unmarshal(jsonStr, &sizes); err != nil {
if err := json.Unmarshal(data, &sizes); err != nil {
return err
}
@ -85,11 +118,25 @@ type Audio struct {
FileName string `json:"file_name,omitempty"`
}
// MediaFile returns &Audio.File
func (a *Audio) MediaType() string {
return "audio"
}
func (a *Audio) MediaFile() *File {
a.fileName = a.FileName
return &a.File
}
func (a *Audio) InputMedia() InputMedia {
return InputMedia{
Type: a.MediaType(),
Caption: a.Caption,
Duration: a.Duration,
Title: a.Title,
Performer: a.Performer,
}
}
// Document object represents a general file (as opposed to Photo or Audio).
// Telegram users can send files of any type of up to 1.5 GB in size.
type Document struct {
@ -102,11 +149,22 @@ type Document struct {
FileName string `json:"file_name,omitempty"`
}
// MediaFile returns &Document.File
func (d *Document) MediaType() string {
return "document"
}
func (d *Document) MediaFile() *File {
d.fileName = d.FileName
return &d.File
}
func (d *Document) InputMedia() InputMedia {
return InputMedia{
Type: d.MediaType(),
Caption: d.Caption,
}
}
// Video object represents a video file.
type Video struct {
File
@ -116,18 +174,33 @@ type Video struct {
Duration int `json:"duration,omitempty"`
// (Optional)
Caption string `json:"caption,omitempty"`
Thumbnail *Photo `json:"thumb,omitempty"`
SupportsStreaming bool `json:"supports_streaming,omitempty"`
MIME string `json:"mime_type,omitempty"`
FileName string `json:"file_name,omitempty"`
Caption string `json:"caption,omitempty"`
Thumbnail *Photo `json:"thumb,omitempty"`
Streaming bool `json:"supports_streaming,omitempty"`
MIME string `json:"mime_type,omitempty"`
FileName string `json:"file_name,omitempty"`
}
func (v *Video) MediaType() string {
return "video"
}
// MediaFile returns &Video.File
func (v *Video) MediaFile() *File {
v.fileName = v.FileName
return &v.File
}
func (v *Video) InputMedia() InputMedia {
return InputMedia{
Type: v.MediaType(),
Caption: v.Caption,
Width: v.Width,
Height: v.Height,
Duration: v.Duration,
Streaming: v.Streaming,
}
}
// Animation object represents a animation file.
type Animation struct {
File
@ -143,11 +216,25 @@ type Animation struct {
FileName string `json:"file_name,omitempty"`
}
// MediaFile returns &Animation.File
func (a *Animation) MediaType() string {
return "animation"
}
func (a *Animation) MediaFile() *File {
a.fileName = a.FileName
return &a.File
}
func (a *Animation) InputMedia() InputMedia {
return InputMedia{
Type: a.MediaType(),
Caption: a.Caption,
Width: a.Width,
Height: a.Height,
Duration: a.Duration,
}
}
// Voice object represents a voice note.
type Voice struct {
File
@ -159,10 +246,18 @@ type Voice struct {
MIME string `json:"mime_type,omitempty"`
}
// VideoNote represents a video message (available in Telegram apps
// as of v.4.0).
func (v *Voice) MediaType() string {
return "voice"
}
func (v *Voice) MediaFile() *File {
return &v.File
}
// VideoNote represents a video message.
type VideoNote struct {
File
Duration int `json:"duration"`
// (Optional)
@ -170,14 +265,42 @@ type VideoNote struct {
Length int `json:"length,omitempty"`
}
// Contact object represents a contact to Telegram user
func (v *VideoNote) MediaType() string {
return "videoNote"
}
func (v *VideoNote) MediaFile() *File {
return &v.File
}
// Sticker object represents a WebP image, so-called sticker.
type Sticker struct {
File
Width int `json:"width"`
Height int `json:"height"`
Animated bool `json:"is_animated"`
Thumbnail *Photo `json:"thumb"`
Emoji string `json:"emoji"`
SetName string `json:"set_name"`
MaskPosition *MaskPosition `json:"mask_position"`
}
func (s *Sticker) MediaType() string {
return "sticker"
}
func (s *Sticker) MediaFile() *File {
return &s.File
}
// Contact object represents a contact to Telegram user.
type Contact struct {
PhoneNumber string `json:"phone_number"`
FirstName string `json:"first_name"`
// (Optional)
LastName string `json:"last_name"`
UserID int `json:"user_id,omitempty"`
UserID int64 `json:"user_id,omitempty"`
}
// Location object represents geographic position.
@ -193,14 +316,6 @@ type Location struct {
LivePeriod int `json:"live_period,omitempty"`
}
// ProximityAlert sent whenever
// a user in the chat triggers a proximity alert set by another user.
type ProximityAlert struct {
Traveler *User `json:"traveler,omitempty"`
Watcher *User `json:"watcher,omitempty"`
Distance int `json:"distance"`
}
// Venue object represents a venue location with name, address and
// optional foursquare ID.
type Venue struct {

@ -3,6 +3,7 @@ package telebot
import (
"strconv"
"time"
"unicode/utf16"
)
// Message object represents a message.
@ -72,14 +73,14 @@ type Message struct {
// For text messages, special entities like usernames, URLs, bot commands,
// etc. that appear in the text.
Entities []MessageEntity `json:"entities,omitempty"`
Entities Entities `json:"entities,omitempty"`
// Some messages containing media, may as well have a caption.
Caption string `json:"caption,omitempty"`
// For messages with a caption, special entities like usernames, URLs,
// bot commands, etc. that appear in the caption.
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
CaptionEntities Entities `json:"caption_entities,omitempty"`
// For an audio recording, information about it.
Audio *Audio `json:"audio"`
@ -239,7 +240,7 @@ type Message struct {
AutoDeleteTimer *AutoDeleteTimer `json:"message_auto_delete_timer_changed,omitempty"`
// Inline keyboard attached to the message.
ReplyMarkup InlineKeyboardMarkup `json:"reply_markup"`
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
}
// MessageEntity object represents "special" parts of text messages,
@ -266,6 +267,17 @@ type MessageEntity struct {
Language string `json:"language,omitempty"`
}
// Entities is used to set message's text entities as a send option.
type Entities []MessageEntity
// ProximityAlert sent whenever a user in the chat triggers
// a proximity alert set by another user.
type ProximityAlert struct {
Traveler *User `json:"traveler,omitempty"`
Watcher *User `json:"watcher,omitempty"`
Distance int `json:"distance"`
}
// AutoDeleteTimer represents a service message about a change in auto-delete timer settings.
type AutoDeleteTimer struct {
Unixtime int `json:"message_auto_delete_time"`
@ -333,3 +345,48 @@ func (m *Message) IsService() bool {
return fact
}
// EntityText returns the substring of the message identified by the
// given MessageEntity.
//
// It's safer than manually slicing Text because Telegram uses
// UTF-16 indices whereas Go string are []byte.
//
func (m *Message) EntityText(e MessageEntity) string {
text := m.Text
if text == "" {
text = m.Caption
}
a := utf16.Encode([]rune(text))
off, end := e.Offset, e.Offset+e.Length
if off < 0 || end > len(a) {
return ""
}
return string(utf16.Decode(a[off:end]))
}
// Media returns the message's media if it contains either photo,
// voice, audio, animation, document, video or video note.
func (m *Message) Media() Media {
switch {
case m.Photo != nil:
return m.Photo
case m.Voice != nil:
return m.Voice
case m.Audio != nil:
return m.Audio
case m.Animation != nil:
return m.Animation
case m.Document != nil:
return m.Document
case m.Video != nil:
return m.Video
case m.VideoNote != nil:
return m.VideoNote
default:
return nil
}
}

@ -4,7 +4,7 @@ import (
"encoding/json"
"log"
tele "gopkg.in/tucnak/telebot.v3"
tele "gopkg.in/telebot.v3"
)
func Logger(logger ...*log.Logger) tele.MiddlewareFunc {

@ -1,6 +1,6 @@
package middleware
import tele "gopkg.in/tucnak/telebot.v3"
import tele "gopkg.in/telebot.v3"
func AutoRespond() tele.MiddlewareFunc {
return func(next tele.HandlerFunc) tele.HandlerFunc {

@ -1,6 +1,6 @@
package middleware
import tele "gopkg.in/tucnak/telebot.v3"
import tele "gopkg.in/telebot.v3"
type RestrictConfig struct {
Chats []int64

@ -66,6 +66,9 @@ type SendOptions struct {
// ParseMode controls how client apps render your message.
ParseMode ParseMode
// Entities is a list of special entities that appear in message text, which can be specified instead of parse_mode.
Entities Entities
// DisableContentDetection abilities to disable server-side file content type detection.
DisableContentDetection bool
@ -165,14 +168,6 @@ type ReplyButton struct {
Poll PollType `json:"request_poll,omitempty"`
}
// InlineKeyboardMarkup represents an inline keyboard that appears
// right next to the message it belongs to.
type InlineKeyboardMarkup struct {
// Array of button rows, each represented by
// an Array of KeyboardButton objects.
InlineKeyboard [][]InlineButton `json:"inline_keyboard,omitempty"`
}
// MarshalJSON implements json.Marshaler. It allows to pass
// PollType as keyboard's poll type instead of KeyboardButtonPollType object.
func (pt PollType) MarshalJSON() ([]byte, error) {

@ -86,7 +86,7 @@ type LongPoller struct {
// poll
// poll_answer
//
AllowedUpdates []string
AllowedUpdates []string `yaml:"allowed_updates"`
}
// Poll does long polling.

@ -32,7 +32,7 @@ func (p *Photo) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
}
b.embedSendOptions(params, opt)
msg, err := b.sendObject(&p.File, "photo", params, nil)
msg, err := b.sendMedia(p, params, nil)
if err != nil {
return nil, err
}
@ -59,7 +59,7 @@ func (a *Audio) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params["duration"] = strconv.Itoa(a.Duration)
}
msg, err := b.sendObject(&a.File, "audio", params, thumbnailToFilemap(a.Thumbnail))
msg, err := b.sendMedia(a, params, thumbnailToFilemap(a.Thumbnail))
if err != nil {
return nil, err
}
@ -91,7 +91,7 @@ func (d *Document) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error
params["file_size"] = strconv.Itoa(d.FileSize)
}
msg, err := b.sendObject(&d.File, "document", params, thumbnailToFilemap(d.Thumbnail))
msg, err := b.sendMedia(d, params, thumbnailToFilemap(d.Thumbnail))
if err != nil {
return nil, err
}
@ -110,7 +110,7 @@ func (s *Sticker) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error)
}
b.embedSendOptions(params, opt)
msg, err := b.sendObject(&s.File, "sticker", params, nil)
msg, err := b.sendMedia(s, params, nil)
if err != nil {
return nil, err
}
@ -139,11 +139,11 @@ func (v *Video) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
if v.Height != 0 {
params["height"] = strconv.Itoa(v.Height)
}
if v.SupportsStreaming {
if v.Streaming {
params["supports_streaming"] = "true"
}
msg, err := b.sendObject(&v.File, "video", params, thumbnailToFilemap(v.Thumbnail))
msg, err := b.sendMedia(v, params, thumbnailToFilemap(v.Thumbnail))
if err != nil {
return nil, err
}
@ -165,7 +165,6 @@ func (v *Video) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
}
// Send delivers animation through bot b to recipient.
// @see https://core.telegram.org/bots/api#sendanimation
func (a *Animation) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
@ -184,29 +183,29 @@ func (a *Animation) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, erro
params["height"] = strconv.Itoa(a.Height)
}
// file_name is required, without file_name GIFs sent as document
// file_name is required, without it animation sends as a document
if params["file_name"] == "" && a.File.OnDisk() {
params["file_name"] = filepath.Base(a.File.FileLocal)
}
msg, err := b.sendObject(&a.File, "animation", params, nil)
msg, err := b.sendMedia(a, params, thumbnailToFilemap(a.Thumbnail))
if err != nil {
return nil, err
}
if msg.Animation != nil {
msg.Animation.File.stealRef(&a.File)
if anim := msg.Animation; anim != nil {
anim.File.stealRef(&a.File)
*a = *msg.Animation
} else {
} else if doc := msg.Document; doc != nil {
*a = Animation{
File: msg.Document.File,
Thumbnail: msg.Document.Thumbnail,
MIME: msg.Document.MIME,
FileName: msg.Document.FileName,
File: doc.File,
Thumbnail: doc.Thumbnail,
MIME: doc.MIME,
FileName: doc.FileName,
}
}
a.Caption = msg.Caption
a.Caption = msg.Caption
return msg, nil
}
@ -222,7 +221,7 @@ func (v *Voice) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params["duration"] = strconv.Itoa(v.Duration)
}
msg, err := b.sendObject(&v.File, "voice", params, nil)
msg, err := b.sendMedia(v, params, nil)
if err != nil {
return nil, err
}
@ -247,7 +246,7 @@ func (v *VideoNote) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, erro
params["length"] = strconv.Itoa(v.Length)
}
msg, err := b.sendObject(&v.File, "videoNote", params, thumbnailToFilemap(v.Thumbnail))
msg, err := b.sendMedia(v, params, thumbnailToFilemap(v.Thumbnail))
if err != nil {
return nil, err
}

@ -5,18 +5,6 @@ import (
"strconv"
)
// Sticker object represents a WebP image, so-called sticker.
type Sticker struct {
File
Width int `json:"width"`
Height int `json:"height"`
Animated bool `json:"is_animated"`
Thumbnail *Photo `json:"thumb"`
Emoji string `json:"emoji"`
SetName string `json:"set_name"`
MaskPosition *MaskPosition `json:"mask_position"`
}
// StickerSet represents a sticker set.
type StickerSet struct {
Name string `json:"name"`

@ -27,7 +27,7 @@
//
package telebot
import "github.com/pkg/errors"
import "errors"
var (
ErrBadRecipient = errors.New("telebot: recipient is nil")
@ -59,13 +59,13 @@ const (
OnContact = "\acontact"
OnLocation = "\alocation"
OnVenue = "\avenue"
OnPinned = "\apinned"
OnDice = "\adice"
OnInvoice = "\ainvoice"
OnPayment = "\apayment"
OnGame = "\agame"
OnPoll = "\apoll"
OnPollAnswer = "\apoll_answer"
OnPinned = "\apinned"
// Will fire on channel posts.
OnChannelPost = "\achannel_post"
@ -90,6 +90,9 @@ const (
// upon switching as its ID will change.
OnMigration = "\amigration"
// Will fire on any unhandled media.
OnMedia = "\amedia"
// Will fire on callback requests.
OnCallback = "\acallback"
@ -111,6 +114,9 @@ const (
// Will fire on chat member's changes.
OnChatMember = "\achat_member"
// Will fire on chat join request.
OnChatJoinRequest = "\achat_join_request"
// Will fire on the start of a voice chat.
OnVoiceChatStarted = "\avoice_chat_started"
@ -144,6 +150,7 @@ const (
RecordingAudio ChatAction = "record_audio"
RecordingVNote ChatAction = "record_video_note"
FindingLocation ChatAction = "find_location"
ChoosingSticker ChatAction = "choose_sticker"
)
// ParseMode determines the way client applications treat the text of the message

@ -7,10 +7,12 @@ import (
"log"
"net/http"
"strconv"
"github.com/pkg/errors"
)
var defaultOnError = func(err error, c Context) {
log.Println(c.Update().ID, err)
}
func (b *Bot) debug(err error) {
log.Println(err)
}
@ -20,7 +22,7 @@ func (b *Bot) deferDebug() {
if err, ok := r.(error); ok {
b.debug(err)
} else if str, ok := r.(string); ok {
b.debug(errors.Errorf("%s", str))
b.debug(fmt.Errorf("%s", str))
}
}
}
@ -29,11 +31,7 @@ func (b *Bot) runHandler(h HandlerFunc, c Context) {
f := func() {
defer b.deferDebug()
if err := h(c); err != nil {
if b.OnError != nil {
b.OnError(err, c)
} else {
log.Println(err)
}
b.OnError(err, c)
}
}
if b.synchronous {
@ -52,59 +50,56 @@ func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc {
// wrapError returns new wrapped telebot-related error.
func wrapError(err error) error {
return errors.Wrap(err, "telebot")
return fmt.Errorf("telebot: %w", err)
}
// extractOk checks given result for error. If result is ok returns nil.
// In other cases it extracts API error. If error is not presented
// in errors.go, it will be prefixed with `unknown` keyword.
func extractOk(data []byte) error {
// Parse the error message as JSON
var tgramApiError struct {
var e struct {
Ok bool `json:"ok"`
ErrorCode int `json:"error_code"`
Code int `json:"error_code"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
}
jdecoder := json.NewDecoder(bytes.NewReader(data))
jdecoder.UseNumber()
err := jdecoder.Decode(&tgramApiError)
if err != nil {
//return errors.Wrap(err, "can't parse JSON reply, the Telegram server is mibehaving")
// FIXME / TODO: in this case the error might be at HTTP level, or the content is not JSON (eg. image?)
return nil
if json.NewDecoder(bytes.NewReader(data)).Decode(&e) != nil {
return nil // FIXME
}
if tgramApiError.Ok {
// No error
if e.Ok {
return nil
}
err = ErrByDescription(tgramApiError.Description)
if err != nil {
apierr, _ := err.(*APIError)
// Formally this is wrong, as the error is not created on the fly
// However, given the current way of handling errors, this a working
// workaround which doesn't break the API
apierr.Parameters = tgramApiError.Parameters
return apierr
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 tgramApiError.ErrorCode {
switch e.Code {
case http.StatusTooManyRequests:
retryAfter, ok := tgramApiError.Parameters["retry_after"]
retryAfter, ok := e.Parameters["retry_after"]
if !ok {
return NewAPIError(429, tgramApiError.Description)
return NewError(e.Code, e.Description)
}
retryAfterInt, _ := strconv.Atoi(fmt.Sprint(retryAfter))
err = FloodError{
APIError: NewAPIError(429, tgramApiError.Description),
RetryAfter: retryAfterInt,
err: NewError(e.Code, e.Description),
RetryAfter: int(retryAfter.(float64)),
}
default:
err = fmt.Errorf("telegram unknown: %s (%d)", tgramApiError.Description, tgramApiError.ErrorCode)
err = fmt.Errorf("telegram: %s (%d)", e.Description, e.Code)
}
return err
@ -132,24 +127,17 @@ func extractMessage(data []byte) (*Message, error) {
}
func extractOptions(how []interface{}) *SendOptions {
var opts *SendOptions
opts := &SendOptions{}
for _, prop := range how {
switch opt := prop.(type) {
case *SendOptions:
opts = opt.copy()
case *ReplyMarkup:
if opts == nil {
opts = &SendOptions{}
}
if opt != nil {
opts.ReplyMarkup = opt.copy()
}
case Option:
if opts == nil {
opts = &SendOptions{}
}
switch opt {
case NoPreview:
opts.DisableWebPagePreview = true
@ -174,10 +162,9 @@ func extractOptions(how []interface{}) *SendOptions {
panic("telebot: unsupported flag-option")
}
case ParseMode:
if opts == nil {
opts = &SendOptions{}
}
opts.ParseMode = opt
case Entities:
opts.Entities = opt
default:
panic("telebot: unsupported send-option")
}
@ -211,6 +198,17 @@ func (b *Bot) embedSendOptions(params map[string]string, opt *SendOptions) {
params["parse_mode"] = opt.ParseMode
}
if len(opt.Entities) > 0 {
delete(params, "parse_mode")
entities, _ := json.Marshal(opt.Entities)
if params["caption"] != "" {
params["caption_entities"] = string(entities)
} else {
params["entities"] = string(entities)
}
}
if opt.DisableContentDetection {
params["disable_content_type_detection"] = "true"
}

@ -8,20 +8,37 @@ import (
)
func TestExtractOk(t *testing.T) {
data := []byte(`{"ok":true,"result":{"foo":"bar"}}`)
data := []byte(`{"ok": true, "result": {}}`)
require.NoError(t, extractOk(data))
data = []byte(`{"ok":false,"error_code":400,"description":"Bad Request: reply message not found"}`)
assert.EqualError(t, extractOk(data), ErrToReplyNotFound.Error())
data = []byte(`{
"ok": false,
"error_code": 400,
"description": "Bad Request: reply message not found"
}`)
assert.EqualError(t, extractOk(data), ErrNotFoundToReply.Error())
data = []byte(`{"ok":false,"error_code":429,"description":"Too Many Requests: retry after 8","parameters":{"retry_after":8}}`)
data = []byte(`{
"ok": false,
"error_code": 429,
"description": "Too Many Requests: retry after 8",
"parameters": {"retry_after": 8}
}`)
assert.Equal(t, FloodError{
APIError: NewAPIError(429, "Too Many Requests: retry after 8"),
err: NewError(429, "Too Many Requests: retry after 8"),
RetryAfter: 8,
}, extractOk(data))
data = []byte(`{"ok":false,"error_code":400,"description":"Bad Request: group chat was upgraded to a supergroup chat","parameters":{"migrate_to_chat_id": -1234}}`)
assert.EqualError(t, extractOk(data), ErrGroupMigrated.Error())
data = []byte(`{
"ok": false,
"error_code": 400,
"description": "Bad Request: group chat was upgraded to a supergroup chat",
"parameters": {"migrate_to_chat_id": -100123456789}
}`)
assert.Equal(t, GroupError{
err: ErrGroupMigrated,
MigratedTo: -100123456789,
}, extractOk(data))
}
func TestExtractMessage(t *testing.T) {

Loading…
Cancel
Save