This commit is contained in:
Кобелев Андрей Андреевич 2023-02-01 12:08:20 +03:00
commit 1b497f2b9b
57 changed files with 10640 additions and 0 deletions

17
.github/workflows/go.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: Go
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: Test
run: export TELEBOT_SECRET=${{ secrets.TELEBOT_SECRET }} && export CHAT_ID=${{ secrets.CHAT_ID }} && export USER_ID=${{ secrets.USER_ID }} && go test -v ./...

34
.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
.idea
.DS_Store
coverage.txt
# Terraform artifacts
*.zip
.terraform*
terraform*
/examples/awslambdaechobot/awslambdaechobot

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 llya Kowalewski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

489
README.md Normal file
View File

@ -0,0 +1,489 @@
# Telebot
>"I never knew creating Telegram bots could be so _sexy_!"
[![GoDoc](https://godoc.org/gopkg.in/telebot.v3?status.svg)](https://godoc.org/gopkg.in/telebot.v3)
[![GitHub Actions](https://github.com/tucnak/telebot/actions/workflows/go.yml/badge.svg)](https://github.com/tucnak/telebot/actions)
[![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/telebot.v3
```
* [Overview](#overview)
* [Getting Started](#getting-started)
- [Context](#context)
- [Middleware](#middleware)
- [Poller](#poller)
- [Commands](#commands)
- [Files](#files)
- [Sendable](#sendable)
- [Editable](#editable)
- [Keyboards](#keyboards)
- [Inline mode](#inline-mode)
* [Contributing](#contributing)
* [Donate](#donate)
* [License](#license)
# Overview
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 strong sides of Telebot are:
* Real concise API
* Command routing
* Middleware
* 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
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:
```go
package main
import (
"log"
"os"
"time"
tele "gopkg.in/telebot.v3"
)
func main() {
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(c tele.Context) error {
return c.Send("Hello!")
})
b.Start()
}
```
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 into one of the Telebot-provided
endpoints. You can find the full list
[here](https://godoc.org/gopkg.in/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.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()
)
// 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.OnChannelPost, func(c tele.Context) error {
// Channel posts only.
msg := c.Message()
})
b.Handle(tele.OnPhoto, func(c tele.Context) error {
// Photos only.
photo := c.Message().Photo
})
b.Handle(tele.OnQuery, func(c tele.Context) error {
// Incoming inline queries.
return c.Answer(...)
})
```
## 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()
adminOnly.Use(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:
```go
// Poller is a provider of Updates.
//
// All pollers must implement Poll(), which accepts bot
// pointer and subscription channel and start polling
// synchronously straight away.
type Poller interface {
// Poll is supposed to take the bot object
// subscription channel and start polling
// for Updates immediately.
//
// Poller must listen for stop constantly and close
// it as soon as it's done polling.
Poll(b *Bot, updates chan Update, stop chan struct{})
}
```
## 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:
```go
// Command: /start <PAYLOAD>
b.Handle("/start", func(c tele.Context) error {
fmt.Println(c.Message().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 50 MB in size.
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")}
fmt.Println(a.OnDisk()) // true
fmt.Println(a.InCloud()) // false
// 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
b.Send(otherRecipient, a)
fmt.Println(a.OnDisk()) // true
fmt.Println(a.InCloud()) // true
fmt.Println(a.FileID) // <Telegram file ID>
```
You might want to save certain `File`s in order to avoid re-uploading. Feel free
to marshal them into whatever format, `File` only contain public fields, so no
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`.
If you create composite types of your own, and they satisfy the `Sendable` interface,
Telebot will be able to send them out.
```go
// Sendable is any object that can send itself.
//
// This is pretty cool, since it lets bots implement
// custom Sendables for complex kinds of media or
// chat objects spanning across multiple messages.
type Sendable interface {
Send(*Bot, Recipient, *SendOptions) (*Message, error)
}
```
The only type at the time that doesn't fit `Send()` is `Album` and there is a reason
for that. Albums were added not so long ago, so they are slightly quirky for backwards
compatibilities sake. In fact, an `Album` can be sent, but never received. Instead,
Telegram returns a `[]Message`, one for each media object in the album:
```go
p := &tele.Photo{File: tele.FromDisk("chicken.jpg")}
v := &tele.Video{File: tele.FromURL("http://video.mp4")}
msgs, err := b.SendAlbum(user, tele.Album{p, v})
```
### Send options
Send options are objects and flags you can pass to `Send()`, `Edit()` and friends
as optional arguments (following the recipient and the text/media). The most
important one is called `SendOptions`, it lets you control _all_ the properties of
the message supported by Telegram. The only drawback is that it's rather
inconvenient to use at times, so `Send()` supports multiple shorthands:
```go
// regular send options
b.Send(user, "text", &tele.SendOptions{
// ...
})
// ReplyMarkup is a part of SendOptions,
// but often it's the only option you need
b.Send(user, "text", &tele.ReplyMarkup{
// ...
})
// flags: no notification && no web link preview
b.Send(user, "text", tele.Silent, tele.NoPreview)
```
Full list of supported option-flags you can find
[here](https://pkg.go.dev/gopkg.in/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 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`:
```go
// Editable is an interface for all objects that
// provide "message signature", a pair of 32-bit
// message ID and 64-bit chat ID, both required
// for edit operations.
//
// Use case: DB model struct for messages to-be
// edited with, say two columns: msg_id,chat_id
// could easily implement MessageSig() making
// instances of stored messages editable.
type Editable interface {
// MessageSig is a "message signature".
//
// For inline messages, return chatID = 0.
MessageSig() (messageID int, chatID int64)
}
```
For example, `Message` type is Editable. Here is the implementation of `StoredMessage`
type, provided by Telebot:
```go
// StoredMessage is an example struct suitable for being
// stored in the database as-is or being embedded into
// a larger struct, which is often the case (you might
// want to store some metadata alongside, or might not.)
type StoredMessage struct {
MessageID int `sql:"message_id" json:"message_id"`
ChatID int64 `sql:"chat_id" json:"chat_id"`
}
func (x StoredMessage) MessageSig() (int, int64) {
return x.MessageID, x.ChatID
}
```
Why bother at all? Well, it allows you to do things like this:
```go
// just two integer columns in the database
var msgs []tele.StoredMessage
db.Find(&msgs) // gorm syntax
for _, msg := range msgs {
bot.Edit(&msg, "Updated text")
// or
bot.Delete(&msg)
}
```
I find it incredibly neat. Worth noting, at this point of time there exists
another method in the Edit family, `EditCaption()` which is of a pretty
rare use, so I didn't bother including it to `Edit()`, just like I did with
`SendAlbum()` as it would inevitably lead to unnecessary complications.
```go
var m *Message
// change caption of a photo, audio, etc.
bot.EditCaption(m, "new caption")
```
## Keyboards
Telebot supports both kinds of keyboards Telegram provides: reply and inline
keyboards. Any button can also act as endpoints for `Handle()`.
```go
var (
// Universal markup builders.
menu = &tele.ReplyMarkup{ResizeKeyboard: true}
selector = &tele.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
// since it's required 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),
)
b.Handle("/start", func(c tele.Context) error {
return c.Send("Hello!", menu)
})
// On reply button pressed (message)
b.Handle(&btnHelp, func(c tele.Context) error {
return c.Edit("Here is some help: ...")
})
// 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 button:
```go
r := b.NewMarkup()
// Reply buttons:
r.Text("Hello!")
r.Contact("Send phone number")
r.Location("Send location")
r.Poll(tele.PollQuiz)
// Inline buttons:
r.Data("Show help", "help") // data is optional
r.Data("Delete item", "delete", item.ID)
r.URL("Visit", "https://google.com")
r.Query("Search", query)
r.QueryChat("Share", query)
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 what it looks like:
```go
b.Handle(tele.OnQuery, func(c tele.Context) error {
urls := []string{
"http://photo.jpg",
"http://photo2.jpg",
}
results := make(tele.Results, len(urls)) // []tele.Result
for i, url := range urls {
result := &tele.PhotoResult{
URL: url,
ThumbURL: url, // required for photos
}
results[i] = result
// needed to set a unique string ID for each result
results[i].SetResultID(strconv.Itoa(i))
}
return c.Answer(&tele.QueryResponse{
Results: results,
CacheTime: 60, // a minute
})
})
```
There's not much to talk about really. It also supports some form of authentication
through deep-linking. For that, use fields `SwitchPMText` and `SwitchPMParameter`
of `QueryResponse`.
# Contributing
1. Fork it
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 v3-feature`
7. Pull request
# Donate
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!
Litecoin: `ltc1qskt5ltrtyg7esfjm0ftx6jnacwffhpzpqmerus`
Ethereum: `0xB78A2Ac1D83a0aD0b993046F9fDEfC5e619efCAB`
# License
Telebot is distributed under MIT.

287
admin.go Normal file
View File

@ -0,0 +1,287 @@
package telebot
import (
"encoding/json"
"strconv"
"time"
)
// Rights is a list of privileges available to chat members.
type Rights struct {
// Anonymous is true, if the user's presence in the chat is hidden.
Anonymous bool `json:"is_anonymous"`
CanBeEdited bool `json:"can_be_edited"`
CanChangeInfo bool `json:"can_change_info"`
CanPostMessages bool `json:"can_post_messages"`
CanEditMessages bool `json:"can_edit_messages"`
CanDeleteMessages bool `json:"can_delete_messages"`
CanPinMessages bool `json:"can_pin_messages"`
CanInviteUsers bool `json:"can_invite_users"`
CanRestrictMembers bool `json:"can_restrict_members"`
CanPromoteMembers bool `json:"can_promote_members"`
CanSendMessages bool `json:"can_send_messages"`
CanSendMedia bool `json:"can_send_media_messages"`
CanSendPolls bool `json:"can_send_polls"`
CanSendOther bool `json:"can_send_other_messages"`
CanAddPreviews bool `json:"can_add_web_page_previews"`
CanManageVideoChats bool `json:"can_manage_video_chats"`
CanManageChat bool `json:"can_manage_chat"`
}
// NoRights is the default Rights{}.
func NoRights() Rights { return Rights{} }
// NoRestrictions should be used when un-restricting or
// un-promoting user.
//
// member.Rights = tele.NoRestrictions()
// b.Restrict(chat, member)
//
func NoRestrictions() Rights {
return Rights{
CanBeEdited: true,
CanChangeInfo: false,
CanPostMessages: false,
CanEditMessages: false,
CanDeleteMessages: false,
CanInviteUsers: false,
CanRestrictMembers: false,
CanPinMessages: false,
CanPromoteMembers: false,
CanSendMessages: true,
CanSendMedia: true,
CanSendPolls: true,
CanSendOther: true,
CanAddPreviews: true,
CanManageVideoChats: false,
CanManageChat: false,
}
}
// AdminRights could be used to promote user to admin.
func AdminRights() Rights {
return Rights{
CanBeEdited: true,
CanChangeInfo: true,
CanPostMessages: true,
CanEditMessages: true,
CanDeleteMessages: true,
CanInviteUsers: true,
CanRestrictMembers: true,
CanPinMessages: true,
CanPromoteMembers: true,
CanSendMessages: true,
CanSendMedia: true,
CanSendPolls: true,
CanSendOther: true,
CanAddPreviews: true,
CanManageVideoChats: true,
CanManageChat: true,
}
}
// Forever is a ExpireUnixtime of "forever" banning.
func Forever() int64 {
return time.Now().Add(367 * 24 * time.Hour).Unix()
}
// Ban will ban user from chat until `member.RestrictedUntil`.
func (b *Bot) Ban(chat *Chat, member *ChatMember, revokeMessages ...bool) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"user_id": member.User.Recipient(),
"until_date": strconv.FormatInt(member.RestrictedUntil, 10),
}
if len(revokeMessages) > 0 {
params["revoke_messages"] = strconv.FormatBool(revokeMessages[0])
}
_, err := b.Raw("kickChatMember", params)
return err
}
// Unban will unban user from chat, who would have thought eh?
// forBanned does nothing if the user is not banned.
func (b *Bot) Unban(chat *Chat, user *User, forBanned ...bool) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"user_id": user.Recipient(),
}
if len(forBanned) > 0 {
params["only_if_banned"] = strconv.FormatBool(forBanned[0])
}
_, err := b.Raw("unbanChatMember", params)
return err
}
// Restrict lets you restrict a subset of member's rights until
// member.RestrictedUntil, such as:
//
// * can send messages
// * can send media
// * can send other
// * can add web page previews
//
func (b *Bot) Restrict(chat *Chat, member *ChatMember) error {
prv, until := member.Rights, member.RestrictedUntil
params := map[string]interface{}{
"chat_id": chat.Recipient(),
"user_id": member.User.Recipient(),
"until_date": strconv.FormatInt(until, 10),
}
embedRights(params, prv)
_, err := b.Raw("restrictChatMember", params)
return err
}
// Promote lets you update member's admin rights, such as:
//
// * can change info
// * can post messages
// * can edit messages
// * can delete messages
// * can invite users
// * can restrict members
// * can pin messages
// * can promote members
//
func (b *Bot) Promote(chat *Chat, member *ChatMember) error {
prv := member.Rights
params := map[string]interface{}{
"chat_id": chat.Recipient(),
"user_id": member.User.Recipient(),
"is_anonymous": member.Anonymous,
}
embedRights(params, prv)
_, err := b.Raw("promoteChatMember", params)
return err
}
// AdminsOf returns a member list of chat admins.
//
// On success, returns an Array of ChatMember objects that
// contains information about all chat administrators except other bots.
//
// If the chat is a group or a supergroup and
// no administrators were appointed, only the creator will be returned.
//
func (b *Bot) AdminsOf(chat *Chat) ([]ChatMember, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
}
data, err := b.Raw("getChatAdministrators", params)
if err != nil {
return nil, err
}
var resp struct {
Result []ChatMember
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
}
// Len returns the number of members in a chat.
func (b *Bot) Len(chat *Chat) (int, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
}
data, err := b.Raw("getChatMembersCount", params)
if err != nil {
return 0, err
}
var resp struct {
Result int
}
if err := json.Unmarshal(data, &resp); err != nil {
return 0, wrapError(err)
}
return resp.Result, nil
}
// SetAdminTitle sets a custom title for an administrator.
// A title should be 0-16 characters length, emoji are not allowed.
func (b *Bot) SetAdminTitle(chat *Chat, user *User, title string) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"user_id": user.Recipient(),
"custom_title": title,
}
_, err := b.Raw("setChatAdministratorCustomTitle", params)
return err
}
// BanSenderChat will use this method to ban a channel chat in a supergroup or a channel.
// Until the chat is unbanned, the owner of the banned chat won't be able
// to send messages on behalf of any of their channels.
func (b *Bot) BanSenderChat(chat *Chat, sender Recipient) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"sender_chat_id": sender.Recipient(),
}
_, err := b.Raw("banChatSenderChat", params)
return err
}
// UnbanSenderChat will use this method to unban a previously banned channel chat in a supergroup or channel.
// The bot must be an administrator for this to work and must have the appropriate administrator rights.
func (b *Bot) UnbanSenderChat(chat *Chat, sender Recipient) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"sender_chat_id": sender.Recipient(),
}
_, err := b.Raw("unbanChatSenderChat", params)
return err
}
// DefaultRights returns the current default administrator rights of the bot.
func (b *Bot) DefaultRights(forChannels bool) (*Rights, error) {
params := map[string]bool{
"for_channels": forChannels,
}
data, err := b.Raw("getMyDefaultAdministratorRights", params)
if err != nil {
return nil, err
}
var resp struct {
Result *Rights
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return resp.Result, nil
}
// SetDefaultRights changes the default administrator rights requested by the bot
// when it's added as an administrator to groups or channels.
func (b *Bot) SetDefaultRights(rights Rights, forChannels bool) error {
params := map[string]interface{}{
"rights": rights,
"for_channels": forChannels,
}
_, err := b.Raw("setMyDefaultAdministratorRights", params)
return err
}
func embedRights(p map[string]interface{}, rights Rights) {
data, _ := json.Marshal(rights)
_ = json.Unmarshal(data, &p)
}

39
admin_test.go Normal file
View File

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

331
api.go Normal file
View File

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

120
api_test.go Normal file
View File

@ -0,0 +1,120 @@
package telebot
import (
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
)
// testPayload implements json.Marshaler
// to test json encoding error behaviour.
type testPayload struct{}
func (testPayload) MarshalJSON() ([]byte, error) {
return nil, errors.New("test error")
}
func testRawServer(w http.ResponseWriter, r *http.Request) {
switch {
// causes EOF error on ioutil.ReadAll
case strings.HasSuffix(r.URL.Path, "/testReadError"):
// tells the body is 1 byte length but actually it's 0
w.Header().Set("Content-Length", "1")
// returns unknown telegram error
case strings.HasSuffix(r.URL.Path, "/testUnknownError"):
data, _ := json.Marshal(struct {
Ok bool `json:"ok"`
Code int `json:"error_code"`
Description string `json:"description"`
}{
Ok: false,
Code: 400,
Description: "unknown error",
})
w.WriteHeader(400)
w.Write(data)
}
}
func TestRaw(t *testing.T) {
if token == "" {
t.Skip("TELEBOT_SECRET is required")
}
b, err := newTestBot()
if err != nil {
t.Fatal(err)
}
_, err = b.Raw("BAD METHOD", nil)
assert.EqualError(t, err, ErrNotFound.Error())
_, err = b.Raw("", &testPayload{})
assert.Error(t, err)
srv := httptest.NewServer(http.HandlerFunc(testRawServer))
defer srv.Close()
b.URL = srv.URL
b.client = srv.Client()
_, err = b.Raw("testReadError", nil)
assert.EqualError(t, err, "telebot: "+io.ErrUnexpectedEOF.Error())
_, err = b.Raw("testUnknownError", nil)
assert.EqualError(t, err, "telegram: unknown error (400)")
}
func TestExtractOk(t *testing.T) {
data := []byte(`{"ok": true, "result": {}}`)
require.NoError(t, extractOk(data))
data = []byte(`{
"ok": false,
"error_code": 400,
"description": "Bad Request: reply message not found"
}`)
assert.EqualError(t, extractOk(data), ErrNotFoundToReply.Error())
data = []byte(`{
"ok": false,
"error_code": 429,
"description": "Too Many Requests: retry after 8",
"parameters": {"retry_after": 8}
}`)
assert.Equal(t, FloodError{
err: NewError(429, "Too Many Requests: retry after 8"),
RetryAfter: 8,
}, extractOk(data))
data = []byte(`{
"ok": false,
"error_code": 400,
"description": "Bad Request: group chat was upgraded to a supergroup chat",
"parameters": {"migrate_to_chat_id": -100123456789}
}`)
assert.Equal(t, GroupError{
err: ErrGroupMigrated,
MigratedTo: -100123456789,
}, extractOk(data))
}
func TestExtractMessage(t *testing.T) {
data := []byte(`{"ok":true,"result":true}`)
_, err := extractMessage(data)
assert.Equal(t, ErrTrueResult, err)
data = []byte(`{"ok":true,"result":{"foo":"bar"}}`)
_, err = extractMessage(data)
require.NoError(t, err)
}

1163
bot.go Normal file

File diff suppressed because it is too large Load Diff

649
bot_test.go Normal file
View File

@ -0,0 +1,649 @@
package telebot
import (
"errors"
"io"
"io/ioutil"
"net/http"
"os"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
// required to test send and edit methods
token = os.Getenv("TELEBOT_SECRET")
chatID, _ = strconv.ParseInt(os.Getenv("CHAT_ID"), 10, 64)
userID, _ = strconv.ParseInt(os.Getenv("USER_ID"), 10, 64)
b, _ = newTestBot() // cached bot instance to avoid getMe method flooding
to = &Chat{ID: chatID} // to chat recipient for send and edit methods
user = &User{ID: userID} // to user recipient for some special cases
)
func defaultSettings() Settings {
return Settings{Token: token}
}
func newTestBot() (*Bot, error) {
return NewBot(defaultSettings())
}
func TestNewBot(t *testing.T) {
var pref Settings
_, err := NewBot(pref)
assert.Error(t, err)
pref.Token = "BAD TOKEN"
_, err = NewBot(pref)
assert.Error(t, err)
pref.URL = "BAD URL"
_, err = NewBot(pref)
assert.Error(t, err)
b, err := NewBot(Settings{Offline: true})
if err != nil {
t.Fatal(err)
}
assert.NotNil(t, b.Me)
assert.Equal(t, DefaultApiURL, b.URL)
assert.Equal(t, 100, cap(b.Updates))
assert.NotZero(t, b.client.Timeout)
pref = defaultSettings()
client := &http.Client{Timeout: time.Minute}
pref.URL = "http://api.telegram.org" // not https
pref.Client = client
pref.Poller = &LongPoller{Timeout: time.Second}
pref.Updates = 50
pref.ParseMode = ModeHTML
pref.Offline = true
b, err = NewBot(pref)
require.NoError(t, err)
assert.Equal(t, client, b.client)
assert.Equal(t, pref.URL, b.URL)
assert.Equal(t, pref.Poller, b.Poller)
assert.Equal(t, 50, cap(b.Updates))
assert.Equal(t, ModeHTML, b.parseMode)
}
func TestBotHandle(t *testing.T) {
if b == nil {
t.Skip("Cached bot instance is bad (probably wrong or empty TELEBOT_SECRET)")
}
b.Handle("/start", func(c Context) error { return nil })
assert.Contains(t, b.handlers, "/start")
reply := ReplyButton{Text: "reply"}
b.Handle(&reply, func(c Context) error { return nil })
inline := InlineButton{Unique: "inline"}
b.Handle(&inline, func(c Context) error { return nil })
btnReply := (&ReplyMarkup{}).Text("btnReply")
b.Handle(&btnReply, func(c Context) error { return nil })
btnInline := (&ReplyMarkup{}).Data("", "btnInline")
b.Handle(&btnInline, func(c Context) error { return nil })
assert.Contains(t, b.handlers, btnReply.CallbackUnique())
assert.Contains(t, b.handlers, btnInline.CallbackUnique())
assert.Contains(t, b.handlers, reply.CallbackUnique())
assert.Contains(t, b.handlers, inline.CallbackUnique())
}
func TestBotStart(t *testing.T) {
if token == "" {
t.Skip("TELEBOT_SECRET is required")
}
pref := defaultSettings()
pref.Poller = &LongPoller{}
b, err := NewBot(pref)
if err != nil {
t.Fatal(err)
}
// remove webhook to be sure that bot can poll
require.NoError(t, b.RemoveWebhook())
go b.Start()
b.Stop()
tp := newTestPoller()
go func() {
tp.updates <- Update{Message: &Message{Text: "/start"}}
}()
b, err = NewBot(pref)
require.NoError(t, err)
b.Poller = tp
var ok bool
b.Handle("/start", func(c Context) error {
assert.Equal(t, c.Text(), "/start")
tp.done <- struct{}{}
ok = true
return nil
})
go b.Start()
<-tp.done
b.Stop()
assert.True(t, ok)
}
func TestBotProcessUpdate(t *testing.T) {
b, err := NewBot(Settings{Synchronous: true, Offline: true})
if err != nil {
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
})
b.Handle("hello", func(c Context) error {
assert.Equal(t, "hello", c.Text())
return nil
})
b.Handle(OnText, func(c Context) error {
assert.Equal(t, "text", c.Text())
return nil
})
b.Handle(OnPinned, func(c Context) error {
assert.NotNil(t, c.Message())
return nil
})
b.Handle(OnPhoto, func(c Context) error {
assert.NotNil(t, c.Message().Photo)
return nil
})
b.Handle(OnVoice, func(c Context) error {
assert.NotNil(t, c.Message().Voice)
return nil
})
b.Handle(OnAudio, func(c Context) error {
assert.NotNil(t, c.Message().Audio)
return nil
})
b.Handle(OnAnimation, func(c Context) error {
assert.NotNil(t, c.Message().Animation)
return nil
})
b.Handle(OnDocument, func(c Context) error {
assert.NotNil(t, c.Message().Document)
return nil
})
b.Handle(OnSticker, func(c Context) error {
assert.NotNil(t, c.Message().Sticker)
return nil
})
b.Handle(OnVideo, func(c Context) error {
assert.NotNil(t, c.Message().Video)
return nil
})
b.Handle(OnVideoNote, func(c Context) error {
assert.NotNil(t, c.Message().VideoNote)
return nil
})
b.Handle(OnContact, func(c Context) error {
assert.NotNil(t, c.Message().Contact)
return nil
})
b.Handle(OnLocation, func(c Context) error {
assert.NotNil(t, c.Message().Location)
return nil
})
b.Handle(OnVenue, func(c Context) error {
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
})
b.Handle(OnPayment, func(c Context) error {
assert.NotNil(t, c.Message().Payment)
return nil
})
b.Handle(OnAddedToGroup, func(c Context) error {
assert.NotNil(t, c.Message().GroupCreated)
return nil
})
b.Handle(OnUserJoined, func(c Context) error {
assert.NotNil(t, c.Message().UserJoined)
return nil
})
b.Handle(OnUserLeft, func(c Context) error {
assert.NotNil(t, c.Message().UserLeft)
return nil
})
b.Handle(OnNewGroupTitle, func(c Context) error {
assert.Equal(t, "title", c.Message().NewGroupTitle)
return nil
})
b.Handle(OnNewGroupPhoto, func(c Context) error {
assert.NotNil(t, c.Message().NewGroupPhoto)
return nil
})
b.Handle(OnGroupPhotoDeleted, func(c Context) error {
assert.True(t, c.Message().GroupPhotoDeleted)
return nil
})
b.Handle(OnMigration, func(c Context) error {
from, to := c.Migration()
assert.Equal(t, int64(1), from)
assert.Equal(t, int64(2), to)
return nil
})
b.Handle(OnEdited, func(c Context) error {
assert.Equal(t, "edited", c.Message().Text)
return nil
})
b.Handle(OnChannelPost, func(c Context) error {
assert.Equal(t, "post", c.Message().Text)
return nil
})
b.Handle(OnEditedChannelPost, func(c Context) error {
assert.Equal(t, "edited post", c.Message().Text)
return nil
})
b.Handle(OnCallback, func(c Context) error {
if data := c.Callback().Data; data[0] != '\f' {
assert.Equal(t, "callback", data)
}
return nil
})
b.Handle("\funique", func(c Context) error {
assert.Equal(t, "callback", c.Callback().Data)
return nil
})
b.Handle(OnQuery, func(c Context) error {
assert.Equal(t, "query", c.Data())
return nil
})
b.Handle(OnInlineResult, func(c Context) error {
assert.Equal(t, "result", c.InlineResult().ResultID)
return nil
})
b.Handle(OnShipping, func(c Context) error {
assert.Equal(t, "shipping", c.ShippingQuery().ID)
return nil
})
b.Handle(OnCheckout, func(c Context) error {
assert.Equal(t, "checkout", c.PreCheckoutQuery().ID)
return nil
})
b.Handle(OnPoll, func(c Context) error {
assert.Equal(t, "poll", c.Poll().ID)
return nil
})
b.Handle(OnPollAnswer, func(c Context) error {
assert.Equal(t, "poll", c.PollAnswer().PollID)
return nil
})
b.Handle(OnWebApp, func(c Context) error {
assert.Equal(t, "webapp", c.Message().WebAppData.Data)
return nil
})
b.ProcessUpdate(Update{Message: &Message{Text: "/start"}})
b.ProcessUpdate(Update{Message: &Message{Text: "/start@other_bot"}})
b.ProcessUpdate(Update{Message: &Message{Text: "hello"}})
b.ProcessUpdate(Update{Message: &Message{Text: "text"}})
b.ProcessUpdate(Update{Message: &Message{PinnedMessage: &Message{}}})
b.ProcessUpdate(Update{Message: &Message{Photo: &Photo{}}})
b.ProcessUpdate(Update{Message: &Message{Voice: &Voice{}}})
b.ProcessUpdate(Update{Message: &Message{Audio: &Audio{}}})
b.ProcessUpdate(Update{Message: &Message{Animation: &Animation{}}})
b.ProcessUpdate(Update{Message: &Message{Document: &Document{}}})
b.ProcessUpdate(Update{Message: &Message{Sticker: &Sticker{}}})
b.ProcessUpdate(Update{Message: &Message{Video: &Video{}}})
b.ProcessUpdate(Update{Message: &Message{VideoNote: &VideoNote{}}})
b.ProcessUpdate(Update{Message: &Message{Contact: &Contact{}}})
b.ProcessUpdate(Update{Message: &Message{Location: &Location{}}})
b.ProcessUpdate(Update{Message: &Message{Venue: &Venue{}}})
b.ProcessUpdate(Update{Message: &Message{Invoice: &Invoice{}}})
b.ProcessUpdate(Update{Message: &Message{Payment: &Payment{}}})
b.ProcessUpdate(Update{Message: &Message{Dice: &Dice{}}})
b.ProcessUpdate(Update{Message: &Message{GroupCreated: true}})
b.ProcessUpdate(Update{Message: &Message{UserJoined: &User{ID: 1}}})
b.ProcessUpdate(Update{Message: &Message{UsersJoined: []User{{ID: 1}}}})
b.ProcessUpdate(Update{Message: &Message{UserLeft: &User{}}})
b.ProcessUpdate(Update{Message: &Message{NewGroupTitle: "title"}})
b.ProcessUpdate(Update{Message: &Message{NewGroupPhoto: &Photo{}}})
b.ProcessUpdate(Update{Message: &Message{GroupPhotoDeleted: true}})
b.ProcessUpdate(Update{Message: &Message{Chat: &Chat{ID: 1}, MigrateTo: 2}})
b.ProcessUpdate(Update{EditedMessage: &Message{Text: "edited"}})
b.ProcessUpdate(Update{ChannelPost: &Message{Text: "post"}})
b.ProcessUpdate(Update{ChannelPost: &Message{PinnedMessage: &Message{}}})
b.ProcessUpdate(Update{EditedChannelPost: &Message{Text: "edited post"}})
b.ProcessUpdate(Update{Callback: &Callback{MessageID: "inline", Data: "callback"}})
b.ProcessUpdate(Update{Callback: &Callback{Data: "callback"}})
b.ProcessUpdate(Update{Callback: &Callback{Data: "\funique|callback"}})
b.ProcessUpdate(Update{Query: &Query{Text: "query"}})
b.ProcessUpdate(Update{InlineResult: &InlineResult{ResultID: "result"}})
b.ProcessUpdate(Update{ShippingQuery: &ShippingQuery{ID: "shipping"}})
b.ProcessUpdate(Update{PreCheckoutQuery: &PreCheckoutQuery{ID: "checkout"}})
b.ProcessUpdate(Update{Poll: &Poll{ID: "poll"}})
b.ProcessUpdate(Update{PollAnswer: &PollAnswer{PollID: "poll"}})
b.ProcessUpdate(Update{Message: &Message{WebAppData: &WebAppData{Data: "webapp"}}})
}
func TestBotOnError(t *testing.T) {
b, err := NewBot(Settings{Synchronous: true, Offline: true})
if err != nil {
t.Fatal(err)
}
var ok bool
b.onError = func(err error, c Context) {
assert.Equal(t, b, c.(*nativeContext).b)
assert.NotNil(t, err)
ok = true
}
b.runHandler(func(c Context) error {
return errors.New("not nil")
}, &nativeContext{b: b})
assert.True(t, ok)
}
func TestBot(t *testing.T) {
if b == nil {
t.Skip("Cached bot instance is bad (probably wrong or empty TELEBOT_SECRET)")
}
if chatID == 0 {
t.Skip("CHAT_ID is required for Bot methods test")
}
_, err := b.Send(to, nil)
assert.Equal(t, ErrUnsupportedWhat, err)
_, err = b.Edit(&Message{Chat: &Chat{}}, nil)
assert.Equal(t, ErrUnsupportedWhat, err)
_, err = b.Send(nil, "")
assert.Equal(t, ErrBadRecipient, err)
_, err = b.Forward(nil, nil)
assert.Equal(t, ErrBadRecipient, err)
photo := &Photo{
File: FromURL("https://telegra.ph/file/65c5237b040ebf80ec278.jpg"),
Caption: t.Name(),
}
var msg *Message
t.Run("Send(what=Sendable)", func(t *testing.T) {
msg, err = b.Send(to, photo)
require.NoError(t, err)
assert.NotNil(t, msg.Photo)
assert.Equal(t, photo.Caption, msg.Caption)
})
t.Run("SendAlbum()", func(t *testing.T) {
_, err = b.SendAlbum(nil, nil)
assert.Equal(t, ErrBadRecipient, err)
_, err = b.SendAlbum(to, nil)
assert.Error(t, err)
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)
})
t.Run("EditCaption()+ParseMode", func(t *testing.T) {
b.parseMode = ModeHTML
edited, err := b.EditCaption(msg, "<b>new caption with html</b>")
require.NoError(t, err)
assert.Equal(t, "new caption with html", edited.Caption)
assert.Equal(t, EntityBold, edited.CaptionEntities[0].Type)
sleep()
edited, err = b.EditCaption(msg, "*new caption with markdown*", ModeMarkdown)
require.NoError(t, err)
assert.Equal(t, "new caption with markdown", edited.Caption)
assert.Equal(t, EntityBold, edited.CaptionEntities[0].Type)
sleep()
edited, err = b.EditCaption(msg, "_new caption with markdown \\(V2\\)_", ModeMarkdownV2)
require.NoError(t, err)
assert.Equal(t, "new caption with markdown (V2)", edited.Caption)
assert.Equal(t, EntityItalic, edited.CaptionEntities[0].Type)
b.parseMode = ModeDefault
})
t.Run("Edit(what=Media)", func(t *testing.T) {
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)
assert.Equal(t, t.Name(), msg.Text)
rpl, err := b.Reply(msg, t.Name())
require.NoError(t, err)
assert.Equal(t, rpl.Text, msg.Text)
assert.NotNil(t, rpl.ReplyTo)
assert.Equal(t, rpl.ReplyTo, msg)
assert.True(t, rpl.IsReply())
fwd, err := b.Forward(to, msg)
require.NoError(t, err)
assert.NotNil(t, msg, fwd)
assert.True(t, fwd.IsForwarded())
fwd.ID += 1 // nonexistent message
_, err = b.Forward(to, fwd)
assert.Equal(t, ErrNotFoundToForward, err)
})
t.Run("Edit(what=string)", func(t *testing.T) {
msg, err = b.Edit(msg, t.Name())
require.NoError(t, err)
assert.Equal(t, t.Name(), msg.Text)
_, err = b.Edit(msg, msg.Text)
assert.Error(t, err) // message is not modified
})
t.Run("Edit(what=ReplyMarkup)", func(t *testing.T) {
good := &ReplyMarkup{
InlineKeyboard: [][]InlineButton{
{{
Data: "btn",
Text: "Hi Telebot!",
}},
},
}
bad := &ReplyMarkup{
InlineKeyboard: [][]InlineButton{
{{
Data: strings.Repeat("*", 65),
Text: "Bad Button",
}},
},
}
edited, err := b.Edit(msg, good)
require.NoError(t, err)
assert.Equal(t, edited.ReplyMarkup.InlineKeyboard, good.InlineKeyboard)
edited, err = b.EditReplyMarkup(edited, nil)
require.NoError(t, err)
assert.Nil(t, edited.ReplyMarkup)
_, err = b.Edit(edited, bad)
assert.Equal(t, ErrBadButtonData, err)
})
t.Run("Edit(what=Location)", func(t *testing.T) {
loc := &Location{Lat: 42, Lng: 69, LivePeriod: 60}
edited, err := b.Send(to, loc)
require.NoError(t, err)
assert.NotNil(t, edited.Location)
loc = &Location{Lat: loc.Lng, Lng: loc.Lat}
edited, err = b.Edit(edited, *loc)
require.NoError(t, err)
assert.NotNil(t, edited.Location)
})
// Should be after the Edit tests.
t.Run("Delete()", func(t *testing.T) {
require.NoError(t, b.Delete(msg))
})
t.Run("Notify()", func(t *testing.T) {
assert.Equal(t, ErrBadRecipient, b.Notify(nil, Typing))
require.NoError(t, b.Notify(to, Typing))
})
t.Run("Answer()", func(t *testing.T) {
assert.Error(t, b.Answer(&Query{}, &QueryResponse{
Results: Results{&ArticleResult{}},
}))
})
t.Run("Respond()", func(t *testing.T) {
assert.Error(t, b.Respond(&Callback{}, &CallbackResponse{}))
})
t.Run("Payments", func(t *testing.T) {
assert.NotPanics(t, func() {
b.Accept(&PreCheckoutQuery{})
b.Accept(&PreCheckoutQuery{}, "error")
})
assert.NotPanics(t, func() {
b.Ship(&ShippingQuery{})
b.Ship(&ShippingQuery{}, "error")
b.Ship(&ShippingQuery{}, ShippingOption{}, ShippingOption{})
assert.Equal(t, ErrUnsupportedWhat, b.Ship(&ShippingQuery{}, 0))
})
})
t.Run("Commands", func(t *testing.T) {
var (
set1 = []Command{{
Text: "test1",
Description: "test command 1",
}}
set2 = []Command{{
Text: "test2",
Description: "test command 2",
}}
scope = CommandScope{
Type: CommandScopeChat,
ChatID: chatID,
}
)
err := b.SetCommands(set1)
require.NoError(t, err)
cmds, err := b.Commands()
require.NoError(t, err)
assert.Equal(t, set1, cmds)
err = b.SetCommands(set2, "en", scope)
require.NoError(t, err)
cmds, err = b.Commands()
require.NoError(t, err)
assert.Equal(t, set1, cmds)
cmds, err = b.Commands("en", scope)
require.NoError(t, err)
assert.Equal(t, set2, cmds)
require.NoError(t, b.DeleteCommands("en", scope))
require.NoError(t, b.DeleteCommands())
})
t.Run("InviteLink", func(t *testing.T) {
inviteLink, err := b.CreateInviteLink(&Chat{ID: chatID}, nil)
require.NoError(t, err)
assert.True(t, len(inviteLink.InviteLink) > 0)
sleep()
response, err := b.EditInviteLink(&Chat{ID: chatID}, &ChatInviteLink{InviteLink: inviteLink.InviteLink})
require.NoError(t, err)
assert.True(t, len(response.InviteLink) > 0)
sleep()
response, err = b.RevokeInviteLink(&Chat{ID: chatID}, inviteLink.InviteLink)
require.Nil(t, err)
assert.True(t, len(response.InviteLink) > 0)
})
}
func sleep() {
time.Sleep(time.Second)
}

89
callback.go Normal file
View File

@ -0,0 +1,89 @@
package telebot
// CallbackEndpoint is an interface any element capable
// of responding to a callback `\f<unique>`.
type CallbackEndpoint interface {
CallbackUnique() string
}
// Callback object represents a query from a callback button in an
// inline keyboard.
type Callback struct {
ID string `json:"id"`
// For message sent to channels, Sender may be empty
Sender *User `json:"from"`
// Message will be set if the button that originated the query
// was attached to a message sent by a bot.
Message *Message `json:"message"`
// MessageID will be set if the button was attached to a message
// sent via the bot in inline mode.
MessageID string `json:"inline_message_id"`
// Data associated with the callback button. Be aware that
// a bad client can send arbitrary data in this field.
Data string `json:"data"`
// Unique displays an unique of the button from which the
// callback was fired. Sets immediately before the handling,
// while the Data field stores only with payload.
Unique string `json:"-"`
}
// MessageSig satisfies Editable interface.
func (c *Callback) MessageSig() (string, int64) {
if c.IsInline() {
return c.MessageID, 0
}
return c.Message.MessageSig()
}
// IsInline says whether message is an inline message.
func (c *Callback) IsInline() bool {
return c.MessageID != ""
}
// CallbackResponse builds a response to a Callback query.
type CallbackResponse struct {
// The ID of the callback to which this is a response.
//
// Note: Telebot sets this field automatically!
CallbackID string `json:"callback_query_id"`
// Text of the notification. If not specified, nothing will be
// shown to the user.
Text string `json:"text,omitempty"`
// (Optional) If true, an alert will be shown by the client instead
// of a notification at the top of the chat screen. Defaults to false.
ShowAlert bool `json:"show_alert,omitempty"`
// (Optional) URL that will be opened by the user's client.
// If you have created a Game and accepted the conditions via
// @BotFather, specify the URL that opens your game.
//
// Note: this will only work if the query comes from a game
// callback button. Otherwise, you may use deep-linking:
// https://telegram.me/your_bot?start=XXXX
URL string `json:"url,omitempty"`
}
// CallbackUnique returns ReplyButton.Text.
func (t *ReplyButton) CallbackUnique() string {
return t.Text
}
// CallbackUnique returns InlineButton.Unique.
func (t *InlineButton) CallbackUnique() string {
return "\f" + t.Unique
}
// CallbackUnique implements CallbackEndpoint.
func (t *Btn) CallbackUnique() string {
if t.Unique != "" {
return "\f" + t.Unique
}
return t.Text
}

453
chat.go Normal file
View File

@ -0,0 +1,453 @@
package telebot
import (
"encoding/json"
"strconv"
"time"
)
// User object represents a Telegram user, bot.
type User struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
LanguageCode string `json:"language_code"`
IsBot bool `json:"is_bot"`
IsPremium bool `json:"is_premium"`
AddedToMenu bool `json:"added_to_attachment_menu"`
// Returns only in getMe
CanJoinGroups bool `json:"can_join_groups"`
CanReadMessages bool `json:"can_read_all_group_messages"`
SupportsInline bool `json:"supports_inline_queries"`
}
// Recipient returns user ID (see Recipient interface).
func (u *User) Recipient() string {
return strconv.FormatInt(u.ID, 10)
}
// Chat object represents a Telegram user, bot, group or a channel.
type Chat struct {
ID int64 `json:"id"`
// See ChatType and consts.
Type ChatType `json:"type"`
// Won't be there for ChatPrivate.
Title string `json:"title"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
// Returns only in getChat
Bio string `json:"bio,omitempty"`
Photo *ChatPhoto `json:"photo,omitempty"`
Description string `json:"description,omitempty"`
InviteLink string `json:"invite_link,omitempty"`
PinnedMessage *Message `json:"pinned_message,omitempty"`
Permissions *Rights `json:"permissions,omitempty"`
SlowMode int `json:"slow_mode_delay,omitempty"`
StickerSet string `json:"sticker_set_name,omitempty"`
CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"`
LinkedChatID int64 `json:"linked_chat_id,omitempty"`
ChatLocation *ChatLocation `json:"location,omitempty"`
Private bool `json:"has_private_forwards,omitempty"`
Protected bool `json:"has_protected_content,omitempty"`
NoVoiceAndVideo bool `json:"has_restricted_voice_and_video_messages"`
}
// Recipient returns chat ID (see Recipient interface).
func (c *Chat) Recipient() string {
return strconv.FormatInt(c.ID, 10)
}
// ChatType represents one of the possible chat types.
type ChatType string
const (
ChatPrivate ChatType = "private"
ChatGroup ChatType = "group"
ChatSuperGroup ChatType = "supergroup"
ChatChannel ChatType = "channel"
ChatChannelPrivate ChatType = "privatechannel"
)
// ChatLocation represents a location to which a chat is connected.
type ChatLocation struct {
Location Location `json:"location,omitempty"`
Address string `json:"address,omitempty"`
}
// ChatPhoto object represents a chat photo.
type ChatPhoto struct {
// File identifiers of small (160x160) chat photo
SmallFileID string `json:"small_file_id"`
SmallUniqueID string `json:"small_file_unique_id"`
// File identifiers of big (640x640) chat photo
BigFileID string `json:"big_file_id"`
BigUniqueID string `json:"big_file_unique_id"`
}
// ChatMember object represents information about a single chat member.
type ChatMember struct {
Rights
User *User `json:"user"`
Role MemberStatus `json:"status"`
Title string `json:"custom_title"`
Anonymous bool `json:"is_anonymous"`
// Date when restrictions will be lifted for the user, unix time.
//
// If user is restricted for more than 366 days or less than
// 30 seconds from the current time, they are considered to be
// restricted forever.
//
// Use tele.Forever().
//
RestrictedUntil int64 `json:"until_date,omitempty"`
JoinToSend string `json:"join_to_send_messages"`
JoinByRequest string `json:"join_by_request"`
}
// MemberStatus is one's chat status.
type MemberStatus string
const (
Creator MemberStatus = "creator"
Administrator MemberStatus = "administrator"
Member MemberStatus = "member"
Restricted MemberStatus = "restricted"
Left MemberStatus = "left"
Kicked MemberStatus = "kicked"
)
// ChatMemberUpdate object represents changes in the status of a chat member.
type ChatMemberUpdate struct {
// Chat where the user belongs to.
Chat *Chat `json:"chat"`
// Sender which user the action was triggered.
Sender *User `json:"from"`
// Unixtime, use Date() to get time.Time.
Unixtime int64 `json:"date"`
// Previous information about the chat member.
OldChatMember *ChatMember `json:"old_chat_member"`
// New information about the chat member.
NewChatMember *ChatMember `json:"new_chat_member"`
// (Optional) InviteLink which was used by the user to
// join the chat; for joining by invite link events only.
InviteLink *ChatInviteLink `json:"invite_link"`
}
// Time returns the moment of the change in local time.
func (c *ChatMemberUpdate) Time() time.Time {
return time.Unix(c.Unixtime, 0)
}
// ChatID represents a chat or an user integer ID, which can be used
// as recipient in bot methods. It is very useful in cases where
// you have special group IDs, for example in your config, and don't
// want to wrap it into *tele.Chat every time you send messages.
//
// Example:
//
// group := tele.ChatID(-100756389456)
// b.Send(group, "Hello!")
//
// type Config struct {
// AdminGroup tele.ChatID `json:"admin_group"`
// }
// b.Send(conf.AdminGroup, "Hello!")
//
type ChatID int64
// Recipient returns chat ID (see Recipient interface).
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"`
}
// ChatInviteLink object represents an invite for a chat.
type ChatInviteLink struct {
// The invite link.
InviteLink string `json:"invite_link"`
// Invite link name.
Name string `json:"name"`
// The creator of the link.
Creator *User `json:"creator"`
// If the link is primary.
IsPrimary bool `json:"is_primary"`
// If the link is revoked.
IsRevoked bool `json:"is_revoked"`
// (Optional) Point in time when the link will expire,
// use ExpireDate() to get time.Time.
ExpireUnixtime int64 `json:"expire_date,omitempty"`
// (Optional) Maximum number of users that can be members of
// the chat simultaneously.
MemberLimit int `json:"member_limit,omitempty"`
// (Optional) True, if users joining the chat via the link need to
// be approved by chat administrators. If True, member_limit can't be specified.
JoinRequest bool `json:"creates_join_request"`
// (Optional) Number of pending join requests created using this link.
PendingCount int `json:"pending_join_request_count"`
}
// ExpireDate returns the moment of the link expiration in local time.
func (c *ChatInviteLink) ExpireDate() time.Time {
return time.Unix(c.ExpireUnixtime, 0)
}
// Time returns the moment of chat join request sending in local time.
func (r ChatJoinRequest) Time() time.Time {
return time.Unix(r.Unixtime, 0)
}
// InviteLink should be used to export chat's invite link.
func (b *Bot) InviteLink(chat *Chat) (string, error) {
params := map[string]string{
"chat_id": chat.Recipient(),
}
data, err := b.Raw("exportChatInviteLink", params)
if err != nil {
return "", err
}
var resp struct {
Result string
}
if err := json.Unmarshal(data, &resp); err != nil {
return "", wrapError(err)
}
return resp.Result, nil
}
// CreateInviteLink creates an additional invite link for a chat.
func (b *Bot) CreateInviteLink(chat Recipient, link *ChatInviteLink) (*ChatInviteLink, error) {
params := map[string]string{
"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
}
// ApproveJoinRequest approves a chat join request.
func (b *Bot) ApproveJoinRequest(chat Recipient, user *User) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"user_id": user.Recipient(),
}
data, err := b.Raw("approveChatJoinRequest", params)
if err != nil {
return err
}
return extractOk(data)
}
// DeclineJoinRequest declines a chat join request.
func (b *Bot) DeclineJoinRequest(chat Recipient, user *User) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"user_id": user.Recipient(),
}
data, err := b.Raw("declineChatJoinRequest", params)
if err != nil {
return err
}
return extractOk(data)
}
// SetGroupTitle should be used to update group title.
func (b *Bot) SetGroupTitle(chat *Chat, title string) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"title": title,
}
_, err := b.Raw("setChatTitle", params)
return err
}
// SetGroupDescription should be used to update group description.
func (b *Bot) SetGroupDescription(chat *Chat, description string) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"description": description,
}
_, err := b.Raw("setChatDescription", params)
return err
}
// SetGroupPhoto should be used to update group photo.
func (b *Bot) SetGroupPhoto(chat *Chat, p *Photo) error {
params := map[string]string{
"chat_id": chat.Recipient(),
}
_, err := b.sendFiles("setChatPhoto", map[string]File{"photo": p.File}, params)
return err
}
// SetGroupStickerSet should be used to update group's group sticker set.
func (b *Bot) SetGroupStickerSet(chat *Chat, setName string) error {
params := map[string]string{
"chat_id": chat.Recipient(),
"sticker_set_name": setName,
}
_, err := b.Raw("setChatStickerSet", params)
return err
}
// SetGroupPermissions sets default chat permissions for all members.
func (b *Bot) SetGroupPermissions(chat *Chat, perms Rights) error {
params := map[string]interface{}{
"chat_id": chat.Recipient(),
"permissions": perms,
}
_, err := b.Raw("setChatPermissions", params)
return err
}
// DeleteGroupPhoto should be used to just remove group photo.
func (b *Bot) DeleteGroupPhoto(chat *Chat) error {
params := map[string]string{
"chat_id": chat.Recipient(),
}
_, err := b.Raw("deleteChatPhoto", params)
return err
}
// DeleteGroupStickerSet should be used to just remove group sticker set.
func (b *Bot) DeleteGroupStickerSet(chat *Chat) error {
params := map[string]string{
"chat_id": chat.Recipient(),
}
_, err := b.Raw("deleteChatStickerSet", params)
return err
}

