init
This commit is contained in:
commit
1b497f2b9b
17
.github/workflows/go.yml
vendored
Normal file
17
.github/workflows/go.yml
vendored
Normal 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
34
.gitignore
vendored
Normal 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
22
LICENSE
Normal 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
489
README.md
Normal 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
287
admin.go
Normal 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
39
admin_test.go
Normal 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
331
api.go
Normal 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
120
api_test.go
Normal 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)
|
||||||
|
}
|
649
bot_test.go
Normal file
649
bot_test.go
Normal 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
89
callback.go
Normal 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
453
chat.go
Normal 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
21
chat_test.go
Normal 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
85
commands.go
Normal 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
486
context.go
Normal 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
18
context_test.go
Normal 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
30
editable.go
Normal 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
242
errors.go
Normal 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
87
file.go
Normal 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
24
file_test.go
Normal 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
99
game.go
Normal 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
34
go.mod
Normal 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
506
go.sum
Normal 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
139
inline.go
Normal 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 don‘t support
|
||||||
|
// pagination. Offset length can’t 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
373
inline_types.go
Normal 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
73
input_types.go
Normal 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
122
layout/config.go
Normal 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
43
layout/default.go
Normal 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
76
layout/example.yml
Normal 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
579
layout/layout.go
Normal 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 <, yaml.Unmarshal(data, <)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
125
layout/layout_test.go
Normal 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
1
layout/locales/en.yml
Normal file
@ -0,0 +1 @@
|
|||||||
|
article_message: This is an article.
|
50
layout/middleware.go
Normal file
50
layout/middleware.go
Normal 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
268
layout/parser.go
Normal 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
322
markup.go
Normal 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 bot‘s 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
65
markup_test.go
Normal 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
355
media.go
Normal 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
428
message.go
Normal 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
29
middleware.go
Normal 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
27
middleware/logger.go
Normal 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
62
middleware/middleware.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
middleware/middleware_test.go
Normal file
30
middleware/middleware_test.go
Normal 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
65
middleware/restrict.go
Normal 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
216
options.go
Normal 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
188
payments.go
Normal 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
14
payments_data.go
Normal file
File diff suppressed because one or more lines are too long
75
poll.go
Normal file
75
poll.go
Normal 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
52
poll_test.go
Normal 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
115
poller.go
Normal 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
67
poller_test.go
Normal 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
408
sendable.go
Normal 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
212
stickers.go
Normal 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
136
telebot.go
Normal 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
346
update.go
Normal 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
29
video_chat.go
Normal 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
18
web_app.go
Normal 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
207
webhook.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user