21
chat_test.go Normal file
View File

@ -0,0 +1,21 @@
package telebot
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestChat(t *testing.T) {
user := &User{ID: 1}
chat := &Chat{ID: 1}
chatID := ChatID(1)
assert.Implements(t, (*Recipient)(nil), user)
assert.Implements(t, (*Recipient)(nil), chat)
assert.Implements(t, (*Recipient)(nil), chatID)
assert.Equal(t, "1", user.Recipient())
assert.Equal(t, "1", chat.Recipient())
assert.Equal(t, "1", chatID.Recipient())
}

85
commands.go Normal file
View File

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

486
context.go Normal file
View File

@ -0,0 +1,486 @@
package telebot
import (
"errors"
"strings"
"sync"
"time"
)
// HandlerFunc represents a handler function, which is
// used to handle actual endpoints.
type HandlerFunc func(Context) error
// Context wraps an update and represents the context of current event.
type Context interface {
// Bot returns the bot instance.
Bot() *Bot
// Update returns the original update.
Update() Update
// Message returns stored message if such presented.
Message() *Message
// Callback returns stored callback if such presented.
Callback() *Callback
// Query returns stored query if such presented.
Query() *Query
// InlineResult returns stored inline result if such presented.
InlineResult() *InlineResult
// ShippingQuery returns stored shipping query if such presented.
ShippingQuery() *ShippingQuery
// PreCheckoutQuery returns stored pre checkout query if such presented.
PreCheckoutQuery() *PreCheckoutQuery
// Poll returns stored poll if such presented.
Poll() *Poll
// PollAnswer returns stored poll answer if such presented.
PollAnswer() *PollAnswer
// ChatMember returns chat member changes.
ChatMember() *ChatMemberUpdate
// ChatJoinRequest returns cha
ChatJoinRequest() *ChatJoinRequest
// Migration returns both migration from and to chat IDs.
Migration() (int64, int64)
// Sender returns the current recipient, depending on the context type.
// Returns nil if user is not presented.
Sender() *User
// Chat returns the current chat, depending on the context type.
// Returns nil if chat is not presented.
Chat() *Chat
// Recipient combines both Sender and Chat functions. If there is no user
// the chat will be returned. The native context cannot be without sender,
// but it is useful in the case when the context created intentionally
// by the NewContext constructor and have only Chat field inside.
Recipient() Recipient
// Text returns the message text, depending on the context type.
// In the case when no related data presented, returns an empty string.
Text() string
// Entities returns the message entities, whether it's media caption's or the text's.
// In the case when no entities presented, returns a nil.
Entities() Entities
// Data returns the current data, depending on the context type.
// If the context contains command, returns its arguments string.
// If the context contains payment, returns its payload.
// In the case when no related data presented, returns an empty string.
Data() string
// Args returns a raw slice of command or callback arguments as strings.
// The message arguments split by space, while the callback's ones by a "|" symbol.
Args() []string
// Send sends a message to the current recipient.
// See Send from bot.go.
Send(what interface{}, opts ...interface{}) error
// SendAlbum sends an album to the current recipient.
// See SendAlbum from bot.go.
SendAlbum(a Album, opts ...interface{}) error
// Reply replies to the current message.
// See Reply from bot.go.
Reply(what interface{}, opts ...interface{}) error
// Forward forwards the given message to the current recipient.
// See Forward from bot.go.
Forward(msg Editable, opts ...interface{}) error
// ForwardTo forwards the current message to the given recipient.
// See Forward from bot.go
ForwardTo(to Recipient, opts ...interface{}) error
// Edit edits the current message.
// See Edit from bot.go.
Edit(what interface{}, opts ...interface{}) error
// EditCaption edits the caption of the current message.
// See EditCaption from bot.go.
EditCaption(caption string, opts ...interface{}) error
// EditOrSend edits the current message if the update is callback,
// otherwise the content is sent to the chat as a separate message.
EditOrSend(what interface{}, opts ...interface{}) error
// EditOrReply edits the current message if the update is callback,
// otherwise the content is replied as a separate message.
EditOrReply(what interface{}, opts ...interface{}) error
// Delete removes the current message.
// 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
// Ship replies to the current shipping query.
// See Ship from bot.go.
Ship(what ...interface{}) error
// Accept finalizes the current deal.
// See Accept from bot.go.
Accept(errorMessage ...string) error
// Answer sends a response to the current inline query.
// See Answer from bot.go.
Answer(resp *QueryResponse) error
// Respond sends a response for the current callback query.
// See Respond from bot.go.
Respond(resp ...*CallbackResponse) error
// Get retrieves data from the context.
Get(key string) interface{}
// Set saves data in the context.
Set(key string, val 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
u Update
lock sync.RWMutex
store map[string]interface{}
}
func (c *nativeContext) Bot() *Bot {
return c.b
}
func (c *nativeContext) Update() Update {
return c.u
}
func (c *nativeContext) Message() *Message {
switch {
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.u.Callback
}
func (c *nativeContext) Query() *Query {
return c.u.Query
}
func (c *nativeContext) InlineResult() *InlineResult {
return c.u.InlineResult
}
func (c *nativeContext) ShippingQuery() *ShippingQuery {
return c.u.ShippingQuery
}
func (c *nativeContext) PreCheckoutQuery() *PreCheckoutQuery {
return c.u.PreCheckoutQuery
}
func (c *nativeContext) ChatMember() *ChatMemberUpdate {
switch {
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.u.Poll
}
func (c *nativeContext) PollAnswer() *PollAnswer {
return c.u.PollAnswer
}
func (c *nativeContext) Migration() (int64, int64) {
return c.u.Message.MigrateFrom, c.u.Message.MigrateTo
}
func (c *nativeContext) Sender() *User {
switch {
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
}
}
func (c *nativeContext) Chat() *Chat {
switch {
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
}
}
func (c *nativeContext) Recipient() Recipient {
chat := c.Chat()
if chat != nil {
return chat
}
return c.Sender()
}
func (c *nativeContext) Text() string {
m := c.Message()
if m == nil {
return ""
}
if m.Caption != "" {
return m.Caption
}
return m.Text
}
func (c *nativeContext) Entities() Entities {
m := c.Message()
if m == nil {
return nil
}
if len(m.CaptionEntities) > 0 {
return m.CaptionEntities
}
return m.Entities
}
func (c *nativeContext) Data() string {
switch {
case c.u.Message != nil:
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 ""
}
}
func (c *nativeContext) Args() []string {
switch {
case c.u.Message != nil:
payload := strings.Trim(c.u.Message.Payload, " ")
if payload != "" {
return strings.Split(payload, " ")
}
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
}
func (c *nativeContext) Send(what interface{}, opts ...interface{}) error {
_, err := c.b.Send(c.Recipient(), what, opts...)
return err
}
func (c *nativeContext) SendAlbum(a Album, opts ...interface{}) error {
_, err := c.b.SendAlbum(c.Recipient(), a, opts...)
return err
}
func (c *nativeContext) Reply(what interface{}, opts ...interface{}) error {
msg := c.Message()
if msg == nil {
return ErrBadContext
}
_, err := c.b.Reply(msg, what, opts...)
return err
}
func (c *nativeContext) Forward(msg Editable, opts ...interface{}) error {
_, err := c.b.Forward(c.Recipient(), msg, opts...)
return err
}
func (c *nativeContext) ForwardTo(to Recipient, opts ...interface{}) error {
msg := c.Message()
if msg == nil {
return ErrBadContext
}
_, err := c.b.Forward(to, msg, opts...)
return err
}
func (c *nativeContext) Edit(what interface{}, opts ...interface{}) error {
if c.u.InlineResult != nil {
_, err := c.b.Edit(c.u.InlineResult, what, opts...)
return err
}
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.u.InlineResult != nil {
_, err := c.b.EditCaption(c.u.InlineResult, caption, opts...)
return err
}
if c.u.Callback != nil {
_, err := c.b.EditCaption(c.u.Callback, caption, opts...)
return err
}
return ErrBadContext
}
func (c *nativeContext) EditOrSend(what interface{}, opts ...interface{}) error {
err := c.Edit(what, opts...)
if err == ErrBadContext {
return c.Send(what, opts...)
}
return err
}
func (c *nativeContext) EditOrReply(what interface{}, opts ...interface{}) error {
err := c.Edit(what, opts...)
if err == ErrBadContext {
return c.Reply(what, opts...)
}
return err
}
func (c *nativeContext) Delete() error {
msg := c.Message()
if msg == nil {
return ErrBadContext
}
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.u.ShippingQuery == nil {
return errors.New("telebot: context shipping query is nil")
}
return c.b.Ship(c.u.ShippingQuery, what...)
}
func (c *nativeContext) Accept(errorMessage ...string) error {
if c.u.PreCheckoutQuery == nil {
return errors.New("telebot: context pre checkout query is nil")
}
return c.b.Accept(c.u.PreCheckoutQuery, errorMessage...)
}
func (c *nativeContext) Respond(resp ...*CallbackResponse) error {
if c.u.Callback == nil {
return errors.New("telebot: context callback is nil")
}
return c.b.Respond(c.u.Callback, resp...)
}
func (c *nativeContext) Answer(resp *QueryResponse) error {
if c.u.Query == nil {
return errors.New("telebot: context inline query is nil")
}
return c.b.Answer(c.u.Query, resp)
}
func (c *nativeContext) Set(key string, value interface{}) {
c.lock.Lock()
defer c.lock.Unlock()
if c.store == nil {
c.store = make(map[string]interface{})
}
c.store[key] = value
}
func (c *nativeContext) Get(key string) interface{} {
c.lock.RLock()
defer c.lock.RUnlock()
return c.store[key]
}

18
context_test.go Normal file
View File

@ -0,0 +1,18 @@
package telebot
import (
"testing"
"github.com/stretchr/testify/assert"
)
var _ Context = (*nativeContext)(nil)
func TestContext(t *testing.T) {
t.Run("Get,Set", func(t *testing.T) {
var c Context
c = new(nativeContext)
c.Set("name", "Jon Snow")
assert.Equal(t, "Jon Snow", c.Get("name"))
})
}

30
editable.go Normal file
View File

@ -0,0 +1,30 @@
package telebot
// Editable is an interface for all objects that
// provide "message signature", a pair of 32-bit
// message ID and 64-bit chat ID, both required
// for edit operations.
//
// Use case: DB model struct for messages to-be
// edited with, say two columns: msg_id,chat_id
// could easily implement MessageSig() making
// instances of stored messages editable.
type Editable interface {
// MessageSig is a "message signature".
//
// For inline messages, return chatID = 0.
MessageSig() (messageID string, chatID int64)
}
// StoredMessage is an example struct suitable for being
// stored in the database as-is or being embedded into
// a larger struct, which is often the case (you might
// want to store some metadata alongside, or might not.)
type StoredMessage struct {
MessageID string `sql:"message_id" json:"message_id"`
ChatID int64 `sql:"chat_id" json:"chat_id"`
}
func (x StoredMessage) MessageSig() (string, int64) {
return x.MessageID, x.ChatID
}

242
errors.go Normal file
View File

@ -0,0 +1,242 @@
package telebot
import (
"fmt"
"strings"
)
type (
Error struct {
Code int
Description string
Message string
}
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 *Error) ʔ() string {
return err.Description
}
// Error implements error interface.
func (err *Error) Error() string {
msg := err.Message
if msg == "" {
split := strings.Split(err.Description, ": ")
if len(split) == 2 {
msg = split[1]
} else {
msg = err.Description
}
}
return fmt.Sprintf("telegram: %s (%d)", msg, err.Code)
}
// 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 NewError(code int, msgs ...string) *Error {
err := &Error{Code: code}
if len(msgs) >= 1 {
err.Description = msgs[0]
}
if len(msgs) >= 2 {
err.Message = msgs[1]
}
return err
}
// General errors
var (
ErrTooLarge = NewError(400, "Request Entity Too Large")
ErrUnauthorized = NewError(401, "Unauthorized")
ErrNotFound = NewError(404, "Not Found")
ErrInternal = NewError(500, "Internal Server Error")
)
// 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")
ErrForwardMessage = NewError(400, "Bad Request: administrators of the chat restricted message forwarding")
)
// 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")
)
// Err returns Error instance by given description.
func Err(s string) error {
switch s {
case ErrTooLarge.ʔ():
return ErrTooLarge
case ErrUnauthorized.ʔ():
return ErrUnauthorized
case ErrNotFound.ʔ():
return ErrNotFound
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 ErrFailedImageProcess.ʔ():
return ErrFailedImageProcess
case ErrGroupMigrated.ʔ():
return ErrGroupMigrated
case ErrMessageNotModified.ʔ():
return ErrMessageNotModified
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 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 ErrWrongFileIDCharacter.ʔ():
return ErrWrongFileIDCharacter
case ErrWrongFileIDLength.ʔ():
return ErrWrongFileIDLength
case ErrWrongFileIDPadding.ʔ():
return ErrWrongFileIDPadding
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
case ErrForwardMessage.ʔ():
return ErrForwardMessage
default:
return nil
}
}
// wrapError returns new wrapped telebot-related error.
func wrapError(err error) error {
return fmt.Errorf("telebot: %w", err)
}

87
file.go Normal file
View File

@ -0,0 +1,87 @@
package telebot
import (
"io"
"os"
)
// File object represents any sort of file.
type File struct {
FileID string `json:"file_id"`
UniqueID string `json:"file_unique_id"`
FileSize int64 `json:"file_size"`
// FilePath is used for files on Telegram server.
FilePath string `json:"file_path"`
// FileLocal is used for files on local file system.
FileLocal string `json:"file_local"`
// FileURL is used for file on the internet.
FileURL string `json:"file_url"`
// FileReader is used for file backed with io.Reader.
FileReader io.Reader `json:"-"`
fileName string
}
// FromDisk constructs a new local (on-disk) file object.
//
// Note, it returns File, not *File for a very good reason:
// in telebot, File is pretty much an embeddable struct,
// so upon uploading media you'll need to set embedded File
// with something. NewFile() returning File makes it a one-liner.
//
// photo := &tele.Photo{File: tele.FromDisk("chicken.jpg")}
//
func FromDisk(filename string) File {
return File{FileLocal: filename}
}
// FromURL constructs a new file on provided HTTP URL.
//
// Note, it returns File, not *File for a very good reason:
// in telebot, File is pretty much an embeddable struct,
// so upon uploading media you'll need to set embedded File
// with something. NewFile() returning File makes it a one-liner.
//
// photo := &tele.Photo{File: tele.FromURL("https://site.com/picture.jpg")}
//
func FromURL(url string) File {
return File{FileURL: url}
}
// FromReader constructs a new file from io.Reader.
//
// Note, it returns File, not *File for a very good reason:
// in telebot, File is pretty much an embeddable struct,
// so upon uploading media you'll need to set embedded File
// with something. NewFile() returning File makes it a one-liner.
//
// photo := &tele.Photo{File: tele.FromReader(bytes.NewReader(...))}
//
func FromReader(reader io.Reader) File {
return File{FileReader: reader}
}
func (f *File) stealRef(g *File) {
if g.OnDisk() {
f.FileLocal = g.FileLocal
}
if g.FileURL != "" {
f.FileURL = g.FileURL
}
}
// InCloud tells whether the file is present on Telegram servers.
func (f *File) InCloud() bool {
return f.FileID != ""
}
// OnDisk will return true if file is present on disk.
func (f *File) OnDisk() bool {
_, err := os.Stat(f.FileLocal)
return err == nil
}

24
file_test.go Normal file
View File

@ -0,0 +1,24 @@
package telebot
import (
"io"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFile(t *testing.T) {
f := FromDisk("telebot.go")
g := FromURL("http://")
assert.True(t, f.OnDisk())
assert.True(t, (&File{FileID: "1"}).InCloud())
assert.Equal(t, File{FileLocal: "telebot.go"}, f)
assert.Equal(t, File{FileURL: "http://"}, g)
assert.Equal(t, File{FileReader: io.Reader(nil)}, FromReader(io.Reader(nil)))
g.stealRef(&f)
f.stealRef(&g)
assert.Equal(t, g.FileLocal, f.FileLocal)
assert.Equal(t, f.FileURL, g.FileURL)
}

99
game.go Normal file
View File

@ -0,0 +1,99 @@
package telebot
import (
"encoding/json"
"strconv"
)
// Game object represents a game.
// Their short names acts as unique identifiers.
type Game struct {
Name string `json:"game_short_name"`
Title string `json:"title"`
Description string `json:"description"`
Photo *Photo `json:"photo"`
// (Optional)
Text string `json:"text"`
Entities []MessageEntity `json:"text_entities"`
Animation *Animation `json:"animation"`
}
// GameHighScore object represents one row
// of the high scores table for a game.
type GameHighScore struct {
User *User `json:"user"`
Position int `json:"position"`
Score int `json:"score"`
Force bool `json:"force"`
NoEdit bool `json:"disable_edit_message"`
}
// GameScores returns the score of the specified user
// and several of their neighbors in a game.
//
// This function will panic upon nil Editable.
//
// Currently, it returns scores for the target user,
// plus two of their closest neighbors on each side.
// Will also return the top three users
// if the user and his neighbors are not among them.
//
func (b *Bot) GameScores(user Recipient, msg Editable) ([]GameHighScore, error) {
msgID, chatID := msg.MessageSig()
params := map[string]string{
"user_id": user.Recipient(),
}
if chatID == 0 { // if inline message
params["inline_message_id"] = msgID
} else {
params["chat_id"] = strconv.FormatInt(chatID, 10)
params["message_id"] = msgID
}
data, err := b.Raw("getGameHighScores", params)
if err != nil {
return nil, err
}
var resp struct {
Result []GameHighScore
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, err
}
return resp.Result, nil
}
// SetGameScore sets the score of the specified user in a game.
//
// If the message was sent by the bot, returns the edited Message,
// otherwise returns nil and ErrTrueResult.
//
func (b *Bot) SetGameScore(user Recipient, msg Editable, score GameHighScore) (*Message, error) {
msgID, chatID := msg.MessageSig()
params := map[string]string{
"user_id": user.Recipient(),
"score": strconv.Itoa(score.Score),
"force": strconv.FormatBool(score.Force),
"disable_edit_message": strconv.FormatBool(score.NoEdit),
}
if chatID == 0 { // if inline message
params["inline_message_id"] = msgID
} else {
params["chat_id"] = strconv.FormatInt(chatID, 10)
params["message_id"] = msgID
}
data, err := b.Raw("setGameScore", params)
if err != nil {
return nil, err
}
return extractMessage(data)
}

34
go.mod Normal file
View File

@ -0,0 +1,34 @@
module git.belvedersky.ru/common/telebot
go 1.19
require (
github.com/goccy/go-yaml v1.9.5
github.com/spf13/viper v1.13.0
github.com/stretchr/testify v1.8.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

506
go.sum Normal file
View File

@ -0,0 +1,506 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.10.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/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
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=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
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.5 h1:Eh/+3uk9kLxG4koCX6lRMAPS1OaMSAi+FJcya0INdB0=
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
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/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
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.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU=
github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
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/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

139
inline.go Normal file
View File

@ -0,0 +1,139 @@
package telebot
import (
"encoding/json"
"fmt"
)
// Query is an incoming inline query. When the user sends
// an empty query, your bot could return some default or
// trending results.
type Query struct {
// Unique identifier for this query. 1-64 bytes.
ID string `json:"id"`
// Sender.
Sender *User `json:"from"`
// Sender location, only for bots that request user location.
Location *Location `json:"location"`
// Text of the query (up to 512 characters).
Text string `json:"query"`
// Offset of the results to be returned, can be controlled by the bot.
Offset string `json:"offset"`
// ChatType of the type of the chat, from which the inline query was sent.
ChatType string `json:"chat_type"`
}
// QueryResponse builds a response to an inline Query.
type QueryResponse struct {
// The ID of the query to which this is a response.
//
// Note: Telebot sets this field automatically!
QueryID string `json:"inline_query_id"`
// The results for the inline query.
Results Results `json:"results"`
// (Optional) The maximum amount of time in seconds that the result
// of the inline query may be cached on the server.
CacheTime int `json:"cache_time,omitempty"`
// (Optional) Pass True, if results may be cached on the server side
// only for the user that sent the query. By default, results may
// be returned to any user who sends the same query.
IsPersonal bool `json:"is_personal"`
// (Optional) Pass the offset that a client should send in the next
// query with the same text to receive more results. Pass an empty
// string if there are no more results or if you dont support
// pagination. Offset length cant exceed 64 bytes.
NextOffset string `json:"next_offset"`
// (Optional) If passed, clients will display a button with specified
// text that switches the user to a private chat with the bot and sends
// the bot a start message with the parameter switch_pm_parameter.
SwitchPMText string `json:"switch_pm_text,omitempty"`
// (Optional) Parameter for the start message sent to the bot when user
// presses the switch button.
SwitchPMParameter string `json:"switch_pm_parameter,omitempty"`
}
// InlineResult represents a result of an inline query that was chosen
// by the user and sent to their chat partner.
type InlineResult struct {
Sender *User `json:"from"`
Location *Location `json:"location,omitempty"`
ResultID string `json:"result_id"`
Query string `json:"query"`
MessageID string `json:"inline_message_id"` // inline messages only!
}
// MessageSig satisfies Editable interface.
func (ir *InlineResult) MessageSig() (string, int64) {
return ir.MessageID, 0
}
// Result represents one result of an inline query.
type Result interface {
ResultID() string
SetResultID(string)
SetParseMode(ParseMode)
SetContent(InputMessageContent)
SetReplyMarkup(*ReplyMarkup)
Process(*Bot)
}
// Results is a slice wrapper for convenient marshalling.
type Results []Result
// MarshalJSON makes sure IQRs have proper IDs and Type variables set.
func (results Results) MarshalJSON() ([]byte, error) {
for _, result := range results {
if result.ResultID() == "" {
result.SetResultID(fmt.Sprintf("%d", &result))
}
if err := inferIQR(result); err != nil {
return nil, err
}
}
return json.Marshal([]Result(results))
}
func inferIQR(result Result) error {
switch r := result.(type) {
case *ArticleResult:
r.Type = "article"
case *AudioResult:
r.Type = "audio"
case *ContactResult:
r.Type = "contact"
case *DocumentResult:
r.Type = "document"
case *GifResult:
r.Type = "gif"
case *LocationResult:
r.Type = "location"
case *Mpeg4GifResult:
r.Type = "mpeg4_gif"
case *PhotoResult:
r.Type = "photo"
case *VenueResult:
r.Type = "venue"
case *VideoResult:
r.Type = "video"
case *VoiceResult:
r.Type = "voice"
case *StickerResult:
r.Type = "sticker"
default:
return fmt.Errorf("telebot: result %v is not supported", result)
}
return nil
}

373
inline_types.go Normal file
View File

@ -0,0 +1,373 @@
package telebot
// ResultBase must be embedded into all IQRs.
type ResultBase struct {
// Unique identifier for this result, 1-64 Bytes.
// If left unspecified, a 64-bit FNV-1 hash will be calculated
ID string `json:"id"`
// Ignore. This field gets set automatically.
Type string `json:"type"`
// Optional. Send Markdown or HTML, if you want Telegram apps to show
// bold, italic, fixed-width text or inline URLs in the media caption.
ParseMode ParseMode `json:"parse_mode,omitempty"`
// Optional. Content of the message to be sent.
Content InputMessageContent `json:"input_message_content,omitempty"`
// Optional. Inline keyboard attached to the message.
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
}
// ResultID returns ResultBase.ID.
func (r *ResultBase) ResultID() string {
return r.ID
}
// SetResultID sets ResultBase.ID.
func (r *ResultBase) SetResultID(id string) {
r.ID = id
}
// SetParseMode sets ResultBase.ParseMode.
func (r *ResultBase) SetParseMode(mode ParseMode) {
r.ParseMode = mode
}
// SetContent sets ResultBase.Content.
func (r *ResultBase) SetContent(content InputMessageContent) {
r.Content = content
}
// SetReplyMarkup sets ResultBase.ReplyMarkup.
func (r *ResultBase) SetReplyMarkup(markup *ReplyMarkup) {
r.ReplyMarkup = markup
}
func (r *ResultBase) Process(b *Bot) {
if r.ParseMode == ModeDefault {
r.ParseMode = b.parseMode
}
if r.Content != nil {
c, ok := r.Content.(*InputTextMessageContent)
if ok && c.ParseMode == ModeDefault {
c.ParseMode = r.ParseMode
}
}
if r.ReplyMarkup != nil {
processButtons(r.ReplyMarkup.InlineKeyboard)
}
}
// ArticleResult represents a link to an article or web page.
type ArticleResult struct {
ResultBase
// Title of the result.
Title string `json:"title"`
// Message text. Shortcut (and mutually exclusive to) specifying
// InputMessageContent.
Text string `json:"message_text,omitempty"`
// Optional. URL of the result.
URL string `json:"url,omitempty"`
// Optional. Pass True, if you don't want the URL to be shown in the message.
HideURL bool `json:"hide_url,omitempty"`
// Optional. Short description of the result.
Description string `json:"description,omitempty"`
// Optional. URL of the thumbnail for the result.
ThumbURL string `json:"thumb_url,omitempty"`
// Optional. Width of the thumbnail for the result.
ThumbWidth int `json:"thumb_width,omitempty"`
// Optional. Height of the thumbnail for the result.
ThumbHeight int `json:"thumb_height,omitempty"`
}
// AudioResult represents a link to an mp3 audio file.
type AudioResult struct {
ResultBase
// Title.
Title string `json:"title"`
// A valid URL for the audio file.
URL string `json:"audio_url"`
// Optional. Performer.
Performer string `json:"performer,omitempty"`
// Optional. Audio duration in seconds.
Duration int `json:"audio_duration,omitempty"`
// Optional. Caption, 0-1024 characters.
Caption string `json:"caption,omitempty"`
// If Cache != "", it'll be used instead
Cache string `json:"audio_file_id,omitempty"`
}
// ContactResult represents a contact with a phone number.
type ContactResult struct {
ResultBase
// Contact's phone number.
PhoneNumber string `json:"phone_number"`
// Optional. Additional data about the contact in the form of a vCard, 0-2048 bytes.
VCard string `json:"vcard,omitempty"`
// Contact's first name.
FirstName string `json:"first_name"`
// Optional. Contact's last name.
LastName string `json:"last_name,omitempty"`
// Optional. URL of the thumbnail for the result.
ThumbURL string `json:"thumb_url,omitempty"`
// Optional. Width of the thumbnail for the result.
ThumbWidth int `json:"thumb_width,omitempty"`
// Optional. Height of the thumbnail for the result.
ThumbHeight int `json:"thumb_height,omitempty"`
}
// DocumentResult represents a link to a file.
type DocumentResult struct {
ResultBase
// Title for the result.
Title string `json:"title"`
// A valid URL for the file
URL string `json:"document_url"`
// Mime type of the content of the file, either “application/pdf” or
// “application/zip”.
MIME string `json:"mime_type"`
// Optional. Caption of the document to be sent, 0-200 characters.
Caption string `json:"caption,omitempty"`
// Optional. Short description of the result.
Description string `json:"description,omitempty"`
// Optional. URL of the thumbnail (jpeg only) for the file.
ThumbURL string `json:"thumb_url,omitempty"`
// Optional. Width of the thumbnail for the result.
ThumbWidth int `json:"thumb_width,omitempty"`
// Optional. Height of the thumbnail for the result.
ThumbHeight int `json:"thumb_height,omitempty"`
// If Cache != "", it'll be used instead
Cache string `json:"document_file_id,omitempty"`
}
// GifResult represents a link to an animated GIF file.
type GifResult struct {
ResultBase
// A valid URL for the GIF file. File size must not exceed 1MB.
URL string `json:"gif_url"`
// Optional. Width of the GIF.
Width int `json:"gif_width,omitempty"`
// Optional. Height of the GIF.
Height int `json:"gif_height,omitempty"`
// Optional. Duration of the GIF.
Duration int `json:"gif_duration,omitempty"`
// URL of the static thumbnail for the result (jpeg or gif).
ThumbURL string `json:"thumb_url"`
// Optional. MIME type of the thumbnail, must be one of
// “image/jpeg”, “image/gif”, or “video/mp4”.
ThumbMIME string `json:"thumb_mime_type,omitempty"`
// Optional. Title for the result.
Title string `json:"title,omitempty"`
// Optional. Caption of the GIF file to be sent, 0-200 characters.
Caption string `json:"caption,omitempty"`
// If Cache != "", it'll be used instead
Cache string `json:"gif_file_id,omitempty"`
}
// LocationResult represents a location on a map.
type LocationResult struct {
ResultBase
Location
// Location title.
Title string `json:"title"`
// Optional. Url of the thumbnail for the result.
ThumbURL string `json:"thumb_url,omitempty"`
}
// Mpeg4GifResult represents a link to a video animation
// (H.264/MPEG-4 AVC video without sound).
type Mpeg4GifResult struct {
ResultBase
// A valid URL for the MP4 file.
URL string `json:"mpeg4_url"`
// Optional. Video width.
Width int `json:"mpeg4_width,omitempty"`
// Optional. Video height.
Height int `json:"mpeg4_height,omitempty"`
// Optional. Video duration.
Duration int `json:"mpeg4_duration,omitempty"`
// URL of the static thumbnail (jpeg or gif) for the result.
ThumbURL string `json:"thumb_url,omitempty"`
// Optional. MIME type of the thumbnail, must be one of
// “image/jpeg”, “image/gif”, or “video/mp4”.
ThumbMIME string `json:"thumb_mime_type,omitempty"`
// Optional. Title for the result.
Title string `json:"title,omitempty"`
// Optional. Caption of the MPEG-4 file to be sent, 0-200 characters.
Caption string `json:"caption,omitempty"`
// If Cache != "", it'll be used instead
Cache string `json:"mpeg4_file_id,omitempty"`
}
// PhotoResult represents a link to a photo.
type PhotoResult struct {
ResultBase
// A valid URL of the photo. Photo must be in jpeg format.
// Photo size must not exceed 5MB.
URL string `json:"photo_url"`
// Optional. Width of the photo.
Width int `json:"photo_width,omitempty"`
// Optional. Height of the photo.
Height int `json:"photo_height,omitempty"`
// Optional. Title for the result.
Title string `json:"title,omitempty"`
// Optional. Short description of the result.
Description string `json:"description,omitempty"`
// Optional. Caption of the photo to be sent, 0-200 characters.
Caption string `json:"caption,omitempty"`
// URL of the thumbnail for the photo.
ThumbURL string `json:"thumb_url"`
// If Cache != "", it'll be used instead
Cache string `json:"photo_file_id,omitempty"`
}
// VenueResult represents a venue.
type VenueResult struct {
ResultBase
Location
// Title of the venue.
Title string `json:"title"`
// Address of the venue.
Address string `json:"address"`
// Optional. Foursquare identifier of the venue if known.
FoursquareID string `json:"foursquare_id,omitempty"`
// Optional. URL of the thumbnail for the result.
ThumbURL string `json:"thumb_url,omitempty"`
// Optional. Width of the thumbnail for the result.
ThumbWidth int `json:"thumb_width,omitempty"`
// Optional. Height of the thumbnail for the result.
ThumbHeight int `json:"thumb_height,omitempty"`
}
// VideoResult represents a link to a page containing an embedded
// video player or a video file.
type VideoResult struct {
ResultBase
// A valid URL for the embedded video player or video file.
URL string `json:"video_url"`
// Mime type of the content of video url, “text/html” or “video/mp4”.
MIME string `json:"mime_type"`
// URL of the thumbnail (jpeg only) for the video.
ThumbURL string `json:"thumb_url"`
// Title for the result.
Title string `json:"title"`
// Optional. Caption of the video to be sent, 0-200 characters.
Caption string `json:"caption,omitempty"`
// Optional. Video width.
Width int `json:"video_width,omitempty"`
// Optional. Video height.
Height int `json:"video_height,omitempty"`
// Optional. Video duration in seconds.
Duration int `json:"video_duration,omitempty"`
// Optional. Short description of the result.
Description string `json:"description,omitempty"`
// If Cache != "", it'll be used instead
Cache string `json:"video_file_id,omitempty"`
}
// VoiceResult represents a link to a voice recording in an .ogg
// container encoded with OPUS.
type VoiceResult struct {
ResultBase
// A valid URL for the voice recording.
URL string `json:"voice_url"`
// Recording title.
Title string `json:"title"`
// Optional. Recording duration in seconds.
Duration int `json:"voice_duration"`
// Optional. Caption, 0-1024 characters.
Caption string `json:"caption,omitempty"`
// If Cache != "", it'll be used instead
Cache string `json:"voice_file_id,omitempty"`
}
// StickerResult represents an inline cached sticker response.
type StickerResult struct {
ResultBase
// If Cache != "", it'll be used instead
Cache string `json:"sticker_file_id,omitempty"`
}

73
input_types.go Normal file
View File

@ -0,0 +1,73 @@
package telebot
// InputMessageContent objects represent the content of a message to be sent
// as a result of an inline query.
type InputMessageContent interface {
IsInputMessageContent() bool
}
// InputTextMessageContent represents the content of a text message to be
// sent as the result of an inline query.
type InputTextMessageContent struct {
// Text of the message to be sent, 1-4096 characters.
Text string `json:"message_text"`
// Optional. Send Markdown or HTML, if you want Telegram apps to show
// bold, italic, fixed-width text or inline URLs in your bot's message.
ParseMode string `json:"parse_mode,omitempty"`
// Optional. Disables link previews for links in the sent message.
DisablePreview bool `json:"disable_web_page_preview"`
}
func (input *InputTextMessageContent) IsInputMessageContent() bool {
return true
}
// InputLocationMessageContent represents the content of a location message
// to be sent as the result of an inline query.
type InputLocationMessageContent struct {
Lat float32 `json:"latitude"`
Lng float32 `json:"longitude"`
}
func (input *InputLocationMessageContent) IsInputMessageContent() bool {
return true
}
// InputVenueMessageContent represents the content of a venue message to
// be sent as the result of an inline query.
type InputVenueMessageContent struct {
Lat float32 `json:"latitude"`
Lng float32 `json:"longitude"`
// Name of the venue.
Title string `json:"title"`
// Address of the venue.
Address string `json:"address"`
// Optional. Foursquare identifier of the venue, if known.
FoursquareID string `json:"foursquare_id,omitempty"`
}
func (input *InputVenueMessageContent) IsInputMessageContent() bool {
return true
}
// InputContactMessageContent represents the content of a contact
// message to be sent as the result of an inline query.
type InputContactMessageContent struct {
// Contact's phone number.
PhoneNumber string `json:"phone_number"`
// Contact's first name.
FirstName string `json:"first_name"`
// Optional. Contact's last name.
LastName string `json:"last_name,omitempty"`
}
func (input *InputContactMessageContent) IsInputMessageContent() bool {
return true
}

122
layout/config.go Normal file
View File

@ -0,0 +1,122 @@
package layout
import (
"strconv"
"time"
tele "git.belvedersky.ru/common/telebot"
"github.com/spf13/viper"
)
// Config represents typed map interface related to the "config" section in layout.
type Config struct {
v *viper.Viper
}
// Unmarshal parses the whole config into the out value. It's useful when you want to
// describe and to pre-define the fields in your custom configuration struct.
func (c *Config) Unmarshal(v interface{}) error {
return c.v.Unmarshal(v)
}
// UnmarshalKey parses the specific key in the config into the out value.
func (c *Config) UnmarshalKey(k string, v interface{}) error {
return c.v.UnmarshalKey(k, v)
}
// Get returns a child map field wrapped into Config.
// If the field isn't a map, returns nil.
func (c *Config) Get(k string) *Config {
v := c.v.Sub(k)
if v == nil {
return nil
}
return &Config{v: v}
}
// Slice returns a child slice of objects wrapped into Config.
// If the field isn't a slice, returns nil.
func (c *Config) Slice(k string) (slice []*Config) {
a, ok := c.v.Get(k).([]interface{})
if !ok {
return nil
}
for i := range a {
m, ok := a[i].(map[string]interface{})
if !ok {
return nil
}
v := viper.New()
v.MergeConfigMap(m)
slice = append(slice, &Config{v: v})
}
return
}
// String returns a field casted to the string.
func (c *Config) String(k string) string {
return c.v.GetString(k)
}
// Int returns a field casted to the int.
func (c *Config) Int(k string) int {
return c.v.GetInt(k)
}
// Int64 returns a field casted to the int64.
func (c *Config) Int64(k string) int64 {
return c.v.GetInt64(k)
}
// Float returns a field casted to the float64.
func (c *Config) Float(k string) float64 {
return c.v.GetFloat64(k)
}
// Bool returns a field casted to the bool.
func (c *Config) Bool(k string) bool {
return c.v.GetBool(k)
}
// Duration returns a field casted to the time.Duration.
// Accepts number-represented duration or a string in 0nsuµmh format.
func (c *Config) Duration(k string) time.Duration {
return c.v.GetDuration(k)
}
// ChatID returns a field casted to the ChatID.
// The value must be an integer.
func (c *Config) ChatID(k string) tele.ChatID {
return tele.ChatID(c.Int64(k))
}
// Strings returns a field casted to the string slice.
func (c *Config) Strings(k string) []string {
return c.v.GetStringSlice(k)
}
// Ints returns a field casted to the int slice.
func (c *Config) Ints(k string) []int {
return c.v.GetIntSlice(k)
}
// Int64s returns a field casted to the int64 slice.
func (c *Config) Int64s(k string) (ints []int64) {
for _, s := range c.Strings(k) {
i, _ := strconv.ParseInt(s, 10, 64)
ints = append(ints, i)
}
return ints
}
// Floats returns a field casted to the float slice.
func (c *Config) Floats(k string) (floats []float64) {
for _, s := range c.Strings(k) {
i, _ := strconv.ParseFloat(s, 64)
floats = append(floats, i)
}
return floats
}

43
layout/default.go Normal file
View File

@ -0,0 +1,43 @@
package layout
import (
tele "git.belvedersky.ru/common/telebot"
)
// DefaultLayout is a simplified layout instance with pre-defined locale by default.
type DefaultLayout struct {
locale string
lt *Layout
Config
}
// Settings returns layout settings.
func (dlt *DefaultLayout) Settings() tele.Settings {
return dlt.lt.Settings()
}
// Text wraps localized layout function Text using your default locale.
func (dlt *DefaultLayout) Text(k string, args ...interface{}) string {
return dlt.lt.TextLocale(dlt.locale, k, args...)
}
// Callback returns a callback endpoint used to handle buttons.
func (dlt *DefaultLayout) Callback(k string) tele.CallbackEndpoint {
return dlt.lt.Callback(k)
}
// Button wraps localized layout function Button using your default locale.
func (dlt *DefaultLayout) Button(k string, args ...interface{}) *tele.Btn {
return dlt.lt.ButtonLocale(dlt.locale, k, args...)
}
// Markup wraps localized layout function Markup using your default locale.
func (dlt *DefaultLayout) Markup(k string, args ...interface{}) *tele.ReplyMarkup {
return dlt.lt.MarkupLocale(dlt.locale, k, args...)
}
// Result wraps localized layout function Result using your default locale.
func (dlt *DefaultLayout) Result(k string, args ...interface{}) tele.Result {
return dlt.lt.ResultLocale(dlt.locale, k, args...)
}

76
layout/example.yml Normal file
View File

@ -0,0 +1,76 @@
settings:
token_env: TOKEN
parse_mode: html
long_poller: {}
commands:
/start: Start the bot
/help: How to use the bot
config:
str: string
num: 123
strs:
- abc
- def
nums:
- 123
- 456
obj: &obj
dur: 10m
arr:
- <<: *obj
- <<: *obj
buttons:
# Shortened reply buttons
help: Help
settings: Settings
# Extended reply button
contact:
text: Send a contact
request_contact: true
# Inline button
stop:
unique: stop
text: Stop
data: '{{.}}'
# Callback data
pay:
unique: pay
text: Pay
data:
- '{{ .UserID }}'
- '{{ .Amount }}'
- '{{ .Currency }}'
web_app:
text: This is a web app
web_app:
url: https://google.com
markups:
reply_shortened:
- [ help ]
- [ settings ]
reply_extended:
keyboard:
- [ contact ]
one_time_keyboard: true
inline:
- [ stop ]
web_app:
- [ web_app ]
results:
article:
type: article
id: '{{ .ID }}'
title: '{{ .Title }}'
description: '{{ .Description }}'
thumb_url: '{{ .PreviewURL }}'
message_text: '{{ text `article_message` }}'

579
layout/layout.go Normal file
View File

@ -0,0 +1,579 @@
package layout
import (
"bytes"
"encoding/json"
"io/ioutil"
"log"
"strings"
"sync"
"text/template"
tele "git.belvedersky.ru/common/telebot"
"github.com/goccy/go-yaml"
)
type (
// Layout provides an interface to interact with the layout,
// parsed from the config file and locales.
Layout struct {
pref *tele.Settings
mu sync.RWMutex // protects ctxs
ctxs map[tele.Context]string
funcs template.FuncMap
commands map[string]string
buttons map[string]Button
markups map[string]Markup
results map[string]Result
locales map[string]*template.Template
Config
}
// Button is a shortcut for tele.Btn.
Button struct {
tele.Btn `yaml:",inline"`
Data interface{} `yaml:"data"`
IsReply bool `yaml:"reply"`
}
// Markup represents layout-specific markup to be parsed.
Markup struct {
inline *bool
keyboard *template.Template
ResizeKeyboard *bool `yaml:"resize_keyboard,omitempty"` // nil == true
ForceReply bool `yaml:"force_reply,omitempty"`
OneTimeKeyboard bool `yaml:"one_time_keyboard,omitempty"`
RemoveKeyboard bool `yaml:"remove_keyboard,omitempty"`
Selective bool `yaml:"selective,omitempty"`
}
// Result represents layout-specific result to be parsed.
Result struct {
result *template.Template
tele.ResultBase `yaml:",inline"`
Content ResultContent `yaml:"content"`
Markup string `yaml:"markup"`
}
// ResultBase represents layout-specific result's base to be parsed.
ResultBase struct {
tele.ResultBase `yaml:",inline"`
Content ResultContent `yaml:"content"`
}
// ResultContent represents any kind of InputMessageContent and implements it.
ResultContent map[string]interface{}
)
// New parses the given layout file.
func New(path string, funcs ...template.FuncMap) (*Layout, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
lt := Layout{
ctxs: make(map[tele.Context]string),
funcs: make(template.FuncMap),
}
for k, v := range builtinFuncs {
lt.funcs[k] = v
}
for i := range funcs {
for k, v := range funcs[i] {
lt.funcs[k] = v
}
}
return &lt, yaml.Unmarshal(data, &lt)
}
// NewDefault parses the given layout file without localization features.
// See Layout.Default for more details.
func NewDefault(path, locale string, funcs ...template.FuncMap) (*DefaultLayout, error) {
lt, err := New(path, funcs...)
if err != nil {
return nil, err
}
return lt.Default(locale), nil
}
var builtinFuncs = template.FuncMap{
// Built-in blank and helper functions.
"locale": func() string { return "" },
"config": func(string) string { return "" },
"text": func(string, ...interface{}) string { return "" },
}
// Settings returns built telebot Settings required for bot initializing.
//
// settings:
// url: (custom url if needed)
// token: (not recommended)
// updates: (chan capacity)
// locales_dir: (optional)
// token_env: (token env var name, example: TOKEN)
// parse_mode: (default parse mode)
// long_poller: (long poller settings)
// webhook: (or webhook settings)
//
// Usage:
//
// lt, err := layout.New("bot.yml")
// b, err := tele.NewBot(lt.Settings())
// // That's all!
func (lt *Layout) Settings() tele.Settings {
if lt.pref == nil {
panic("telebot/layout: settings is empty")
}
return *lt.pref
}
// Default returns a simplified layout instance with the pre-defined locale.
// It's useful when you have no need for localization and don't want to pass
// context each time you use layout functions.
func (lt *Layout) Default(locale string) *DefaultLayout {
return &DefaultLayout{
locale: locale,
lt: lt,
Config: lt.Config,
}
}
// Locales returns all presented locales.
func (lt *Layout) Locales() []string {
var keys []string
for k := range lt.locales {
keys = append(keys, k)
}
return keys
}
// Locale returns the context locale.
func (lt *Layout) Locale(c tele.Context) (string, bool) {
lt.mu.RLock()
defer lt.mu.RUnlock()
locale, ok := lt.ctxs[c]
return locale, ok
}
// SetLocale allows you to change a locale for the passed context.
func (lt *Layout) SetLocale(c tele.Context, locale string) {
lt.mu.Lock()
lt.ctxs[c] = locale
lt.mu.Unlock()
}
// Commands returns a list of telebot commands, which can be
// used in b.SetCommands later.
func (lt *Layout) Commands() (cmds []tele.Command) {
for k, v := range lt.commands {
cmds = append(cmds, tele.Command{
Text: strings.TrimLeft(k, "/"),
Description: v,
})
}
return
}
// CommandsLocale returns a list of telebot commands and localized description, which can be
// used in b.SetCommands later.
//
// Example of bot.yml:
//
// commands:
// /start: '{{ text "cmdStart" }}'
//
// en.yml:
//
// cmdStart: Start the bot
//
// ru.yml:
//
// cmdStart: Запуск бота
//
// Usage:
//
// b.SetCommands(lt.CommandsLocale("en"), "en")
// b.SetCommands(lt.CommandsLocale("ru"), "ru")
func (lt *Layout) CommandsLocale(locale string, args ...interface{}) (cmds []tele.Command) {
var arg interface{}
if len(args) > 0 {
arg = args[0]
}
for k, v := range lt.commands {
tmpl, err := lt.template(template.New(k).Funcs(lt.funcs), locale).Parse(v)
if err != nil {
log.Println("telebot/layout:", err)
return nil
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, arg); err != nil {
log.Println("telebot/layout:", err)
return nil
}
cmds = append(cmds, tele.Command{
Text: strings.TrimLeft(k, "/"),
Description: buf.String(),
})
}
return
}
// Text returns a text, which locale is dependent on the context.
// The given optional argument will be passed to the template engine.
//
// Example of en.yml:
//
// start: Hi, {{.FirstName}}!
//
// Usage:
//
// func onStart(c tele.Context) error {
// return c.Send(lt.Text(c, "start", c.Sender()))
// }
func (lt *Layout) Text(c tele.Context, k string, args ...interface{}) string {
locale, ok := lt.Locale(c)
if !ok {
return ""
}
return lt.TextLocale(locale, k, args...)
}
// TextLocale returns a localized text processed with text/template engine.
// See Text for more details.
func (lt *Layout) TextLocale(locale, k string, args ...interface{}) string {
tmpl, ok := lt.locales[locale]
if !ok {
return ""
}
var arg interface{}
if len(args) > 0 {
arg = args[0]
}
var buf bytes.Buffer
if err := lt.template(tmpl, locale).ExecuteTemplate(&buf, k, arg); err != nil {
log.Println("telebot/layout:", err)
}
return buf.String()
}
// Callback returns a callback endpoint used to handle buttons.
//
// Example:
//
// // Handling settings button
// b.Handle(lt.Callback("settings"), onSettings)
func (lt *Layout) Callback(k string) tele.CallbackEndpoint {
btn, ok := lt.buttons[k]
if !ok {
return nil
}
return &btn
}
// Button returns a button, which locale is dependent on the context.
// The given optional argument will be passed to the template engine.
//
// buttons:
// item:
// unique: item
// callback_data: {{.ID}}
// text: Item #{{.Number}}
//
// Usage:
//
// btns := make([]tele.Btn, len(items))
// for i, item := range items {
// btns[i] = lt.Button(c, "item", struct {
// Number int
// Item Item
// }{
// Number: i,
// Item: item,
// })
// }
//
// m := b.NewMarkup()
// m.Inline(m.Row(btns...))
// // Your generated markup is ready.
func (lt *Layout) Button(c tele.Context, k string, args ...interface{}) *tele.Btn {
locale, ok := lt.Locale(c)
if !ok {
return nil
}
return lt.ButtonLocale(locale, k, args...)
}
// ButtonLocale returns a localized button processed with text/template engine.
// See Button for more details.
func (lt *Layout) ButtonLocale(locale, k string, args ...interface{}) *tele.Btn {
btn, ok := lt.buttons[k]
if !ok {
return nil
}
var arg interface{}
if len(args) > 0 {
arg = args[0]
}
data, err := yaml.Marshal(btn)
if err != nil {
log.Println("telebot/layout:", err)
return nil
}
tmpl, err := lt.template(template.New(k).Funcs(lt.funcs), locale).Parse(string(data))
if err != nil {
log.Println("telebot/layout:", err)
return nil
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, arg); err != nil {
log.Println("telebot/layout:", err)
return nil
}
if err := yaml.Unmarshal(buf.Bytes(), &btn); err != nil {
log.Println("telebot/layout:", err)
return nil
}
return &btn.Btn
}
// Markup returns a markup, which locale is dependent on the context.
// The given optional argument will be passed to the template engine.
//
// buttons:
// settings: 'Settings'
// markups:
// menu:
// - [settings]
//
// Usage:
//
// func onStart(c tele.Context) error {
// return c.Send(
// lt.Text(c, "start"),
// lt.Markup(c, "menu"),
// )
// }
func (lt *Layout) Markup(c tele.Context, k string, args ...interface{}) *tele.ReplyMarkup {
locale, ok := lt.Locale(c)
if !ok {
return nil
}
return lt.MarkupLocale(locale, k, args...)
}
// MarkupLocale returns a localized markup processed with text/template engine.
// See Markup for more details.
func (lt *Layout) MarkupLocale(locale, k string, args ...interface{}) *tele.ReplyMarkup {
markup, ok := lt.markups[k]
if !ok {
return nil
}
var arg interface{}
if len(args) > 0 {
arg = args[0]
}
var buf bytes.Buffer
if err := lt.template(markup.keyboard, locale).Execute(&buf, arg); err != nil {
log.Println("telebot/layout:", err)
}
r := &tele.ReplyMarkup{}
if *markup.inline {
if err := yaml.Unmarshal(buf.Bytes(), &r.InlineKeyboard); err != nil {
log.Println("telebot/layout:", err)
}
} else {
r.ResizeKeyboard = markup.ResizeKeyboard == nil || *markup.ResizeKeyboard
r.ForceReply = markup.ForceReply
r.OneTimeKeyboard = markup.OneTimeKeyboard
r.RemoveKeyboard = markup.RemoveKeyboard
r.Selective = markup.Selective
if err := yaml.Unmarshal(buf.Bytes(), &r.ReplyKeyboard); err != nil {
log.Println("telebot/layout:", err)
}
}
return r
}
// Result returns an inline result, which locale is dependent on the context.
// The given optional argument will be passed to the template engine.
//
// results:
// article:
// type: article
// id: '{{ .ID }}'
// title: '{{ .Title }}'
// description: '{{ .Description }}'
// message_text: '{{ .Content }}'
// thumb_url: '{{ .PreviewURL }}'
//
// Usage:
//
// func onQuery(c tele.Context) error {
// results := make(tele.Results, len(articles))
// for i, article := range articles {
// results[i] = lt.Result(c, "article", article)
// }
// return c.Answer(&tele.QueryResponse{
// Results: results,
// CacheTime: 100,
// })
// }
func (lt *Layout) Result(c tele.Context, k string, args ...interface{}) tele.Result {
locale, ok := lt.Locale(c)
if !ok {
return nil
}
return lt.ResultLocale(locale, k, args...)
}
// ResultLocale returns a localized result processed with text/template engine.
// See Result for more details.
func (lt *Layout) ResultLocale(locale, k string, args ...interface{}) tele.Result {
result, ok := lt.results[k]
if !ok {
return nil
}
var arg interface{}
if len(args) > 0 {
arg = args[0]
}
var buf bytes.Buffer
if err := lt.template(result.result, locale).Execute(&buf, arg); err != nil {
log.Println("telebot/layout:", err)
}
var (
data = buf.Bytes()
base Result
r tele.Result
)
if err := yaml.Unmarshal(data, &base); err != nil {
log.Println("telebot/layout:", err)
}
switch base.Type {
case "article":
r = &tele.ArticleResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "audio":
r = &tele.AudioResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "contact":
r = &tele.ContactResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "document":
r = &tele.DocumentResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "gif":
r = &tele.GifResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "location":
r = &tele.LocationResult{ResultBase: base.ResultBase}
if err := json.Unmarshal(data, &r); err != nil {
log.Println("telebot/layout:", err)
}
case "mpeg4_gif":
r = &tele.Mpeg4GifResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "photo":
r = &tele.PhotoResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "venue":
r = &tele.VenueResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "video":
r = &tele.VideoResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "voice":
r = &tele.VoiceResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "sticker":
r = &tele.StickerResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
default:
log.Println("telebot/layout: unsupported inline result type")
return nil
}
if base.Content != nil {
r.SetContent(base.Content)
}
if result.Markup != "" {
markup := lt.MarkupLocale(locale, result.Markup, args...)
if markup == nil {
log.Printf("telebot/layout: markup with name %s was not found\n", result.Markup)
} else {
r.SetReplyMarkup(markup)
}
}
return r
}
func (lt *Layout) template(tmpl *template.Template, locale string) *template.Template {
funcs := make(template.FuncMap)
// Redefining built-in blank functions
funcs["config"] = lt.String
funcs["text"] = func(k string, args ...interface{}) string { return lt.TextLocale(locale, k, args...) }
funcs["locale"] = func() string { return locale }
return tmpl.Funcs(funcs)
}
// IsInputMessageContent implements telebot.InputMessageContent.
func (ResultContent) IsInputMessageContent() bool {
return true
}

125
layout/layout_test.go Normal file
View File

@ -0,0 +1,125 @@
package layout
import (
"os"
"testing"
"time"
tele "git.belvedersky.ru/common/telebot"
"github.com/stretchr/testify/assert"
)
func TestLayout(t *testing.T) {
os.Setenv("TOKEN", "TEST")
lt, err := New("example.yml")
if err != nil {
t.Fatal(err)
}
pref := lt.Settings()
assert.Equal(t, "TEST", pref.Token)
assert.Equal(t, "html", pref.ParseMode)
assert.Equal(t, &tele.LongPoller{}, pref.Poller)
assert.ElementsMatch(t, []tele.Command{{
Text: "start",
Description: "Start the bot",
}, {
Text: "help",
Description: "How to use the bot",
}}, lt.Commands())
assert.Equal(t, "string", lt.String("str"))
assert.Equal(t, 123, lt.Int("num"))
assert.Equal(t, int64(123), lt.Int64("num"))
assert.Equal(t, float64(123), lt.Float("num"))
assert.Equal(t, tele.ChatID(123), lt.ChatID("num"))
assert.Equal(t, []string{"abc", "def"}, lt.Strings("strs"))
assert.Equal(t, []int{123, 456}, lt.Ints("nums"))
assert.Equal(t, []int64{123, 456}, lt.Int64s("nums"))
assert.Equal(t, []float64{123, 456}, lt.Floats("nums"))
obj := lt.Get("obj")
assert.NotNil(t, obj)
const dur = 10 * time.Minute
assert.Equal(t, dur, obj.Duration("dur"))
assert.True(t, lt.Duration("obj.dur") == obj.Duration("dur"))
arr := lt.Slice("arr")
assert.Len(t, arr, 2)
for _, v := range arr {
assert.Equal(t, dur, v.Duration("dur"))
}
assert.Equal(t, &tele.Btn{
Unique: "pay",
Text: "Pay",
Data: "1|100.00|USD",
}, lt.ButtonLocale("en", "pay", struct {
UserID int
Amount string
Currency string
}{
UserID: 1,
Amount: "100.00",
Currency: "USD",
}))
assert.Equal(t, &tele.ReplyMarkup{
ReplyKeyboard: [][]tele.ReplyButton{
{{Text: "Help"}},
{{Text: "Settings"}},
},
ResizeKeyboard: true,
}, lt.MarkupLocale("en", "reply_shortened"))
assert.Equal(t, &tele.ReplyMarkup{
ReplyKeyboard: [][]tele.ReplyButton{{{Text: "Send a contact", Contact: true}}},
ResizeKeyboard: true,
OneTimeKeyboard: true,
}, lt.MarkupLocale("en", "reply_extended"))
assert.Equal(t, &tele.ReplyMarkup{
InlineKeyboard: [][]tele.InlineButton{{
{
Unique: "stop",
Text: "Stop",
Data: "1",
},
}},
}, lt.MarkupLocale("en", "inline", 1))
assert.Equal(t, &tele.ReplyMarkup{
InlineKeyboard: [][]tele.InlineButton{{
{
Text: "This is a web app",
WebApp: &tele.WebApp{URL: "https://google.com"},
},
}},
}, lt.MarkupLocale("en", "web_app"))
assert.Equal(t, &tele.ArticleResult{
ResultBase: tele.ResultBase{
ID: "1853",
Type: "article",
},
Title: "Some title",
Description: "Some description",
ThumbURL: "https://preview.picture",
Text: "This is an article.",
}, lt.ResultLocale("en", "article", struct {
ID int
Title string
Description string
PreviewURL string
}{
ID: 1853,
Title: "Some title",
Description: "Some description",
PreviewURL: "https://preview.picture",
}))
}

1
layout/locales/en.yml Normal file
View File

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

50
layout/middleware.go Normal file
View File

@ -0,0 +1,50 @@
package layout
import (
tele "git.belvedersky.ru/common/telebot"
)
// LocaleFunc is the function used to fetch the locale of the recipient.
// Returned locale will be remembered and linked to the corresponding context.
type LocaleFunc func(tele.Recipient) string
// Middleware builds a telebot middleware to make localization work.
//
// Usage:
//
// b.Use(lt.Middleware("en", func(r tele.Recipient) string {
// loc, _ := db.UserLocale(r.Recipient())
// return loc
// }))
func (lt *Layout) Middleware(defaultLocale string, localeFunc ...LocaleFunc) tele.MiddlewareFunc {
var f LocaleFunc
if len(localeFunc) > 0 {
f = localeFunc[0]
}
return func(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
locale := defaultLocale
if f != nil {
if l := f(c.Sender()); l != "" {
locale = l
}
}
lt.SetLocale(c, locale)
defer func() {
lt.mu.Lock()
delete(lt.ctxs, c)
lt.mu.Unlock()
}()
return next(c)
}
}
}
// Middleware wraps ordinary layout middleware with your default locale.
func (dlt *DefaultLayout) Middleware() tele.MiddlewareFunc {
return dlt.lt.Middleware(dlt.locale)
}

268
layout/parser.go Normal file
View File

@ -0,0 +1,268 @@
package layout
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/template"
tele "git.belvedersky.ru/common/telebot"
"github.com/goccy/go-yaml"
"github.com/spf13/viper"
)
type Settings struct {
URL string
Token string
Updates int
LocalesDir string `yaml:"locales_dir"`
TokenEnv string `yaml:"token_env"`
ParseMode string `yaml:"parse_mode"`
Webhook *tele.Webhook `yaml:"webhook"`
LongPoller *tele.LongPoller `yaml:"long_poller"`
}
func (lt *Layout) UnmarshalYAML(data []byte) error {
var aux struct {
Settings *Settings
Config map[string]interface{}
Commands map[string]string
Buttons yaml.MapSlice
Markups yaml.MapSlice
Results yaml.MapSlice
Locales map[string]map[string]string
}
if err := yaml.Unmarshal(data, &aux); err != nil {
return err
}
v := viper.New()
if err := v.MergeConfigMap(aux.Config); err != nil {
return err
}
lt.Config = Config{v: v}
lt.commands = aux.Commands
if pref := aux.Settings; pref != nil {
lt.pref = &tele.Settings{
URL: pref.URL,
Token: pref.Token,
Updates: pref.Updates,
ParseMode: pref.ParseMode,
}
if pref.TokenEnv != "" {
lt.pref.Token = os.Getenv(pref.TokenEnv)
}
if pref.Webhook != nil {
lt.pref.Poller = pref.Webhook
} else if pref.LongPoller != nil {
lt.pref.Poller = pref.LongPoller
}
}
lt.buttons = make(map[string]Button, len(aux.Buttons))
for _, item := range aux.Buttons {
k, v := item.Key.(string), item.Value
// 1. Shortened reply button
if v, ok := v.(string); ok {
btn := tele.Btn{Text: v}
lt.buttons[k] = Button{Btn: btn}
continue
}
// 2. Extended reply or inline button
data, err := yaml.MarshalWithOptions(v, yaml.JSON())
if err != nil {
return err
}
var btn Button
if err := yaml.Unmarshal(data, &btn); err != nil {
return err
}
if !btn.IsReply && btn.Data != nil {
if a, ok := btn.Data.([]interface{}); ok {
s := make([]string, len(a))
for i, v := range a {
s[i] = fmt.Sprint(v)
}
btn.Btn.Data = strings.Join(s, "|")
} else if s, ok := btn.Data.(string); ok {
btn.Btn.Data = s
} else {
return fmt.Errorf("telebot/layout: invalid callback_data for %s button", k)
}
}
lt.buttons[k] = btn
}
lt.markups = make(map[string]Markup, len(aux.Markups))
for _, item := range aux.Markups {
k, v := item.Key.(string), item.Value
data, err := yaml.Marshal(v)
if err != nil {
return err
}
var shortenedMarkup [][]string
if yaml.Unmarshal(data, &shortenedMarkup) == nil {
// 1. Shortened reply or inline markup
kb := make([][]Button, len(shortenedMarkup))
for i, btns := range shortenedMarkup {
row := make([]Button, len(btns))
for j, btn := range btns {
b, ok := lt.buttons[btn]
if !ok {
return fmt.Errorf("telebot/layout: no %s button for %s markup", btn, k)
}
row[j] = b
}
kb[i] = row
}
data, err := yaml.Marshal(kb)
if err != nil {
return err
}
tmpl, err := template.New(k).Funcs(lt.funcs).Parse(string(data))
if err != nil {
return err
}
markup := Markup{keyboard: tmpl}
for _, row := range kb {
for _, btn := range row {
inline := btn.URL != "" ||
btn.Unique != "" ||
btn.InlineQuery != "" ||
btn.InlineQueryChat != "" ||
btn.Login != nil ||
btn.WebApp != nil
inline = !btn.IsReply && inline
if markup.inline == nil {
markup.inline = &inline
} else if *markup.inline != inline {
return fmt.Errorf("telebot/layout: mixed reply and inline buttons in %s markup", k)
}
}
}
lt.markups[k] = markup
} else {
// 2. Extended reply markup
var markup struct {
Markup `yaml:",inline"`
Keyboard [][]string `yaml:"keyboard"`
}
if err := yaml.Unmarshal(data, &markup); err != nil {
return err
}
kb := make([][]tele.ReplyButton, len(markup.Keyboard))
for i, btns := range markup.Keyboard {
row := make([]tele.ReplyButton, len(btns))
for j, btn := range btns {
row[j] = *lt.buttons[btn].Reply()
}
kb[i] = row
}
data, err := yaml.Marshal(kb)
if err != nil {
return err
}
tmpl, err := template.New(k).Funcs(lt.funcs).Parse(string(data))
if err != nil {
return err
}
markup.inline = new(bool)
markup.Markup.keyboard = tmpl
lt.markups[k] = markup.Markup
}
}
lt.results = make(map[string]Result, len(aux.Results))
for _, item := range aux.Results {
k, v := item.Key.(string), item.Value
data, err := yaml.Marshal(v)
if err != nil {
return err
}
tmpl, err := template.New(k).Funcs(lt.funcs).Parse(string(data))
if err != nil {
return err
}
var result Result
if err := yaml.Unmarshal(data, &result); err != nil {
return err
}
result.result = tmpl
lt.results[k] = result
}
if aux.Locales == nil {
if aux.Settings.LocalesDir == "" {
aux.Settings.LocalesDir = "locales"
}
return lt.parseLocales(aux.Settings.LocalesDir)
}
return nil
}
func (lt *Layout) parseLocales(dir string) error {
lt.locales = make(map[string]*template.Template)
return filepath.Walk(dir, func(path string, fi os.FileInfo, _ error) error {
if fi == nil || fi.IsDir() {
return nil
}
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
var texts map[string]string
if err := yaml.Unmarshal(data, &texts); err != nil {
return err
}
name := fi.Name()
name = strings.TrimSuffix(name, filepath.Ext(name))
tmpl := template.New(name).Funcs(lt.funcs)
for key, text := range texts {
_, err = tmpl.New(key).Parse(strings.Trim(text, "\r\n"))
if err != nil {
return err
}
}
lt.locales[name] = tmpl
return nil
})
}

322
markup.go Normal file
View File

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

65
markup_test.go Normal file
View File

@ -0,0 +1,65 @@
package telebot
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBtn(t *testing.T) {
r := &ReplyMarkup{}
assert.Equal(t, &ReplyButton{Text: "T"}, r.Text("T").Reply())
assert.Equal(t, &ReplyButton{Text: "T", Contact: true}, r.Contact("T").Reply())
assert.Equal(t, &ReplyButton{Text: "T", Location: true}, r.Location("T").Reply())
assert.Equal(t, &ReplyButton{Text: "T", Poll: PollAny}, r.Poll("T", PollAny).Reply())
assert.Nil(t, r.Data("T", "u").Reply())
assert.Equal(t, &InlineButton{Unique: "u", Text: "T"}, r.Data("T", "u").Inline())
assert.Equal(t, &InlineButton{Unique: "u", Text: "T", Data: "1|2"}, r.Data("T", "u", "1", "2").Inline())
assert.Equal(t, &InlineButton{Text: "T", URL: "url"}, r.URL("T", "url").Inline())
assert.Equal(t, &InlineButton{Text: "T", InlineQuery: "q"}, r.Query("T", "q").Inline())
assert.Equal(t, &InlineButton{Text: "T", InlineQueryChat: "q"}, r.QueryChat("T", "q").Inline())
assert.Equal(t, &InlineButton{Text: "T", Login: &Login{Text: "T"}}, r.Login("T", &Login{Text: "T"}).Inline())
assert.Equal(t, &InlineButton{Text: "T", WebApp: &WebApp{URL: "url"}}, r.WebApp("T", &WebApp{URL: "url"}).Inline())
}
func TestOptions(t *testing.T) {
r := &ReplyMarkup{}
r.Reply(
r.Row(r.Text("Menu")),
r.Row(r.Text("Settings")),
)
assert.Equal(t, [][]ReplyButton{
{{Text: "Menu"}},
{{Text: "Settings"}},
}, r.ReplyKeyboard)
i := &ReplyMarkup{}
i.Inline(i.Row(
i.Data("Previous", "prev"),
i.Data("Next", "next"),
))
assert.Equal(t, [][]InlineButton{{
{Unique: "prev", Text: "Previous"},
{Unique: "next", Text: "Next"},
}}, i.InlineKeyboard)
assert.Panics(t, func() {
r.Reply(r.Row(r.Data("T", "u")))
i.Inline(i.Row(i.Text("T")))
})
assert.Equal(t, r.copy(), r)
assert.Equal(t, i.copy(), i)
o := &SendOptions{ReplyMarkup: r}
assert.Equal(t, o.copy(), o)
data, err := PollQuiz.MarshalJSON()
require.NoError(t, err)
assert.Equal(t, []byte(`{"type":"quiz"}`), data)
}

355
media.go Normal file
View File

@ -0,0 +1,355 @@
package telebot
import (
"encoding/json"
)
// Media is a generic type for all kinds of media that includes File.
type Media interface {
// MediaType returns string-represented media type.
MediaType() string
// 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"`
DisableTypeDetection bool `json:"disable_content_type_detection,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"`
Caption string `json:"caption,omitempty"`
}
type photoSize struct {
File
Width int `json:"width"`
Height int `json:"height"`
Caption string `json:"caption,omitempty"`
}
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(data []byte) error {
var hq photoSize
if data[0] == '{' {
if err := json.Unmarshal(data, &hq); err != nil {
return err
}
} else {
var sizes []photoSize
if err := json.Unmarshal(data, &sizes); err != nil {
return err
}
hq = sizes[len(sizes)-1]
}
p.File = hq.File
p.Width = hq.Width
p.Height = hq.Height
return nil
}
// Audio object represents an audio file.
type Audio struct {
File
Duration int `json:"duration,omitempty"`
// (Optional)
Caption string `json:"caption,omitempty"`
Thumbnail *Photo `json:"thumb,omitempty"`
Title string `json:"title,omitempty"`
Performer string `json:"performer,omitempty"`
MIME string `json:"mime_type,omitempty"`
FileName string `json:"file_name,omitempty"`
}
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 {
File
// (Optional)
Thumbnail *Photo `json:"thumb,omitempty"`
Caption string `json:"caption,omitempty"`
MIME string `json:"mime_type"`
FileName string `json:"file_name,omitempty"`
DisableTypeDetection bool `json:"disable_content_type_detection,omitempty"`
}
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,
DisableTypeDetection: d.DisableTypeDetection,
}
}
// Video object represents a video file.
type Video struct {
File
Width int `json:"width"`
Height int `json:"height"`
Duration int `json:"duration,omitempty"`
// (Optional)
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"
}
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
Width int `json:"width"`
Height int `json:"height"`
Duration int `json:"duration,omitempty"`
// (Optional)
Caption string `json:"caption,omitempty"`
Thumbnail *Photo `json:"thumb,omitempty"`
MIME string `json:"mime_type,omitempty"`
FileName string `json:"file_name,omitempty"`
}
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
Duration int `json:"duration"`
// (Optional)
Caption string `json:"caption,omitempty"`
MIME string `json:"mime_type,omitempty"`
}
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)
Thumbnail *Photo `json:"thumb,omitempty"`
Length int `json:"length,omitempty"`
}
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"`
Video bool `json:"is_video"`
Thumbnail *Photo `json:"thumb"`
Emoji string `json:"emoji"`
SetName string `json:"set_name"`
MaskPosition *MaskPosition `json:"mask_position"`
PremiumAnimation *File `json:"premium_animation"`
}
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 int64 `json:"user_id,omitempty"`
}
// Location object represents geographic position.
type Location struct {
Lat float32 `json:"latitude"`
Lng float32 `json:"longitude"`
HorizontalAccuracy *float32 `json:"horizontal_accuracy,omitempty"`
Heading int `json:"heading,omitempty"`
AlertRadius int `json:"proximity_alert_radius,omitempty"`
// Period in seconds for which the location will be updated
// (see Live Locations, should be between 60 and 86400.)
LivePeriod int `json:"live_period,omitempty"`
}
// Venue object represents a venue location with name, address and
// optional foursquare ID.
type Venue struct {
Location Location `json:"location"`
Title string `json:"title"`
Address string `json:"address"`
// (Optional)
FoursquareID string `json:"foursquare_id,omitempty"`
FoursquareType string `json:"foursquare_type,omitempty"`
GooglePlaceID string `json:"google_place_id,omitempty"`
GooglePlaceType string `json:"google_place_type,omitempty"`
}
// Dice object represents a dice with a random value
// from 1 to 6 for currently supported base emoji.
type Dice struct {
Type DiceType `json:"emoji"`
Value int `json:"value"`
}
// DiceType defines dice types.
type DiceType string
var (
Cube = &Dice{Type: "🎲"}
Dart = &Dice{Type: "🎯"}
Ball = &Dice{Type: "🏀"}
Goal = &Dice{Type: "⚽"}
Slot = &Dice{Type: "🎰"}
Bowl = &Dice{Type: "🎳"}
)

428
message.go Normal file
View File

@ -0,0 +1,428 @@
package telebot
import (
"strconv"
"time"
"unicode/utf16"
)
// Message object represents a message.
type Message struct {
ID int `json:"message_id"`
ThreadId *int `json:"message_thread_id"`
// For message sent to channels, Sender will be nil
Sender *User `json:"from"`
// Unixtime, use Message.Time() to get time.Time
Unixtime int64 `json:"date"`
// Conversation the message belongs to.
Chat *Chat `json:"chat"`
// Sender of the message, sent on behalf of a chat.
SenderChat *Chat `json:"sender_chat"`
// For forwarded messages, sender of the original message.
OriginalSender *User `json:"forward_from"`
// For forwarded messages, chat of the original message when
// forwarded from a channel.
OriginalChat *Chat `json:"forward_from_chat"`
// For forwarded messages, identifier of the original message
// when forwarded from a channel.
OriginalMessageID int `json:"forward_from_message_id"`
// For forwarded messages, signature of the post author.
OriginalSignature string `json:"forward_signature"`
// For forwarded messages, sender's name from users who
// disallow adding a link to their account.
OriginalSenderName string `json:"forward_sender_name"`
// For forwarded messages, unixtime of the original message.
OriginalUnixtime int `json:"forward_date"`
// Message is a channel post that was automatically forwarded to the connected discussion group.
AutomaticForward bool `json:"is_automatic_forward"`
// For replies, ReplyTo represents the original message.
//
// Note that the Message object in this field will not
// contain further ReplyTo fields even if it
// itself is a reply.
ReplyTo *Message `json:"reply_to_message"`
// Shows through which bot the message was sent.
Via *User `json:"via_bot"`
// (Optional) Time of last edit in Unix.
LastEdit int64 `json:"edit_date"`
// (Optional) Message can't be forwarded.
Protected bool `json:"has_protected_content,omitempty"`
// AlbumID is the unique identifier of a media message group
// this message belongs to.
AlbumID string `json:"media_group_id"`
// Author signature (in channels).
Signature string `json:"author_signature"`
// For a text message, the actual UTF-8 text of the message.
Text string `json:"text"`
// For registered commands, will contain the string payload:
//
// Ex: `/command <payload>` or `/command@botname <payload>`
Payload string `json:"-"`
// For text messages, special entities like usernames, URLs, bot commands,
// etc. that appear in the text.
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 Entities `json:"caption_entities,omitempty"`
// For an audio recording, information about it.
Audio *Audio `json:"audio"`
// For a general file, information about it.
Document *Document `json:"document"`
// For a photo, all available sizes (thumbnails).
Photo *Photo `json:"photo"`
// For a sticker, information about it.
Sticker *Sticker `json:"sticker"`
// For a voice message, information about it.
Voice *Voice `json:"voice"`
// For a video note, information about it.
VideoNote *VideoNote `json:"video_note"`
// For a video, information about it.
Video *Video `json:"video"`
// For a animation, information about it.
Animation *Animation `json:"animation"`
// For a contact, contact information itself.
Contact *Contact `json:"contact"`
// For a location, its longitude and latitude.
Location *Location `json:"location"`
// For a venue, information about it.
Venue *Venue `json:"venue"`
// For a poll, information the native poll.
Poll *Poll `json:"poll"`
// For a game, information about it.
Game *Game `json:"game"`
// For a dice, information about it.
Dice *Dice `json:"dice"`
// For a service message, represents a user,
// that just got added to chat, this message came from.
//
// Sender leads to User, capable of invite.
//
// UserJoined might be the Bot itself.
UserJoined *User `json:"new_chat_member"`
// For a service message, represents a user,
// that just left chat, this message came from.
//
// If user was kicked, Sender leads to a User,
// capable of this kick.
//
// UserLeft might be the Bot itself.
UserLeft *User `json:"left_chat_member"`
// For a service message, represents a new title
// for chat this message came from.
//
// Sender would lead to a User, capable of change.
NewGroupTitle string `json:"new_chat_title"`
// For a service message, represents all available
// thumbnails of the new chat photo.
//
// Sender would lead to a User, capable of change.
NewGroupPhoto *Photo `json:"new_chat_photo"`
// For a service message, new members that were added to
// the group or supergroup and information about them
// (the bot itself may be one of these members).
UsersJoined []User `json:"new_chat_members"`
// For a service message, true if chat photo just
// got removed.
//
// Sender would lead to a User, capable of change.
GroupPhotoDeleted bool `json:"delete_chat_photo"`
// For a service message, true if group has been created.
//
// You would receive such a message if you are one of
// initial group chat members.
//
// Sender would lead to creator of the chat.
GroupCreated bool `json:"group_chat_created"`
// For a service message, true if supergroup has been created.
//
// You would receive such a message if you are one of
// initial group chat members.
//
// Sender would lead to creator of the chat.
SuperGroupCreated bool `json:"supergroup_chat_created"`
// For a service message, true if channel has been created.
//
// You would receive such a message if you are one of
// initial channel administrators.
//
// Sender would lead to creator of the chat.
ChannelCreated bool `json:"channel_chat_created"`
// For a service message, the destination (supergroup) you
// migrated to.
//
// You would receive such a message when your chat has migrated
// to a supergroup.
//
// Sender would lead to creator of the migration.
MigrateTo int64 `json:"migrate_to_chat_id"`
// For a service message, the Origin (normal group) you migrated
// from.
//
// You would receive such a message when your chat has migrated
// to a supergroup.
//
// Sender would lead to creator of the migration.
MigrateFrom int64 `json:"migrate_from_chat_id"`
// Specified message was pinned. Note that the Message object
// in this field will not contain further ReplyTo fields even
// if it is itself a reply.
PinnedMessage *Message `json:"pinned_message"`
// Message is an invoice for a payment.
Invoice *Invoice `json:"invoice"`
// Message is a service message about a successful payment.
Payment *Payment `json:"successful_payment"`
// The domain name of the website on which the user has logged in.
ConnectedWebsite string `json:"connected_website,omitempty"`
// For a service message, a video chat started in the chat.
VideoChatStarted *VideoChatStarted `json:"video_chat_started,omitempty"`
// For a service message, a video chat ended in the chat.
VideoChatEnded *VideoChatEnded `json:"video_chat_ended,omitempty"`
// For a service message, some users were invited in the video chat.
VideoChatParticipants *VideoChatParticipants `json:"video_chat_participants_invited,omitempty"`
// For a service message, a video chat schedule in the chat.
VideoChatScheduled *VideoChatScheduled `json:"video_chat_scheduled,omitempty"`
// For a data sent by a Web App.
WebAppData *WebAppData `json:"web_app_data,omitempty"`
// For a service message, represents the content of a service message,
// sent whenever a user in the chat triggers a proximity alert set by another user.
ProximityAlert *ProximityAlert `json:"proximity_alert_triggered,omitempty"`
// For a service message, represents about a change in auto-delete timer settings.
AutoDeleteTimer *AutoDeleteTimer `json:"message_auto_delete_timer_changed,omitempty"`
// Inline keyboard attached to the message.
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
}
// MessageEntity object represents "special" parts of text messages,
// including hashtags, usernames, URLs, etc.
type MessageEntity struct {
// Specifies entity type.
Type EntityType `json:"type"`
// Offset in UTF-16 code units to the start of the entity.
Offset int `json:"offset"`
// Length of the entity in UTF-16 code units.
Length int `json:"length"`
// (Optional) For EntityTextLink entity type only.
//
// URL will be opened after user taps on the text.
URL string `json:"url,omitempty"`
// (Optional) For EntityTMention entity type only.
User *User `json:"user,omitempty"`
// (Optional) For EntityCodeBlock entity type only.
Language string `json:"language,omitempty"`
// (Optional) For EntityCustomEmoji entity type only.
CustomEmoji string `json:"custom_emoji_id"`
}
// EntityType is a MessageEntity type.
type EntityType string
const (
EntityMention EntityType = "mention"
EntityTMention EntityType = "text_mention"
EntityHashtag EntityType = "hashtag"
EntityCashtag EntityType = "cashtag"
EntityCommand EntityType = "bot_command"
EntityURL EntityType = "url"
EntityEmail EntityType = "email"
EntityPhone EntityType = "phone_number"
EntityBold EntityType = "bold"
EntityItalic EntityType = "italic"
EntityUnderline EntityType = "underline"
EntityStrikethrough EntityType = "strikethrough"
EntityCode EntityType = "code"
EntityCodeBlock EntityType = "pre"
EntityTextLink EntityType = "text_link"
EntitySpoiler EntityType = "spoiler"
EntityCustomEmoji EntityType = "custom_emoji"
)
// Entities is used to set message's text entities as a send option.
type Entities []MessageEntity
// 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"`
}
// MessageSig satisfies Editable interface (see Editable.)
func (m *Message) MessageSig() (string, int64) {
return strconv.Itoa(m.ID), m.Chat.ID
}
// Time returns the moment of message creation in local time.
func (m *Message) Time() time.Time {
return time.Unix(m.Unixtime, 0)
}
// LastEdited returns time.Time of last edit.
func (m *Message) LastEdited() time.Time {
return time.Unix(m.LastEdit, 0)
}
// IsForwarded says whether message is forwarded copy of another
// message or not.
func (m *Message) IsForwarded() bool {
return m.OriginalSender != nil || m.OriginalChat != nil
}
// IsReply says whether message is a reply to another message.
func (m *Message) IsReply() bool {
return m.ReplyTo != nil
}
// Private returns true, if it's a personal message.
func (m *Message) Private() bool {
return m.Chat.Type == ChatPrivate
}
// FromGroup returns true, if message came from a group OR a supergroup.
func (m *Message) FromGroup() bool {
return m.Chat.Type == ChatGroup || m.Chat.Type == ChatSuperGroup
}
// FromChannel returns true, if message came from a channel.
func (m *Message) FromChannel() bool {
return m.Chat.Type == ChatChannel
}
// IsService returns true, if message is a service message,
// returns false otherwise.
//
// Service messages are automatically sent messages, which
// typically occur on some global action. For instance, when
// anyone leaves the chat or chat title changes.
func (m *Message) IsService() bool {
fact := false
fact = fact || m.UserJoined != nil
fact = fact || len(m.UsersJoined) > 0
fact = fact || m.UserLeft != nil
fact = fact || m.NewGroupTitle != ""
fact = fact || m.NewGroupPhoto != nil
fact = fact || m.GroupPhotoDeleted
fact = fact || m.GroupCreated || m.SuperGroupCreated
fact = fact || (m.MigrateTo != m.MigrateFrom)
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, sticker, 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.Sticker != nil:
return m.Sticker
case m.Document != nil:
return m.Document
case m.Video != nil:
return m.Video
case m.VideoNote != nil:
return m.VideoNote
default:
return nil
}
}

29
middleware.go Normal file
View File

@ -0,0 +1,29 @@
package telebot
// MiddlewareFunc represents a middleware processing function,
// which get called before the endpoint group or specific handler.
type MiddlewareFunc func(HandlerFunc) HandlerFunc
func applyMiddleware(h HandlerFunc, m ...MiddlewareFunc) HandlerFunc {
for i := len(m) - 1; i >= 0; i-- {
h = m[i](h)
}
return h
}
// Group is a separated group of handlers, united by the general middleware.
type Group struct {
b *Bot
middleware []MiddlewareFunc
}
// Use adds middleware to the chain.
func (g *Group) Use(middleware ...MiddlewareFunc) {
g.middleware = append(g.middleware, middleware...)
}
// Handle adds endpoint handler to the bot, combining group's middleware
// with the optional given middleware.
func (g *Group) Handle(endpoint interface{}, h HandlerFunc, m ...MiddlewareFunc) {
g.b.Handle(endpoint, h, append(g.middleware, m...)...)
}

27
middleware/logger.go Normal file
View File

@ -0,0 +1,27 @@
package middleware
import (
"encoding/json"
"log"
tele "git.belvedersky.ru/common/telebot"
)
// Logger returns a middleware that logs incoming updates.
// If no custom logger provided, log.Default() will be used.
func Logger(logger ...*log.Logger) tele.MiddlewareFunc {
var l *log.Logger
if len(logger) > 0 {
l = logger[0]
} else {
l = log.Default()
}
return func(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
data, _ := json.MarshalIndent(c.Update(), "", " ")
l.Println(string(data))
return next(c)
}
}
}

62
middleware/middleware.go Normal file
View File

@ -0,0 +1,62 @@
package middleware
import (
"errors"
tele "git.belvedersky.ru/common/telebot"
)
// AutoRespond returns a middleware that automatically responds
// to every callback.
func AutoRespond() tele.MiddlewareFunc {
return func(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
if c.Callback() != nil {
defer c.Respond()
}
return next(c)
}
}
}
// IgnoreVia returns a middleware that ignores all the
// "sent via" messages.
func IgnoreVia() tele.MiddlewareFunc {
return func(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
if msg := c.Message(); msg != nil && msg.Via != nil {
return nil
}
return next(c)
}
}
}
// Recover returns a middleware that recovers a panic happened in
// the handler.
func Recover(onError ...func(error)) tele.MiddlewareFunc {
return func(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
var f func(error)
if len(onError) > 0 {
f = onError[0]
} else {
f = func(err error) {
c.Bot().OnError(err, nil)
}
}
defer func() {
if r := recover(); r != nil {
if err, ok := r.(error); ok {
f(err)
} else if s, ok := r.(string); ok {
f(errors.New(s))
}
}
}()
return next(c)
}
}
}

View File

@ -0,0 +1,30 @@
package middleware
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
tele "git.belvedersky.ru/common/telebot"
)
var b, _ = tele.NewBot(tele.Settings{Offline: true})
func TestRecover(t *testing.T) {
onError := func(err error) {
require.Error(t, err, "recover test")
}
h := func(c tele.Context) error {
panic("recover test")
}
assert.Panics(t, func() {
h(nil)
})
assert.NotPanics(t, func() {
Recover(onError)(h)(nil)
})
}

65
middleware/restrict.go Normal file
View File

@ -0,0 +1,65 @@
package middleware
import tele "git.belvedersky.ru/common/telebot"
// RestrictConfig defines config for Restrict middleware.
type RestrictConfig struct {
// Chats is a list of chats that are going to be affected
// by either In or Out function.
Chats []int64
// In defines a function that will be called if the chat
// of an update will be found in the Chats list.
In tele.HandlerFunc
// Out defines a function that will be called if the chat
// of an update will NOT be found in the Chats list.
Out tele.HandlerFunc
}
// Restrict returns a middleware that handles a list of provided
// chats with the logic defined by In and Out functions.
// If the chat is found in the Chats field, In function will be called,
// otherwise Out function will be called.
func Restrict(v RestrictConfig) tele.MiddlewareFunc {
return func(next tele.HandlerFunc) tele.HandlerFunc {
if v.In == nil {
v.In = next
}
if v.Out == nil {
v.Out = next
}
return func(c tele.Context) error {
for _, chat := range v.Chats {
if chat == c.Sender().ID {
return v.In(c)
}
}
return v.Out(c)
}
}
}
// Blacklist returns a middleware that skips the update for users
// specified in the chats field.
func Blacklist(chats ...int64) tele.MiddlewareFunc {
return func(next tele.HandlerFunc) tele.HandlerFunc {
return Restrict(RestrictConfig{
Chats: chats,
Out: next,
In: func(c tele.Context) error { return nil },
})(next)
}
}
// Whitelist returns a middleware that skips the update for users
// NOT specified in the chats field.
func Whitelist(chats ...int64) tele.MiddlewareFunc {
return func(next tele.HandlerFunc) tele.HandlerFunc {
return Restrict(RestrictConfig{
Chats: chats,
In: next,
Out: func(c tele.Context) error { return nil },
})(next)
}
}

216
options.go Normal file
View File

@ -0,0 +1,216 @@
package telebot
import (
"encoding/json"
"strconv"
)
// Option is a shortcut flag type for certain message features
// (so-called options). It means that instead of passing
// fully-fledged SendOptions* to Send(), you can use these
// flags instead.
//
// Supported options are defined as iota-constants.
type Option int
const (
// NoPreview = SendOptions.DisableWebPagePreview
NoPreview Option = iota
// Silent = SendOptions.DisableNotification
Silent
// AllowWithoutReply = SendOptions.AllowWithoutReply
AllowWithoutReply
// Protected = SendOptions.Protected
Protected
// ForceReply = ReplyMarkup.ForceReply
ForceReply
// OneTimeKeyboard = ReplyMarkup.OneTimeKeyboard
OneTimeKeyboard
// RemoveKeyboard = ReplyMarkup.RemoveKeyboard
RemoveKeyboard
)
// Placeholder is used to set input field placeholder as a send option.
func Placeholder(text string) *SendOptions {
return &SendOptions{
ReplyMarkup: &ReplyMarkup{
ForceReply: true,
Placeholder: text,
},
}
}
// SendOptions has most complete control over in what way the message
// must be sent, providing an API-complete set of custom properties
// and options.
//
// Despite its power, SendOptions is rather inconvenient to use all
// the way through bot logic, so you might want to consider storing
// and re-using it somewhere or be using Option flags instead.
type SendOptions struct {
// If the message is a reply, original message.
ReplyTo *Message
MessageThreadId *int
// See ReplyMarkup struct definition.
ReplyMarkup *ReplyMarkup
// For text messages, disables previews for links in this message.
DisableWebPagePreview bool
// Sends the message silently. iOS users will not receive a notification, Android users will receive a notification with no sound.
DisableNotification bool
// 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
// AllowWithoutReply allows sending messages not a as reply if the replied-to message has already been deleted.
AllowWithoutReply bool
// Protected protects the contents of the sent message from forwarding and saving
Protected bool
}
func (og *SendOptions) copy() *SendOptions {
cp := *og
if cp.ReplyMarkup != nil {
cp.ReplyMarkup = cp.ReplyMarkup.copy()
}
return &cp
}
func extractOptions(how []interface{}) *SendOptions {
opts := &SendOptions{}
for _, prop := range how {
switch opt := prop.(type) {
case *SendOptions:
opts = opt.copy()
case *ReplyMarkup:
if opt != nil {
opts.ReplyMarkup = opt.copy()
}
case Option:
switch opt {
case NoPreview:
opts.DisableWebPagePreview = true
case Silent:
opts.DisableNotification = true
case AllowWithoutReply:
opts.AllowWithoutReply = true
case ForceReply:
if opts.ReplyMarkup == nil {
opts.ReplyMarkup = &ReplyMarkup{}
}
opts.ReplyMarkup.ForceReply = true
case OneTimeKeyboard:
if opts.ReplyMarkup == nil {
opts.ReplyMarkup = &ReplyMarkup{}
}
opts.ReplyMarkup.OneTimeKeyboard = true
case RemoveKeyboard:
if opts.ReplyMarkup == nil {
opts.ReplyMarkup = &ReplyMarkup{}
}
opts.ReplyMarkup.RemoveKeyboard = true
case Protected:
opts.Protected = true
default:
panic("telebot: unsupported flag-option")
}
case ParseMode:
opts.ParseMode = opt
case Entities:
opts.Entities = opt
default:
panic("telebot: unsupported send-option")
}
}
return opts
}
func (b *Bot) embedSendOptions(params map[string]string, opt *SendOptions) {
if b.parseMode != ModeDefault {
params["parse_mode"] = b.parseMode
}
if opt == nil {
return
}
if opt.MessageThreadId != nil {
params["message_thread_id"] = strconv.Itoa(*opt.MessageThreadId)
}
if opt.ReplyTo != nil && opt.ReplyTo.ID != 0 {
params["reply_to_message_id"] = strconv.Itoa(opt.ReplyTo.ID)
}
if opt.DisableWebPagePreview {
params["disable_web_page_preview"] = "true"
}
if opt.DisableNotification {
params["disable_notification"] = "true"
}
if opt.ParseMode != ModeDefault {
params["parse_mode"] = opt.ParseMode
}
if len(opt.Entities) > 0 {
delete(params, "parse_mode")
entities, _ := json.Marshal(opt.Entities)
if params["caption"] != "" {
params["caption_entities"] = string(entities)
} else {
params["entities"] = string(entities)
}
}
if opt.AllowWithoutReply {
params["allow_sending_without_reply"] = "true"
}
if opt.ReplyMarkup != nil {
processButtons(opt.ReplyMarkup.InlineKeyboard)
replyMarkup, _ := json.Marshal(opt.ReplyMarkup)
params["reply_markup"] = string(replyMarkup)
}
if opt.Protected {
params["protect_content"] = "true"
}
}
func processButtons(keys [][]InlineButton) {
if keys == nil || len(keys) < 1 || len(keys[0]) < 1 {
return
}
for i := range keys {
for j := range keys[i] {
key := &keys[i][j]
if key.Unique != "" {
// Format: "\f<callback_name>|<data>"
data := key.Data
if data == "" {
key.Data = "\f" + key.Unique
} else {
key.Data = "\f" + key.Unique + "|" + data
}
}
}
}
}

188
payments.go Normal file
View File

@ -0,0 +1,188 @@
package telebot
import (
"encoding/json"
"math"
"strconv"
)
// ShippingQuery contains information about an incoming shipping query.
type ShippingQuery struct {
Sender *User `json:"from"`
ID string `json:"id"`
Payload string `json:"invoice_payload"`
Address ShippingAddress `json:"shipping_address"`
}
// ShippingAddress represents a shipping address.
type ShippingAddress struct {
CountryCode string `json:"country_code"`
State string `json:"state"`
City string `json:"city"`
StreetLine1 string `json:"street_line1"`
StreetLine2 string `json:"street_line2"`
PostCode string `json:"post_code"`
}
// ShippingOption represents one shipping option.
type ShippingOption struct {
ID string `json:"id"`
Title string `json:"title"`
Prices []Price `json:"prices"`
}
// Payment contains basic information about a successful payment.
type Payment struct {
Currency string `json:"currency"`
Total int `json:"total_amount"`
Payload string `json:"invoice_payload"`
OptionID string `json:"shipping_option_id"`
Order Order `json:"order_info"`
TelegramChargeID string `json:"telegram_payment_charge_id"`
ProviderChargeID string `json:"provider_payment_charge_id"`
}
// PreCheckoutQuery contains information about an incoming pre-checkout query.
type PreCheckoutQuery struct {
Sender *User `json:"from"`
ID string `json:"id"`
Currency string `json:"currency"`
Payload string `json:"invoice_payload"`
Total int `json:"total_amount"`
OptionID string `json:"shipping_option_id"`
Order Order `json:"order_info"`
}
// Order represents information about an order.
type Order struct {
Name string `json:"name"`
PhoneNumber string `json:"phone_number"`
Email string `json:"email"`
Address ShippingAddress `json:"shipping_address"`
}
// Invoice contains basic information about an invoice.
type Invoice struct {
Title string `json:"title"`
Description string `json:"description"`
Payload string `json:"payload"`
Currency string `json:"currency"`
Prices []Price `json:"prices"`
Token string `json:"provider_token"`
Data string `json:"provider_data"`
Photo *Photo `json:"photo"`
PhotoSize int `json:"photo_size"`
// Unique deep-linking parameter that can be used to
// generate this invoice when used as a start parameter (0).
Start string `json:"start_parameter"`
// Shows the total price in the smallest units of the currency.
// For example, for a price of US$ 1.45 pass amount = 145.
Total int `json:"total_amount"`
MaxTipAmount int `json:"max_tip_amount"`
SuggestedTipAmounts []int `json:"suggested_tip_amounts"`
NeedName bool `json:"need_name"`
NeedPhoneNumber bool `json:"need_phone_number"`
NeedEmail bool `json:"need_email"`
NeedShippingAddress bool `json:"need_shipping_address"`
SendPhoneNumber bool `json:"send_phone_number_to_provider"`
SendEmail bool `json:"send_email_to_provider"`
Flexible bool `json:"is_flexible"`
}
func (i Invoice) params() map[string]string {
params := map[string]string{
"title": i.Title,
"description": i.Description,
"start_parameter": i.Start,
"payload": i.Payload,
"provider_token": i.Token,
"provider_data": i.Data,
"currency": i.Currency,
"max_tip_amount": strconv.Itoa(i.MaxTipAmount),
"need_name": strconv.FormatBool(i.NeedName),
"need_phone_number": strconv.FormatBool(i.NeedPhoneNumber),
"need_email": strconv.FormatBool(i.NeedEmail),
"need_shipping_address": strconv.FormatBool(i.NeedShippingAddress),
"send_phone_number_to_provider": strconv.FormatBool(i.SendPhoneNumber),
"send_email_to_provider": strconv.FormatBool(i.SendEmail),
"is_flexible": strconv.FormatBool(i.Flexible),
}
if i.Photo != nil {
if i.Photo.FileURL != "" {
params["photo_url"] = i.Photo.FileURL
}
if i.PhotoSize > 0 {
params["photo_size"] = strconv.Itoa(i.PhotoSize)
}
if i.Photo.Width > 0 {
params["photo_width"] = strconv.Itoa(i.Photo.Width)
}
if i.Photo.Height > 0 {
params["photo_height"] = strconv.Itoa(i.Photo.Height)
}
}
if len(i.Prices) > 0 {
data, _ := json.Marshal(i.Prices)
params["prices"] = string(data)
}
if len(i.SuggestedTipAmounts) > 0 {
var amounts []string
for _, n := range i.SuggestedTipAmounts {
amounts = append(amounts, strconv.Itoa(n))
}
data, _ := json.Marshal(amounts)
params["suggested_tip_amounts"] = string(data)
}
return params
}
// Price represents a portion of the price for goods or services.
type Price struct {
Label string `json:"label"`
Amount int `json:"amount"`
}
// Currency contains information about supported currency for payments.
type Currency struct {
Code string `json:"code"`
Title string `json:"title"`
Symbol string `json:"symbol"`
Native string `json:"native"`
ThousandsSep string `json:"thousands_sep"`
DecimalSep string `json:"decimal_sep"`
SymbolLeft bool `json:"symbol_left"`
SpaceBetween bool `json:"space_between"`
Exp int `json:"exp"`
MinAmount interface{} `json:"min_amount"`
MaxAmount interface{} `json:"max_amount"`
}
func (c Currency) FromTotal(total int) float64 {
return float64(total) / math.Pow(10, float64(c.Exp))
}
func (c Currency) ToTotal(total float64) int {
return int(total) * int(math.Pow(10, float64(c.Exp)))
}
// CreateInvoiceLink creates a link for a payment invoice.
func (b *Bot) CreateInvoiceLink(i Invoice) (string, error) {
data, err := b.Raw("createInvoiceLink", i.params())
if err != nil {
return "", err
}
var resp struct {
Result string
}
if err := json.Unmarshal(data, &resp); err != nil {
return "", wrapError(err)
}
return resp.Result, nil
}

14
payments_data.go Normal file

File diff suppressed because one or more lines are too long

75
poll.go Normal file
View File

@ -0,0 +1,75 @@
package telebot
import "time"
// PollType defines poll types.
type PollType string
const (
// NOTE:
// Despite "any" type isn't described in documentation,
// it needed for proper KeyboardButtonPollType marshaling.
PollAny PollType = "any"
PollQuiz PollType = "quiz"
PollRegular PollType = "regular"
)
// Poll contains information about a poll.
type Poll struct {
ID string `json:"id"`
Type PollType `json:"type"`
Question string `json:"question"`
Options []PollOption `json:"options"`
VoterCount int `json:"total_voter_count"`
// (Optional)
Closed bool `json:"is_closed,omitempty"`
CorrectOption int `json:"correct_option_id,omitempty"`
MultipleAnswers bool `json:"allows_multiple_answers,omitempty"`
Explanation string `json:"explanation,omitempty"`
ParseMode ParseMode `json:"explanation_parse_mode,omitempty"`
Entities []MessageEntity `json:"explanation_entities"`
// True by default, shouldn't be omitted.
Anonymous bool `json:"is_anonymous"`
// (Mutually exclusive)
OpenPeriod int `json:"open_period,omitempty"`
CloseUnixdate int64 `json:"close_date,omitempty"`
}
// PollOption contains information about one answer option in a poll.
type PollOption struct {
Text string `json:"text"`
VoterCount int `json:"voter_count"`
}
// PollAnswer represents an answer of a user in a non-anonymous poll.
type PollAnswer struct {
PollID string `json:"poll_id"`
Sender *User `json:"user"`
Options []int `json:"option_ids"`
}
// IsRegular says whether poll is a regular.
func (p *Poll) IsRegular() bool {
return p.Type == PollRegular
}
// IsQuiz says whether poll is a quiz.
func (p *Poll) IsQuiz() bool {
return p.Type == PollQuiz
}
// CloseDate returns the close date of poll in local time.
func (p *Poll) CloseDate() time.Time {
return time.Unix(p.CloseUnixdate, 0)
}
// AddOptions adds text options to the poll.
func (p *Poll) AddOptions(opts ...string) {
for _, t := range opts {
p.Options = append(p.Options, PollOption{Text: t})
}
}

52
poll_test.go Normal file
View File

@ -0,0 +1,52 @@
package telebot
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPoll(t *testing.T) {
assert.True(t, (&Poll{Type: PollRegular}).IsRegular())
assert.True(t, (&Poll{Type: PollQuiz}).IsQuiz())
p := &Poll{}
opts := []PollOption{{Text: "Option 1"}, {Text: "Option 2"}}
p.AddOptions(opts[0].Text, opts[1].Text)
assert.Equal(t, opts, p.Options)
}
func TestPollSend(t *testing.T) {
if b == nil {
t.Skip("Cached bot instance is bad (probably wrong or empty TELEBOT_SECRET)")
}
if userID == 0 {
t.Skip("USER_ID is required for Poll methods test")
}
_, err := b.Send(user, &Poll{}) // empty poll
assert.Equal(t, ErrBadPollOptions, err)
poll := &Poll{
Type: PollQuiz,
Question: "Test Poll",
CloseUnixdate: time.Now().Unix() + 60,
Explanation: "Explanation",
}
poll.AddOptions("1", "2")
msg, err := b.Send(user, poll)
require.NoError(t, err)
assert.Equal(t, poll.Type, msg.Poll.Type)
assert.Equal(t, poll.Question, msg.Poll.Question)
assert.Equal(t, poll.Options, msg.Poll.Options)
assert.Equal(t, poll.CloseUnixdate, msg.Poll.CloseUnixdate)
assert.Equal(t, poll.CloseDate(), msg.Poll.CloseDate())
p, err := b.StopPoll(msg)
require.NoError(t, err)
assert.Equal(t, poll.Options, p.Options)
assert.Equal(t, 0, p.VoterCount)
}

115
poller.go Normal file
View File

@ -0,0 +1,115 @@
package telebot
import "time"
// Poller is a provider of Updates.
//
// All pollers must implement Poll(), which accepts bot
// pointer and subscription channel and start polling
// synchronously straight away.
//
type Poller interface {
// Poll is supposed to take the bot object
// subscription channel and start polling
// for Updates immediately.
//
// Poller must listen for stop constantly and close
// it as soon as it's done polling.
Poll(b *Bot, updates chan Update, stop chan struct{})
}
// LongPoller is a classic LongPoller with timeout.
type LongPoller struct {
Limit int
Timeout time.Duration
LastUpdateID int
// AllowedUpdates contains the update types
// you want your bot to receive.
//
// Possible values:
// message
// edited_message
// channel_post
// edited_channel_post
// inline_query
// chosen_inline_result
// callback_query
// shipping_query
// pre_checkout_query
// poll
// poll_answer
//
AllowedUpdates []string `yaml:"allowed_updates"`
}
// Poll does long polling.
func (p *LongPoller) Poll(b *Bot, dest chan Update, stop chan struct{}) {
for {
select {
case <-stop:
return
default:
}
updates, err := b.getUpdates(p.LastUpdateID+1, p.Limit, p.Timeout, p.AllowedUpdates)
if err != nil {
b.debug(err)
continue
}
for _, update := range updates {
p.LastUpdateID = update.ID
dest <- update
}
}
}
// MiddlewarePoller is a special kind of poller that acts
// like a filter for updates. It could be used for spam
// handling, banning or whatever.
//
// For heavy middleware, use increased capacity.
//
type MiddlewarePoller struct {
Capacity int // Default: 1
Poller Poller
Filter func(*Update) bool
}
// NewMiddlewarePoller wait for it... constructs a new middleware poller.
func NewMiddlewarePoller(original Poller, filter func(*Update) bool) *MiddlewarePoller {
return &MiddlewarePoller{
Poller: original,
Filter: filter,
}
}
// Poll sieves updates through middleware filter.
func (p *MiddlewarePoller) Poll(b *Bot, dest chan Update, stop chan struct{}) {
if p.Capacity < 1 {
p.Capacity = 1
}
middle := make(chan Update, p.Capacity)
stopPoller := make(chan struct{})
stopConfirm := make(chan struct{})
go func() {
p.Poller.Poll(b, middle, stopPoller)
close(stopConfirm)
}()
for {
select {
case <-stop:
close(stopPoller)
<-stopConfirm
return
case upd := <-middle:
if p.Filter(&upd) {
dest <- upd
}
}
}
}

67
poller_test.go Normal file
View File

@ -0,0 +1,67 @@
package telebot
import (
"testing"
"github.com/stretchr/testify/assert"
)
type testPoller struct {
updates chan Update
done chan struct{}
}
func newTestPoller() *testPoller {
return &testPoller{
updates: make(chan Update, 1),
done: make(chan struct{}, 1),
}
}
func (p *testPoller) Poll(b *Bot, updates chan Update, stop chan struct{}) {
for {
select {
case upd := <-p.updates:
updates <- upd
case <-stop:
return
default:
}
}
}
func TestMiddlewarePoller(t *testing.T) {
tp := newTestPoller()
var ids []int
pref := defaultSettings()
pref.Offline = true
b, err := NewBot(pref)
if err != nil {
t.Fatal(err)
}
b.Poller = NewMiddlewarePoller(tp, func(u *Update) bool {
if u.ID > 0 {
ids = append(ids, u.ID)
return true
}
tp.done <- struct{}{}
return false
})
go func() {
tp.updates <- Update{ID: 1}
tp.updates <- Update{ID: 2}
tp.updates <- Update{ID: 0}
}()
go b.Start()
<-tp.done
b.Stop()
assert.Contains(t, ids, 1)
assert.Contains(t, ids, 2)
}

408
sendable.go Normal file
View File

@ -0,0 +1,408 @@
package telebot
import (
"encoding/json"
"fmt"
"path/filepath"
"strconv"
)
// Recipient is any possible endpoint you can send
// messages to: either user, group or a channel.
type Recipient interface {
Recipient() string // must return legit Telegram chat_id or username
}
// Sendable is any object that can send itself.
//
// This is pretty cool, since it lets bots implement
// custom Sendables for complex kind of media or
// chat objects spanning across multiple messages.
//
type Sendable interface {
Send(*Bot, Recipient, *SendOptions) (*Message, error)
}
// Send delivers media through bot b to recipient.
func (p *Photo) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
"caption": p.Caption,
}
b.embedSendOptions(params, opt)
msg, err := b.sendMedia(p, params, nil)
if err != nil {
return nil, err
}
msg.Photo.File.stealRef(&p.File)
*p = *msg.Photo
p.Caption = msg.Caption
return msg, nil
}
// Send delivers media through bot b to recipient.
func (a *Audio) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
"caption": a.Caption,
"performer": a.Performer,
"title": a.Title,
"file_name": a.FileName,
}
b.embedSendOptions(params, opt)
if a.Duration != 0 {
params["duration"] = strconv.Itoa(a.Duration)
}
msg, err := b.sendMedia(a, params, thumbnailToFilemap(a.Thumbnail))
if err != nil {
return nil, err
}
if msg.Audio != nil {
msg.Audio.File.stealRef(&a.File)
*a = *msg.Audio
a.Caption = msg.Caption
}
if msg.Document != nil {
msg.Document.File.stealRef(&a.File)
a.File = msg.Document.File
}
return msg, nil
}
// Send delivers media through bot b to recipient.
func (d *Document) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
"caption": d.Caption,
"file_name": d.FileName,
}
b.embedSendOptions(params, opt)
if d.FileSize != 0 {
params["file_size"] = strconv.FormatInt(d.FileSize, 10)
}
if d.DisableTypeDetection {
params["disable_content_type_detection"] = "true"
}
msg, err := b.sendMedia(d, params, thumbnailToFilemap(d.Thumbnail))
if err != nil {
return nil, err
}
if doc := msg.Document; doc != nil {
doc.File.stealRef(&d.File)
*d = *doc
d.Caption = msg.Caption
} else if vid := msg.Video; vid != nil {
vid.File.stealRef(&d.File)
d.Caption = vid.Caption
d.MIME = vid.MIME
d.Thumbnail = vid.Thumbnail
}
return msg, nil
}
// Send delivers media through bot b to recipient.
func (s *Sticker) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
}
b.embedSendOptions(params, opt)
msg, err := b.sendMedia(s, params, nil)
if err != nil {
return nil, err
}
msg.Sticker.File.stealRef(&s.File)
*s = *msg.Sticker
return msg, nil
}
// Send delivers media through bot b to recipient.
func (v *Video) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
"caption": v.Caption,
"file_name": v.FileName,
}
b.embedSendOptions(params, opt)
if v.Duration != 0 {
params["duration"] = strconv.Itoa(v.Duration)
}
if v.Width != 0 {
params["width"] = strconv.Itoa(v.Width)
}
if v.Height != 0 {
params["height"] = strconv.Itoa(v.Height)
}
if v.Streaming {
params["supports_streaming"] = "true"
}
msg, err := b.sendMedia(v, params, thumbnailToFilemap(v.Thumbnail))
if err != nil {
return nil, err
}
if vid := msg.Video; vid != nil {
vid.File.stealRef(&v.File)
*v = *vid
v.Caption = msg.Caption
} else if doc := msg.Document; doc != nil {
// If video has no sound, Telegram can turn it into Document (GIF)
doc.File.stealRef(&v.File)
v.Caption = doc.Caption
v.MIME = doc.MIME
v.Thumbnail = doc.Thumbnail
}
return msg, nil
}
// Send delivers animation through bot b to recipient.
func (a *Animation) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
"caption": a.Caption,
"file_name": a.FileName,
}
b.embedSendOptions(params, opt)
if a.Duration != 0 {
params["duration"] = strconv.Itoa(a.Duration)
}
if a.Width != 0 {
params["width"] = strconv.Itoa(a.Width)
}
if a.Height != 0 {
params["height"] = strconv.Itoa(a.Height)
}
// 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.sendMedia(a, params, thumbnailToFilemap(a.Thumbnail))
if err != nil {
return nil, err
}
if anim := msg.Animation; anim != nil {
anim.File.stealRef(&a.File)
*a = *msg.Animation
} else if doc := msg.Document; doc != nil {
*a = Animation{
File: doc.File,
Thumbnail: doc.Thumbnail,
MIME: doc.MIME,
FileName: doc.FileName,
}
}
a.Caption = msg.Caption
return msg, nil
}
// Send delivers media through bot b to recipient.
func (v *Voice) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
"caption": v.Caption,
}
b.embedSendOptions(params, opt)
if v.Duration != 0 {
params["duration"] = strconv.Itoa(v.Duration)
}
msg, err := b.sendMedia(v, params, nil)
if err != nil {
return nil, err
}
msg.Voice.File.stealRef(&v.File)
*v = *msg.Voice
return msg, nil
}
// Send delivers media through bot b to recipient.
func (v *VideoNote) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
}
b.embedSendOptions(params, opt)
if v.Duration != 0 {
params["duration"] = strconv.Itoa(v.Duration)
}
if v.Length != 0 {
params["length"] = strconv.Itoa(v.Length)
}
msg, err := b.sendMedia(v, params, thumbnailToFilemap(v.Thumbnail))
if err != nil {
return nil, err
}
msg.VideoNote.File.stealRef(&v.File)
*v = *msg.VideoNote
return msg, nil
}
// Send delivers media through bot b to recipient.
func (x *Location) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
"latitude": fmt.Sprintf("%f", x.Lat),
"longitude": fmt.Sprintf("%f", x.Lng),
"live_period": strconv.Itoa(x.LivePeriod),
}
if x.HorizontalAccuracy != nil {
params["horizontal_accuracy"] = fmt.Sprintf("%f", *x.HorizontalAccuracy)
}
if x.Heading != 0 {
params["heading"] = strconv.Itoa(x.Heading)
}
if x.AlertRadius != 0 {
params["proximity_alert_radius"] = strconv.Itoa(x.Heading)
}
b.embedSendOptions(params, opt)
data, err := b.Raw("sendLocation", params)
if err != nil {
return nil, err
}
return extractMessage(data)
}
// Send delivers media through bot b to recipient.
func (v *Venue) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
"latitude": fmt.Sprintf("%f", v.Location.Lat),
"longitude": fmt.Sprintf("%f", v.Location.Lng),
"title": v.Title,
"address": v.Address,
"foursquare_id": v.FoursquareID,
"foursquare_type": v.FoursquareType,
"google_place_id": v.GooglePlaceID,
"google_place_type": v.GooglePlaceType,
}
b.embedSendOptions(params, opt)
data, err := b.Raw("sendVenue", params)
if err != nil {
return nil, err
}
return extractMessage(data)
}
// Send delivers invoice through bot b to recipient.
func (i *Invoice) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := i.params()
params["chat_id"] = to.Recipient()
b.embedSendOptions(params, opt)
data, err := b.Raw("sendInvoice", params)
if err != nil {
return nil, err
}
return extractMessage(data)
}
// Send delivers poll through bot b to recipient.
func (p *Poll) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
"question": p.Question,
"type": string(p.Type),
"is_closed": strconv.FormatBool(p.Closed),
"is_anonymous": strconv.FormatBool(p.Anonymous),
"allows_multiple_answers": strconv.FormatBool(p.MultipleAnswers),
"correct_option_id": strconv.Itoa(p.CorrectOption),
}
if p.Explanation != "" {
params["explanation"] = p.Explanation
params["explanation_parse_mode"] = p.ParseMode
}
if p.OpenPeriod != 0 {
params["open_period"] = strconv.Itoa(p.OpenPeriod)
} else if p.CloseUnixdate != 0 {
params["close_date"] = strconv.FormatInt(p.CloseUnixdate, 10)
}
b.embedSendOptions(params, opt)
var options []string
for _, o := range p.Options {
options = append(options, o.Text)
}
opts, _ := json.Marshal(options)
params["options"] = string(opts)
data, err := b.Raw("sendPoll", params)
if err != nil {
return nil, err
}
return extractMessage(data)
}
// Send delivers dice through bot b to recipient.
func (d *Dice) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
"emoji": string(d.Type),
}
b.embedSendOptions(params, opt)
data, err := b.Raw("sendDice", params)
if err != nil {
return nil, err
}
return extractMessage(data)
}
// Send delivers game through bot b to recipient.
func (g *Game) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
params := map[string]string{
"chat_id": to.Recipient(),
"game_short_name": g.Name,
}
b.embedSendOptions(params, opt)
data, err := b.Raw("sendGame", params)
if err != nil {
return nil, err
}
return extractMessage(data)
}
func thumbnailToFilemap(thumb *Photo) map[string]File {
if thumb != nil {
return map[string]File{"thumb": thumb.File}
}
return nil
}

212
stickers.go Normal file
View File

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

136
telebot.go Normal file
View File

@ -0,0 +1,136 @@
// Package telebot is a framework for Telegram bots.
//
// Example:
//
// package main
//
// import (
// "time"
// tele "gopkg.in/telebot.v3"
// )
//
// func main() {
// b, err := tele.NewBot(tele.Settings{
// Token: "...",
// Poller: &tele.LongPoller{Timeout: 10 * time.Second},
// })
// if err != nil {
// return
// }
//
// b.Handle("/start", func(c tele.Context) error {
// return c.Send("Hello world!")
// })
//
// b.Start()
// }
//
package telebot
import "errors"
var (
ErrBadRecipient = errors.New("telebot: recipient is nil")
ErrUnsupportedWhat = errors.New("telebot: unsupported what argument")
ErrCouldNotUpdate = errors.New("telebot: could not fetch new updates")
ErrTrueResult = errors.New("telebot: result is True")
ErrBadContext = errors.New("telebot: context does not contain message")
)
const DefaultApiURL = "https://api.telegram.org"
// These are one of the possible events Handle() can deal with.
//
// For convenience, all Telebot-provided endpoints start with
// an "alert" character \a.
//
const (
// Basic message handlers.
OnText = "\atext"
OnEdited = "\aedited"
OnPhoto = "\aphoto"
OnAudio = "\aaudio"
OnAnimation = "\aanimation"
OnDocument = "\adocument"
OnSticker = "\asticker"
OnVideo = "\avideo"
OnVoice = "\avoice"
OnVideoNote = "\avideo_note"
OnContact = "\acontact"
OnLocation = "\alocation"
OnVenue = "\avenue"
OnDice = "\adice"
OnInvoice = "\ainvoice"
OnPayment = "\apayment"
OnGame = "\agame"
OnPoll = "\apoll"
OnPollAnswer = "\apoll_answer"
OnPinned = "\apinned"
OnChannelPost = "\achannel_post"
OnEditedChannelPost = "\aedited_channel_post"
OnAddedToGroup = "\aadded_to_group"
OnUserJoined = "\auser_joined"
OnUserLeft = "\auser_left"
OnNewGroupTitle = "\anew_chat_title"
OnNewGroupPhoto = "\anew_chat_photo"
OnGroupPhotoDeleted = "\achat_photo_deleted"
OnGroupCreated = "\agroup_created"
OnSuperGroupCreated = "\asupergroup_created"
OnChannelCreated = "\achannel_created"
// OnMigration happens when group switches to
// a supergroup. You might want to update
// your internal references to this chat
// upon switching as its ID will change.
OnMigration = "\amigration"
OnMedia = "\amedia"
OnCallback = "\acallback"
OnQuery = "\aquery"
OnInlineResult = "\ainline_result"
OnShipping = "\ashipping_query"
OnCheckout = "\apre_checkout_query"
OnMyChatMember = "\amy_chat_member"
OnChatMember = "\achat_member"
OnChatJoinRequest = "\achat_join_request"
OnProximityAlert = "\aproximity_alert_triggered"
OnAutoDeleteTimer = "\amessage_auto_delete_timer_changed"
OnWebApp = "\aweb_app"
OnVideoChatStarted = "\avideo_chat_started"
OnVideoChatEnded = "\avideo_chat_ended"
OnVideoChatParticipants = "\avideo_chat_participants_invited"
OnVideoChatScheduled = "\avideo_chat_scheduled"
)
// ChatAction is a client-side status indicating bot activity.
type ChatAction string
const (
Typing ChatAction = "typing"
UploadingPhoto ChatAction = "upload_photo"
UploadingVideo ChatAction = "upload_video"
UploadingAudio ChatAction = "upload_audio"
UploadingDocument ChatAction = "upload_document"
UploadingVNote ChatAction = "upload_video_note"
RecordingVideo ChatAction = "record_video"
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
type ParseMode = string
const (
ModeDefault ParseMode = ""
ModeMarkdown ParseMode = "Markdown"
ModeMarkdownV2 ParseMode = "MarkdownV2"
ModeHTML ParseMode = "HTML"
)
// M is a shortcut for map[string]interface{}. Use it for passing
// arguments to the layout functions.
type M = map[string]interface{}

346
update.go Normal file
View File

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

29
video_chat.go Normal file
View File

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

18
web_app.go Normal file
View File

@ -0,0 +1,18 @@
package telebot
// WebApp represents a parameter of the inline keyboard button
// or the keyboard button used to launch Web App.
type WebApp struct {
URL string `json:"url"`
}
// WebAppMessage describes an inline message sent by a Web App on behalf of a user.
type WebAppMessage struct {
InlineMessageID string `json:"inline_message_id"`
}
// WebAppData object represents a data sent from a Web App to the bot
type WebAppData struct {
Data string `json:"data"`
Text string `json:"button_text"`
}

207
webhook.go Normal file
View File

@ -0,0 +1,207 @@
package telebot
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
)
// A WebhookTLS specifies the path to a key and a cert so the poller can open
// a TLS listener.
type WebhookTLS struct {
Key string `json:"key"`
Cert string `json:"cert"`
}
// A WebhookEndpoint describes the endpoint to which telegram will send its requests.
// This must be a public URL and can be a loadbalancer or something similar. If the
// endpoint uses TLS and the certificate is self-signed you have to add the certificate
// path of this certificate so telegram will trust it. This field can be ignored if you
// have a trusted certificate (letsencrypt, ...).
type WebhookEndpoint struct {
PublicURL string `json:"public_url"`
Cert string `json:"cert"`
}
// A Webhook configures the poller for webhooks. It opens a port on the given
// listen address. If TLS is filled, the listener will use the key and cert to open
// a secure port. Otherwise it will use plain HTTP.
//
// If you have a loadbalancer ore other infrastructure in front of your service, you
// must fill the Endpoint structure so this poller will send this data to telegram. If
// you leave these values empty, your local address will be sent to telegram which is mostly
// not what you want (at least while developing). If you have a single instance of your
// bot you should consider to use the LongPoller instead of a WebHook.
//
// You can also leave the Listen field empty. In this case it is up to the caller to
// add the Webhook to a http-mux.
//
type Webhook struct {
Listen string `json:"url"`
MaxConnections int `json:"max_connections"`
AllowedUpdates []string `json:"allowed_updates"`
IP string `json:"ip_address"`
DropUpdates bool `json:"drop_pending_updates"`
SecretToken string `json:"secret_token"`
// (WebhookInfo)
HasCustomCert bool `json:"has_custom_certificate"`
PendingUpdates int `json:"pending_update_count"`
ErrorUnixtime int64 `json:"last_error_date"`
ErrorMessage string `json:"last_error_message"`
SyncErrorUnixtime int64 `json:"last_synchronization_error_date"`
TLS *WebhookTLS
Endpoint *WebhookEndpoint
dest chan<- Update
bot *Bot
}
func (h *Webhook) getFiles() map[string]File {
m := make(map[string]File)
if h.TLS != nil {
m["certificate"] = FromDisk(h.TLS.Cert)
}
// check if it is overwritten by an endpoint
if h.Endpoint != nil {
if h.Endpoint.Cert == "" {
// this can be the case if there is a loadbalancer or reverseproxy in
// front with a public cert. in this case we do not need to upload it
// to telegram. we delete the certificate from the map, because someone
// can have an internal TLS listener with a private cert
delete(m, "certificate")
} else {
// someone configured a certificate
m["certificate"] = FromDisk(h.Endpoint.Cert)
}
}
return m
}
func (h *Webhook) getParams() map[string]string {
params := make(map[string]string)
if h.MaxConnections != 0 {
params["max_connections"] = strconv.Itoa(h.MaxConnections)
}
if len(h.AllowedUpdates) > 0 {
data, _ := json.Marshal(h.AllowedUpdates)
params["allowed_updates"] = string(data)
}
if h.IP != "" {
params["ip_address"] = h.IP
}
if h.DropUpdates {
params["drop_pending_updates"] = strconv.FormatBool(h.DropUpdates)
}
if h.SecretToken != "" {
params["secret_token"] = h.SecretToken
}
if h.TLS != nil {
params["url"] = "https://" + h.Listen
} else {
// this will not work with telegram, they want TLS
// but i allow this because telegram will send an error
// when you register this hook. in their docs they write
// that port 80/http is allowed ...
params["url"] = "http://" + h.Listen
}
if h.Endpoint != nil {
params["url"] = h.Endpoint.PublicURL
}
return params
}
func (h *Webhook) Poll(b *Bot, dest chan Update, stop chan struct{}) {
if err := b.SetWebhook(h); err != nil {
b.debug(err)
close(stop)
return
}
// store the variables so the HTTP-handler can use 'em
h.dest = dest
h.bot = b
if h.Listen == "" {
h.waitForStop(stop)
return
}
s := &http.Server{
Addr: h.Listen,
Handler: h,
}
go func(stop chan struct{}) {
h.waitForStop(stop)
s.Shutdown(context.Background())
}(stop)
if h.TLS != nil {
s.ListenAndServeTLS(h.TLS.Cert, h.TLS.Key)
} else {
s.ListenAndServe()
}
}
func (h *Webhook) waitForStop(stop chan struct{}) {
<-stop
close(stop)
}
// The handler simply reads the update from the body of the requests
// and writes them to the update channel.
func (h *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.SecretToken != "" && r.Header.Get("X-Telegram-Bot-Api-Secret-Token") != h.SecretToken {
h.bot.debug(fmt.Errorf("invalid secret token in request"))
return
}
var update Update
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
h.bot.debug(fmt.Errorf("cannot decode update: %v", err))
return
}
h.dest <- update
}
// Webhook returns the current webhook status.
func (b *Bot) Webhook() (*Webhook, error) {
data, err := b.Raw("getWebhookInfo", nil)
if err != nil {
return nil, err
}
var resp struct {
Result Webhook
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, wrapError(err)
}
return &resp.Result, nil
}
// SetWebhook configures a bot to receive incoming
// updates via an outgoing webhook.
func (b *Bot) SetWebhook(w *Webhook) error {
_, err := b.sendFiles("setWebhook", w.getFiles(), w.getParams())
return err
}
// RemoveWebhook removes webhook integration.
func (b *Bot) RemoveWebhook(dropPending ...bool) error {
drop := false
if len(dropPending) > 0 {
drop = dropPending[0]
}
_, err := b.Raw("deleteWebhook", map[string]bool{
"drop_pending_updates": drop,
})
return err
}