copy from 537c005
This commit is contained in:
commit
f86a67eb8f
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.idea/
|
||||
coverage.out
|
||||
tmp/
|
||||
book/
|
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Syfaro
|
||||
|
||||
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.
|
121
README.md
Normal file
121
README.md
Normal file
@ -0,0 +1,121 @@
|
||||
# Golang bindings for the Telegram Bot API
|
||||
|
||||
[![Go Reference](https://pkg.go.dev/badge/github.com/go-telegram-bot-api/telegram-bot-api/v5.svg)](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5)
|
||||
[![Test](https://github.com/go-telegram-bot-api/telegram-bot-api/actions/workflows/test.yml/badge.svg)](https://github.com/go-telegram-bot-api/telegram-bot-api/actions/workflows/test.yml)
|
||||
|
||||
All methods are fairly self-explanatory, and reading the [godoc](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5) page should
|
||||
explain everything. If something isn't clear, open an issue or submit
|
||||
a pull request.
|
||||
|
||||
There are more tutorials and high-level information on the website, [go-telegram-bot-api.dev](https://go-telegram-bot-api.dev).
|
||||
|
||||
The scope of this project is just to provide a wrapper around the API
|
||||
without any additional features. There are other projects for creating
|
||||
something with plugins and command handlers without having to design
|
||||
all that yourself.
|
||||
|
||||
Join [the development group](https://telegram.me/go_telegram_bot_api) if
|
||||
you want to ask questions or discuss development.
|
||||
|
||||
## Example
|
||||
|
||||
First, ensure the library is installed and up to date by running
|
||||
`go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5`.
|
||||
|
||||
This is a very simple bot that just displays any gotten updates,
|
||||
then replies it to that chat.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken")
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
bot.Debug = true
|
||||
|
||||
log.Printf("Authorized on account %s", bot.Self.UserName)
|
||||
|
||||
u := tgbotapi.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
|
||||
updates := bot.GetUpdatesChan(u)
|
||||
|
||||
for update := range updates {
|
||||
if update.Message != nil { // If we got a message
|
||||
log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text)
|
||||
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
|
||||
msg.ReplyToMessageID = update.Message.MessageID
|
||||
|
||||
bot.Send(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you need to use webhooks (if you wish to run on Google App Engine),
|
||||
you may use a slightly different method.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
bot.Debug = true
|
||||
|
||||
log.Printf("Authorized on account %s", bot.Self.UserName)
|
||||
|
||||
wh, _ := tgbotapi.NewWebhookWithCert("https://www.example.com:8443/"+bot.Token, "cert.pem")
|
||||
|
||||
_, err = bot.Request(wh)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
info, err := bot.GetWebhookInfo()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if info.LastErrorDate != 0 {
|
||||
log.Printf("Telegram callback failed: %s", info.LastErrorMessage)
|
||||
}
|
||||
|
||||
updates := bot.ListenForWebhook("/" + bot.Token)
|
||||
go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil)
|
||||
|
||||
for update := range updates {
|
||||
log.Printf("%+v\n", update)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you need, you may generate a self-signed certificate, as this requires
|
||||
HTTPS / TLS. The above example tells Telegram that this is your
|
||||
certificate and that it should be trusted, even though it is not
|
||||
properly signed.
|
||||
|
||||
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3560 -subj "//O=Org\CN=Test" -nodes
|
||||
|
||||
Now that [Let's Encrypt](https://letsencrypt.org) is available,
|
||||
you may wish to generate your free TLS certificate there.
|
9
book.toml
Normal file
9
book.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[book]
|
||||
authors = ["Syfaro"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "docs"
|
||||
title = "Go Telegram Bot API"
|
||||
|
||||
[output.html]
|
||||
git-repository-url = "https://github.com/go-telegram-bot-api/telegram-bot-api"
|
748
bot.go
Normal file
748
bot.go
Normal file
@ -0,0 +1,748 @@
|
||||
// Package tgbotapi has functions and types used for interacting with
|
||||
// the Telegram Bot API.
|
||||
package tgbotapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HTTPClient is the type needed for the bot to perform HTTP requests.
|
||||
type HTTPClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// BotAPI allows you to interact with the Telegram Bot API.
|
||||
type BotAPI struct {
|
||||
Token string `json:"token"`
|
||||
Debug bool `json:"debug"`
|
||||
Buffer int `json:"buffer"`
|
||||
|
||||
Self User `json:"-"`
|
||||
Client HTTPClient `json:"-"`
|
||||
shutdownChannel chan interface{}
|
||||
|
||||
apiEndpoint string
|
||||
}
|
||||
|
||||
// NewBotAPI creates a new BotAPI instance.
|
||||
//
|
||||
// It requires a token, provided by @BotFather on Telegram.
|
||||
func NewBotAPI(token string) (*BotAPI, error) {
|
||||
return NewBotAPIWithClient(token, APIEndpoint, &http.Client{})
|
||||
}
|
||||
|
||||
// NewBotAPIWithAPIEndpoint creates a new BotAPI instance
|
||||
// and allows you to pass API endpoint.
|
||||
//
|
||||
// It requires a token, provided by @BotFather on Telegram and API endpoint.
|
||||
func NewBotAPIWithAPIEndpoint(token, apiEndpoint string) (*BotAPI, error) {
|
||||
return NewBotAPIWithClient(token, apiEndpoint, &http.Client{})
|
||||
}
|
||||
|
||||
// NewBotAPIWithClient creates a new BotAPI instance
|
||||
// and allows you to pass a http.Client.
|
||||
//
|
||||
// It requires a token, provided by @BotFather on Telegram and API endpoint.
|
||||
func NewBotAPIWithClient(token, apiEndpoint string, client HTTPClient) (*BotAPI, error) {
|
||||
bot := &BotAPI{
|
||||
Token: token,
|
||||
Client: client,
|
||||
Buffer: 100,
|
||||
shutdownChannel: make(chan interface{}),
|
||||
|
||||
apiEndpoint: apiEndpoint,
|
||||
}
|
||||
|
||||
self, err := bot.GetMe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bot.Self = self
|
||||
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
// SetAPIEndpoint changes the Telegram Bot API endpoint used by the instance.
|
||||
func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) {
|
||||
bot.apiEndpoint = apiEndpoint
|
||||
}
|
||||
|
||||
func buildParams(in Params) url.Values {
|
||||
if in == nil {
|
||||
return url.Values{}
|
||||
}
|
||||
|
||||
out := url.Values{}
|
||||
|
||||
for key, value := range in {
|
||||
out.Set(key, value)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// MakeRequest makes a request to a specific endpoint with our token.
|
||||
func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) {
|
||||
if bot.Debug {
|
||||
log.Printf("Endpoint: %s, params: %v\n", endpoint, params)
|
||||
}
|
||||
|
||||
method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
|
||||
|
||||
values := buildParams(params)
|
||||
|
||||
req, err := http.NewRequest("POST", method, strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
return &APIResponse{}, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := bot.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var apiResp APIResponse
|
||||
bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
|
||||
if err != nil {
|
||||
return &apiResp, err
|
||||
}
|
||||
|
||||
if bot.Debug {
|
||||
log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
|
||||
}
|
||||
|
||||
if !apiResp.Ok {
|
||||
var parameters ResponseParameters
|
||||
|
||||
if apiResp.Parameters != nil {
|
||||
parameters = *apiResp.Parameters
|
||||
}
|
||||
|
||||
return &apiResp, &Error{
|
||||
Code: apiResp.ErrorCode,
|
||||
Message: apiResp.Description,
|
||||
ResponseParameters: parameters,
|
||||
}
|
||||
}
|
||||
|
||||
return &apiResp, nil
|
||||
}
|
||||
|
||||
// decodeAPIResponse decode response and return slice of bytes if debug enabled.
|
||||
// If debug disabled, just decode http.Response.Body stream to APIResponse struct
|
||||
// for efficient memory usage
|
||||
func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) ([]byte, error) {
|
||||
if !bot.Debug {
|
||||
dec := json.NewDecoder(responseBody)
|
||||
err := dec.Decode(resp)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if debug, read response body
|
||||
data, err := ioutil.ReadAll(responseBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// UploadFiles makes a request to the API with files.
|
||||
func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFile) (*APIResponse, error) {
|
||||
r, w := io.Pipe()
|
||||
m := multipart.NewWriter(w)
|
||||
|
||||
// This code modified from the very helpful @HirbodBehnam
|
||||
// https://github.com/go-telegram-bot-api/telegram-bot-api/issues/354#issuecomment-663856473
|
||||
go func() {
|
||||
defer w.Close()
|
||||
defer m.Close()
|
||||
|
||||
for field, value := range params {
|
||||
if err := m.WriteField(field, value); err != nil {
|
||||
w.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.Data.NeedsUpload() {
|
||||
name, reader, err := file.Data.UploadData()
|
||||
if err != nil {
|
||||
w.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
part, err := m.CreateFormFile(file.Name, name)
|
||||
if err != nil {
|
||||
w.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, reader); err != nil {
|
||||
w.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if closer, ok := reader.(io.ReadCloser); ok {
|
||||
if err = closer.Close(); err != nil {
|
||||
w.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value := file.Data.SendData()
|
||||
|
||||
if err := m.WriteField(file.Name, value); err != nil {
|
||||
w.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if bot.Debug {
|
||||
log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files))
|
||||
}
|
||||
|
||||
method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
|
||||
|
||||
req, err := http.NewRequest("POST", method, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", m.FormDataContentType())
|
||||
|
||||
resp, err := bot.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var apiResp APIResponse
|
||||
bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
|
||||
if err != nil {
|
||||
return &apiResp, err
|
||||
}
|
||||
|
||||
if bot.Debug {
|
||||
log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
|
||||
}
|
||||
|
||||
if !apiResp.Ok {
|
||||
var parameters ResponseParameters
|
||||
|
||||
if apiResp.Parameters != nil {
|
||||
parameters = *apiResp.Parameters
|
||||
}
|
||||
|
||||
return &apiResp, &Error{
|
||||
Message: apiResp.Description,
|
||||
ResponseParameters: parameters,
|
||||
}
|
||||
}
|
||||
|
||||
return &apiResp, nil
|
||||
}
|
||||
|
||||
// GetFileDirectURL returns direct URL to file
|
||||
//
|
||||
// It requires the FileID.
|
||||
func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
|
||||
file, err := bot.GetFile(FileConfig{fileID})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return file.Link(bot.Token), nil
|
||||
}
|
||||
|
||||
// GetMe fetches the currently authenticated bot.
|
||||
//
|
||||
// This method is called upon creation to validate the token,
|
||||
// and so you may get this data from BotAPI.Self without the need for
|
||||
// another request.
|
||||
func (bot *BotAPI) GetMe() (User, error) {
|
||||
resp, err := bot.MakeRequest("getMe", nil)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
var user User
|
||||
err = json.Unmarshal(resp.Result, &user)
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
// IsMessageToMe returns true if message directed to this bot.
|
||||
//
|
||||
// It requires the Message.
|
||||
func (bot *BotAPI) IsMessageToMe(message Message) bool {
|
||||
return strings.Contains(message.Text, "@"+bot.Self.UserName)
|
||||
}
|
||||
|
||||
func hasFilesNeedingUpload(files []RequestFile) bool {
|
||||
for _, file := range files {
|
||||
if file.Data.NeedsUpload() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Request sends a Chattable to Telegram, and returns the APIResponse.
|
||||
func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) {
|
||||
params, err := c.params()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t, ok := c.(Fileable); ok {
|
||||
files := t.files()
|
||||
|
||||
// If we have files that need to be uploaded, we should delegate the
|
||||
// request to UploadFile.
|
||||
if hasFilesNeedingUpload(files) {
|
||||
return bot.UploadFiles(t.method(), params, files)
|
||||
}
|
||||
|
||||
// However, if there are no files to be uploaded, there's likely things
|
||||
// that need to be turned into params instead.
|
||||
for _, file := range files {
|
||||
params[file.Name] = file.Data.SendData()
|
||||
}
|
||||
}
|
||||
|
||||
return bot.MakeRequest(c.method(), params)
|
||||
}
|
||||
|
||||
// Send will send a Chattable item to Telegram and provides the
|
||||
// returned Message.
|
||||
func (bot *BotAPI) Send(c Chattable) (Message, error) {
|
||||
resp, err := bot.Request(c)
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
|
||||
var message Message
|
||||
err = json.Unmarshal(resp.Result, &message)
|
||||
|
||||
return message, err
|
||||
}
|
||||
|
||||
// SendMediaGroup sends a media group and returns the resulting messages.
|
||||
func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var messages []Message
|
||||
err = json.Unmarshal(resp.Result, &messages)
|
||||
|
||||
return messages, err
|
||||
}
|
||||
|
||||
// GetUserProfilePhotos gets a user's profile photos.
|
||||
//
|
||||
// It requires UserID.
|
||||
// Offset and Limit are optional.
|
||||
func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return UserProfilePhotos{}, err
|
||||
}
|
||||
|
||||
var profilePhotos UserProfilePhotos
|
||||
err = json.Unmarshal(resp.Result, &profilePhotos)
|
||||
|
||||
return profilePhotos, err
|
||||
}
|
||||
|
||||
// GetFile returns a File which can download a file from Telegram.
|
||||
//
|
||||
// Requires FileID.
|
||||
func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
|
||||
var file File
|
||||
err = json.Unmarshal(resp.Result, &file)
|
||||
|
||||
return file, err
|
||||
}
|
||||
|
||||
// GetUpdates fetches updates.
|
||||
// If a WebHook is set, this will not return any data!
|
||||
//
|
||||
// Offset, Limit, Timeout, and AllowedUpdates are optional.
|
||||
// To avoid stale items, set Offset to one higher than the previous item.
|
||||
// Set Timeout to a large number to reduce requests, so you can get updates
|
||||
// instantly instead of having to wait between requests.
|
||||
func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return []Update{}, err
|
||||
}
|
||||
|
||||
var updates []Update
|
||||
err = json.Unmarshal(resp.Result, &updates)
|
||||
|
||||
return updates, err
|
||||
}
|
||||
|
||||
// GetWebhookInfo allows you to fetch information about a webhook and if
|
||||
// one currently is set, along with pending update count and error messages.
|
||||
func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
|
||||
resp, err := bot.MakeRequest("getWebhookInfo", nil)
|
||||
if err != nil {
|
||||
return WebhookInfo{}, err
|
||||
}
|
||||
|
||||
var info WebhookInfo
|
||||
err = json.Unmarshal(resp.Result, &info)
|
||||
|
||||
return info, err
|
||||
}
|
||||
|
||||
// GetUpdatesChan starts and returns a channel for getting updates.
|
||||
func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
|
||||
ch := make(chan Update, bot.Buffer)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-bot.shutdownChannel:
|
||||
close(ch)
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
updates, err := bot.GetUpdates(config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
log.Println("Failed to get updates, retrying in 3 seconds...")
|
||||
time.Sleep(time.Second * 3)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, update := range updates {
|
||||
if update.UpdateID >= config.Offset {
|
||||
config.Offset = update.UpdateID + 1
|
||||
ch <- update
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// StopReceivingUpdates stops the go routine which receives updates
|
||||
func (bot *BotAPI) StopReceivingUpdates() {
|
||||
if bot.Debug {
|
||||
log.Println("Stopping the update receiver routine...")
|
||||
}
|
||||
close(bot.shutdownChannel)
|
||||
}
|
||||
|
||||
// ListenForWebhook registers a http handler for a webhook.
|
||||
func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
|
||||
ch := make(chan Update, bot.Buffer)
|
||||
|
||||
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
|
||||
update, err := bot.HandleUpdate(r)
|
||||
if err != nil {
|
||||
errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
ch <- *update
|
||||
})
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// ListenForWebhookRespReqFormat registers a http handler for a single incoming webhook.
|
||||
func (bot *BotAPI) ListenForWebhookRespReqFormat(w http.ResponseWriter, r *http.Request) UpdatesChannel {
|
||||
ch := make(chan Update, bot.Buffer)
|
||||
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
update, err := bot.HandleUpdate(r)
|
||||
if err != nil {
|
||||
errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
ch <- *update
|
||||
close(ch)
|
||||
}(w, r)
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// HandleUpdate parses and returns update received via webhook
|
||||
func (bot *BotAPI) HandleUpdate(r *http.Request) (*Update, error) {
|
||||
if r.Method != http.MethodPost {
|
||||
err := errors.New("wrong HTTP method required POST")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var update Update
|
||||
err := json.NewDecoder(r.Body).Decode(&update)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &update, nil
|
||||
}
|
||||
|
||||
// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
|
||||
//
|
||||
// It doesn't support uploading files.
|
||||
//
|
||||
// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
|
||||
// for details.
|
||||
func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
|
||||
params, err := c.params()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t, ok := c.(Fileable); ok {
|
||||
if hasFilesNeedingUpload(t.files()) {
|
||||
return errors.New("unable to use http response to upload files")
|
||||
}
|
||||
}
|
||||
|
||||
values := buildParams(params)
|
||||
values.Set("method", c.method())
|
||||
|
||||
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
_, err = w.Write([]byte(values.Encode()))
|
||||
return err
|
||||
}
|
||||
|
||||
// GetChat gets information about a chat.
|
||||
func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return Chat{}, err
|
||||
}
|
||||
|
||||
var chat Chat
|
||||
err = json.Unmarshal(resp.Result, &chat)
|
||||
|
||||
return chat, err
|
||||
}
|
||||
|
||||
// GetChatAdministrators gets a list of administrators in the chat.
|
||||
//
|
||||
// If none have been appointed, only the creator will be returned.
|
||||
// Bots are not shown, even if they are an administrator.
|
||||
func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return []ChatMember{}, err
|
||||
}
|
||||
|
||||
var members []ChatMember
|
||||
err = json.Unmarshal(resp.Result, &members)
|
||||
|
||||
return members, err
|
||||
}
|
||||
|
||||
// GetChatMembersCount gets the number of users in a chat.
|
||||
func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
var count int
|
||||
err = json.Unmarshal(resp.Result, &count)
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetChatMember gets a specific chat member.
|
||||
func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return ChatMember{}, err
|
||||
}
|
||||
|
||||
var member ChatMember
|
||||
err = json.Unmarshal(resp.Result, &member)
|
||||
|
||||
return member, err
|
||||
}
|
||||
|
||||
// GetGameHighScores allows you to get the high scores for a game.
|
||||
func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return []GameHighScore{}, err
|
||||
}
|
||||
|
||||
var highScores []GameHighScore
|
||||
err = json.Unmarshal(resp.Result, &highScores)
|
||||
|
||||
return highScores, err
|
||||
}
|
||||
|
||||
// GetInviteLink get InviteLink for a chat
|
||||
func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var inviteLink string
|
||||
err = json.Unmarshal(resp.Result, &inviteLink)
|
||||
|
||||
return inviteLink, err
|
||||
}
|
||||
|
||||
// GetStickerSet returns a StickerSet.
|
||||
func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return StickerSet{}, err
|
||||
}
|
||||
|
||||
var stickers StickerSet
|
||||
err = json.Unmarshal(resp.Result, &stickers)
|
||||
|
||||
return stickers, err
|
||||
}
|
||||
|
||||
// StopPoll stops a poll and returns the result.
|
||||
func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return Poll{}, err
|
||||
}
|
||||
|
||||
var poll Poll
|
||||
err = json.Unmarshal(resp.Result, &poll)
|
||||
|
||||
return poll, err
|
||||
}
|
||||
|
||||
// GetMyCommands gets the currently registered commands.
|
||||
func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
|
||||
return bot.GetMyCommandsWithConfig(GetMyCommandsConfig{})
|
||||
}
|
||||
|
||||
// GetMyCommandsWithConfig gets the currently registered commands with a config.
|
||||
func (bot *BotAPI) GetMyCommandsWithConfig(config GetMyCommandsConfig) ([]BotCommand, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var commands []BotCommand
|
||||
err = json.Unmarshal(resp.Result, &commands)
|
||||
|
||||
return commands, err
|
||||
}
|
||||
|
||||
// CopyMessage copy messages of any kind. The method is analogous to the method
|
||||
// forwardMessage, but the copied message doesn't have a link to the original
|
||||
// message. Returns the MessageID of the sent message on success.
|
||||
func (bot *BotAPI) CopyMessage(config CopyMessageConfig) (MessageID, error) {
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return MessageID{}, err
|
||||
}
|
||||
|
||||
var messageID MessageID
|
||||
err = json.Unmarshal(resp.Result, &messageID)
|
||||
|
||||
return messageID, err
|
||||
}
|
||||
|
||||
// AnswerWebAppQuery sets the result of an interaction with a Web App and send a
|
||||
// corresponding message on behalf of the user to the chat from which the query originated.
|
||||
func (bot *BotAPI) AnswerWebAppQuery(config AnswerWebAppQueryConfig) (SentWebAppMessage, error) {
|
||||
var sentWebAppMessage SentWebAppMessage
|
||||
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return sentWebAppMessage, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(resp.Result, &sentWebAppMessage)
|
||||
return sentWebAppMessage, err
|
||||
}
|
||||
|
||||
// GetMyDefaultAdministratorRights gets the current default administrator rights of the bot.
|
||||
func (bot *BotAPI) GetMyDefaultAdministratorRights(config GetMyDefaultAdministratorRightsConfig) (ChatAdministratorRights, error) {
|
||||
var rights ChatAdministratorRights
|
||||
|
||||
resp, err := bot.Request(config)
|
||||
if err != nil {
|
||||
return rights, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(resp.Result, &rights)
|
||||
return rights, err
|
||||
}
|
||||
|
||||
// EscapeText takes an input text and escape Telegram markup symbols.
|
||||
// In this way we can send a text without being afraid of having to escape the characters manually.
|
||||
// Note that you don't have to include the formatting style in the input text, or it will be escaped too.
|
||||
// If there is an error, an empty string will be returned.
|
||||
//
|
||||
// parseMode is the text formatting mode (ModeMarkdown, ModeMarkdownV2 or ModeHTML)
|
||||
// text is the input string that will be escaped
|
||||
func EscapeText(parseMode string, text string) string {
|
||||
var replacer *strings.Replacer
|
||||
|
||||
if parseMode == ModeHTML {
|
||||
replacer = strings.NewReplacer("<", "<", ">", ">", "&", "&")
|
||||
} else if parseMode == ModeMarkdown {
|
||||
replacer = strings.NewReplacer("_", "\\_", "*", "\\*", "`", "\\`", "[", "\\[")
|
||||
} else if parseMode == ModeMarkdownV2 {
|
||||
replacer = strings.NewReplacer(
|
||||
"_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(",
|
||||
"\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>",
|
||||
"#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|",
|
||||
"\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!",
|
||||
)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
return replacer.Replace(text)
|
||||
}
|
1051
bot_test.go
Normal file
1051
bot_test.go
Normal file
File diff suppressed because it is too large
Load Diff
2568
configs.go
Normal file
2568
configs.go
Normal file
File diff suppressed because it is too large
Load Diff
17
docs/SUMMARY.md
Normal file
17
docs/SUMMARY.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Summary
|
||||
|
||||
- [Getting Started](./getting-started/README.md)
|
||||
- [Library Structure](./getting-started/library-structure.md)
|
||||
- [Files](./getting-started/files.md)
|
||||
- [Important Notes](./getting-started/important-notes.md)
|
||||
- [Examples](./examples/README.md)
|
||||
- [Command Handling](./examples/command-handling.md)
|
||||
- [Keyboard](./examples/keyboard.md)
|
||||
- [Inline Keyboard](./examples/inline-keyboard.md)
|
||||
- [Change Log](./changelog.md)
|
||||
|
||||
# Contributing
|
||||
|
||||
- [Internals](./internals/README.md)
|
||||
- [Adding Endpoints](./internals/adding-endpoints.md)
|
||||
- [Uploading Files](./internals/uploading-files.md)
|
17
docs/changelog.md
Normal file
17
docs/changelog.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Change Log
|
||||
|
||||
## v5.4.0
|
||||
|
||||
- Remove all methods that return `(APIResponse, error)`.
|
||||
- Use the `Request` method instead.
|
||||
- For more information, see [Library Structure][library-structure].
|
||||
- Remove all `New*Upload` and `New*Share` methods, replace with `New*`.
|
||||
- Use different [file types][files] to specify if upload or share.
|
||||
- Rename `UploadFile` to `UploadFiles`, accept `[]RequestFile` instead of a
|
||||
single fieldname and file.
|
||||
- Fix methods returning `APIResponse` and errors to always use pointers.
|
||||
- Update user IDs to `int64` because of Bot API changes.
|
||||
- Add missing Bot API features.
|
||||
|
||||
[library-structure]: ./getting-started/library-structure.md#methods
|
||||
[files]: ./getting-started/files.md
|
4
docs/examples/README.md
Normal file
4
docs/examples/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Examples
|
||||
|
||||
With a better understanding of how the library works, let's look at some more
|
||||
examples showing off some of Telegram's features.
|
60
docs/examples/command-handling.md
Normal file
60
docs/examples/command-handling.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Command Handling
|
||||
|
||||
This is a simple example of changing behavior based on a provided command.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
bot.Debug = true
|
||||
|
||||
log.Printf("Authorized on account %s", bot.Self.UserName)
|
||||
|
||||
u := tgbotapi.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
|
||||
updates := bot.GetUpdatesChan(u)
|
||||
|
||||
for update := range updates {
|
||||
if update.Message == nil { // ignore any non-Message updates
|
||||
continue
|
||||
}
|
||||
|
||||
if !update.Message.IsCommand() { // ignore any non-command Messages
|
||||
continue
|
||||
}
|
||||
|
||||
// Create a new MessageConfig. We don't have text yet,
|
||||
// so we leave it empty.
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "")
|
||||
|
||||
// Extract the command from the Message.
|
||||
switch update.Message.Command() {
|
||||
case "help":
|
||||
msg.Text = "I understand /sayhi and /status."
|
||||
case "sayhi":
|
||||
msg.Text = "Hi :)"
|
||||
case "status":
|
||||
msg.Text = "I'm ok."
|
||||
default:
|
||||
msg.Text = "I don't know that command"
|
||||
}
|
||||
|
||||
if _, err := bot.Send(msg); err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
80
docs/examples/inline-keyboard.md
Normal file
80
docs/examples/inline-keyboard.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Inline Keyboard
|
||||
|
||||
This bot waits for you to send it the message "open" before sending you an
|
||||
inline keyboard containing a URL and some numbers. When a number is clicked, it
|
||||
sends you a message with your selected number.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonURL("1.com", "http://1.com"),
|
||||
tgbotapi.NewInlineKeyboardButtonData("2", "2"),
|
||||
tgbotapi.NewInlineKeyboardButtonData("3", "3"),
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("4", "4"),
|
||||
tgbotapi.NewInlineKeyboardButtonData("5", "5"),
|
||||
tgbotapi.NewInlineKeyboardButtonData("6", "6"),
|
||||
),
|
||||
)
|
||||
|
||||
func main() {
|
||||
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
bot.Debug = true
|
||||
|
||||
log.Printf("Authorized on account %s", bot.Self.UserName)
|
||||
|
||||
u := tgbotapi.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
|
||||
updates := bot.GetUpdatesChan(u)
|
||||
|
||||
// Loop through each update.
|
||||
for update := range updates {
|
||||
// Check if we've gotten a message update.
|
||||
if update.Message != nil {
|
||||
// Construct a new message from the given chat ID and containing
|
||||
// the text that we received.
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
|
||||
|
||||
// If the message was open, add a copy of our numeric keyboard.
|
||||
switch update.Message.Text {
|
||||
case "open":
|
||||
msg.ReplyMarkup = numericKeyboard
|
||||
|
||||
}
|
||||
|
||||
// Send the message.
|
||||
if _, err = bot.Send(msg); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else if update.CallbackQuery != nil {
|
||||
// Respond to the callback query, telling Telegram to show the user
|
||||
// a message with the data received.
|
||||
callback := tgbotapi.NewCallback(update.CallbackQuery.ID, update.CallbackQuery.Data)
|
||||
if _, err := bot.Request(callback); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// And finally, send a message containing the data received.
|
||||
msg := tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, update.CallbackQuery.Data)
|
||||
if _, err := bot.Send(msg); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
63
docs/examples/keyboard.md
Normal file
63
docs/examples/keyboard.md
Normal file
@ -0,0 +1,63 @@
|
||||
# Keyboard
|
||||
|
||||
This bot shows a numeric keyboard when you send a "open" message and hides it
|
||||
when you send "close" message.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
var numericKeyboard = tgbotapi.NewReplyKeyboard(
|
||||
tgbotapi.NewKeyboardButtonRow(
|
||||
tgbotapi.NewKeyboardButton("1"),
|
||||
tgbotapi.NewKeyboardButton("2"),
|
||||
tgbotapi.NewKeyboardButton("3"),
|
||||
),
|
||||
tgbotapi.NewKeyboardButtonRow(
|
||||
tgbotapi.NewKeyboardButton("4"),
|
||||
tgbotapi.NewKeyboardButton("5"),
|
||||
tgbotapi.NewKeyboardButton("6"),
|
||||
),
|
||||
)
|
||||
|
||||
func main() {
|
||||
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
bot.Debug = true
|
||||
|
||||
log.Printf("Authorized on account %s", bot.Self.UserName)
|
||||
|
||||
u := tgbotapi.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
|
||||
updates := bot.GetUpdatesChan(u)
|
||||
|
||||
for update := range updates {
|
||||
if update.Message == nil { // ignore non-Message updates
|
||||
continue
|
||||
}
|
||||
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
|
||||
|
||||
switch update.Message.Text {
|
||||
case "open":
|
||||
msg.ReplyMarkup = numericKeyboard
|
||||
case "close":
|
||||
msg.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true)
|
||||
}
|
||||
|
||||
if _, err := bot.Send(msg); err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
109
docs/getting-started/README.md
Normal file
109
docs/getting-started/README.md
Normal file
@ -0,0 +1,109 @@
|
||||
# Getting Started
|
||||
|
||||
This library is designed as a simple wrapper around the Telegram Bot API.
|
||||
It's encouraged to read [Telegram's docs][telegram-docs] first to get an
|
||||
understanding of what Bots are capable of doing. They also provide some good
|
||||
approaches to solve common problems.
|
||||
|
||||
[telegram-docs]: https://core.telegram.org/bots
|
||||
|
||||
## Installing
|
||||
|
||||
```bash
|
||||
go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5
|
||||
```
|
||||
|
||||
## A Simple Bot
|
||||
|
||||
To walk through the basics, let's create a simple echo bot that replies to your
|
||||
messages repeating what you said. Make sure you get an API token from
|
||||
[@Botfather][botfather] before continuing.
|
||||
|
||||
Let's start by constructing a new [BotAPI][bot-api-docs].
|
||||
|
||||
[botfather]: https://t.me/Botfather
|
||||
[bot-api-docs]: https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5?tab=doc#BotAPI
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bot.Debug = true
|
||||
}
|
||||
```
|
||||
|
||||
Instead of typing the API token directly into the file, we're using
|
||||
environment variables. This makes it easy to configure our Bot to use the right
|
||||
account and prevents us from leaking our real token into the world. Anyone with
|
||||
your token can send and receive messages from your Bot!
|
||||
|
||||
We've also set `bot.Debug = true` in order to get more information about the
|
||||
requests being sent to Telegram. If you run the example above, you'll see
|
||||
information about a request to the [`getMe`][get-me] endpoint. The library
|
||||
automatically calls this to ensure your token is working as expected. It also
|
||||
fills in the `Self` field in your `BotAPI` struct with information about the
|
||||
Bot.
|
||||
|
||||
Now that we've connected to Telegram, let's start getting updates and doing
|
||||
things. We can add this code in right after the line enabling debug mode.
|
||||
|
||||
[get-me]: https://core.telegram.org/bots/api#getme
|
||||
|
||||
```go
|
||||
// Create a new UpdateConfig struct with an offset of 0. Offsets are used
|
||||
// to make sure Telegram knows we've handled previous values and we don't
|
||||
// need them repeated.
|
||||
updateConfig := tgbotapi.NewUpdate(0)
|
||||
|
||||
// Tell Telegram we should wait up to 30 seconds on each request for an
|
||||
// update. This way we can get information just as quickly as making many
|
||||
// frequent requests without having to send nearly as many.
|
||||
updateConfig.Timeout = 30
|
||||
|
||||
// Start polling Telegram for updates.
|
||||
updates := bot.GetUpdatesChan(updateConfig)
|
||||
|
||||
// Let's go through each update that we're getting from Telegram.
|
||||
for update := range updates {
|
||||
// Telegram can send many types of updates depending on what your Bot
|
||||
// is up to. We only want to look at messages for now, so we can
|
||||
// discard any other updates.
|
||||
if update.Message == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Now that we know we've gotten a new message, we can construct a
|
||||
// reply! We'll take the Chat ID and Text from the incoming message
|
||||
// and use it to create a new message.
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
|
||||
// We'll also say that this message is a reply to the previous message.
|
||||
// For any other specifications than Chat ID or Text, you'll need to
|
||||
// set fields on the `MessageConfig`.
|
||||
msg.ReplyToMessageID = update.Message.MessageID
|
||||
|
||||
// Okay, we're sending our message off! We don't care about the message
|
||||
// we just sent, so we'll discard it.
|
||||
if _, err := bot.Send(msg); err != nil {
|
||||
// Note that panics are a bad way to handle errors. Telegram can
|
||||
// have service outages or network errors, you should retry sending
|
||||
// messages or more gracefully handle failures.
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Congradulations! You've made your very own bot!
|
||||
|
||||
Now that you've got some of the basics down, we can start talking about how the
|
||||
library is structured and more advanced features.
|
68
docs/getting-started/files.md
Normal file
68
docs/getting-started/files.md
Normal file
@ -0,0 +1,68 @@
|
||||
# Files
|
||||
|
||||
Telegram supports specifying files in many different formats. In order to
|
||||
accommodate them all, there are multiple structs and type aliases required.
|
||||
|
||||
All of these types implement the `RequestFileData` interface.
|
||||
|
||||
| Type | Description |
|
||||
| ------------ | ------------------------------------------------------------------------- |
|
||||
| `FilePath` | A local path to a file |
|
||||
| `FileID` | Existing file ID on Telegram's servers |
|
||||
| `FileURL` | URL to file, must be served with expected MIME type |
|
||||
| `FileReader` | Use an `io.Reader` to provide a file. Lazily read to save memory. |
|
||||
| `FileBytes` | `[]byte` containing file data. Prefer to use `FileReader` to save memory. |
|
||||
|
||||
## `FilePath`
|
||||
|
||||
A path to a local file.
|
||||
|
||||
```go
|
||||
file := tgbotapi.FilePath("tests/image.jpg")
|
||||
```
|
||||
|
||||
## `FileID`
|
||||
|
||||
An ID previously uploaded to Telegram. IDs may only be reused by the same bot
|
||||
that received them. Additionally, thumbnail IDs cannot be reused.
|
||||
|
||||
```go
|
||||
file := tgbotapi.FileID("AgACAgIAAxkDAALesF8dCjAAAa_…")
|
||||
```
|
||||
|
||||
## `FileURL`
|
||||
|
||||
A URL to an existing resource. It must be served with a correct MIME type to
|
||||
work as expected.
|
||||
|
||||
```go
|
||||
file := tgbotapi.FileURL("https://i.imgur.com/unQLJIb.jpg")
|
||||
```
|
||||
|
||||
## `FileReader`
|
||||
|
||||
Use an `io.Reader` to provide file contents as needed. Requires a filename for
|
||||
the virtual file.
|
||||
|
||||
```go
|
||||
var reader io.Reader
|
||||
|
||||
file := tgbotapi.FileReader{
|
||||
Name: "image.jpg",
|
||||
Reader: reader,
|
||||
}
|
||||
```
|
||||
|
||||
## `FileBytes`
|
||||
|
||||
Use a `[]byte` to provide file contents. Generally try to avoid this as it
|
||||
results in high memory usage. Also requires a filename for the virtual file.
|
||||
|
||||
```go
|
||||
var data []byte
|
||||
|
||||
file := tgbotapi.FileBytes{
|
||||
Name: "image.jpg",
|
||||
Bytes: data,
|
||||
}
|
||||
```
|
56
docs/getting-started/important-notes.md
Normal file
56
docs/getting-started/important-notes.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Important Notes
|
||||
|
||||
The Telegram Bot API has a few potentially unanticipated behaviors. Here are a
|
||||
few of them. If any behavior was surprising to you, please feel free to open a
|
||||
pull request!
|
||||
|
||||
## Callback Queries
|
||||
|
||||
- Every callback query must be answered, even if there is nothing to display to
|
||||
the user. Failure to do so will show a loading icon on the keyboard until the
|
||||
operation times out.
|
||||
|
||||
## ChatMemberUpdated
|
||||
|
||||
- In order to receive `ChatMember` updates, you must explicitly add
|
||||
`UpdateTypeChatMember` to your `AllowedUpdates` when getting updates or
|
||||
setting your webhook.
|
||||
|
||||
## Entities use UTF16
|
||||
|
||||
- When extracting text entities using offsets and lengths, characters can appear
|
||||
to be in incorrect positions. This is because Telegram uses UTF16 lengths
|
||||
while Golang uses UTF8. It's possible to convert between the two, see
|
||||
[issue #231][issue-231] for more details.
|
||||
|
||||
[issue-231]: https://github.com/go-telegram-bot-api/telegram-bot-api/issues/231
|
||||
|
||||
## GetUpdatesChan
|
||||
|
||||
- This method is very basic and likely unsuitable for production use. Consider
|
||||
creating your own implementation instead, as it's very simple to replicate.
|
||||
- This method only allows your bot to process one update at a time. You can
|
||||
spawn goroutines to handle updates concurrently or switch to webhooks instead.
|
||||
Webhooks are suggested for high traffic bots.
|
||||
|
||||
## Nil Updates
|
||||
|
||||
- At most one of the fields in an `Update` will be set to a non-nil value. When
|
||||
evaluating updates, you must make sure you check that the field is not nil
|
||||
before trying to access any of it's fields.
|
||||
|
||||
## Privacy Mode
|
||||
|
||||
- By default, bots only get updates directly addressed to them. If you need to
|
||||
get all messages, you must disable privacy mode with Botfather. Bots already
|
||||
added to groups will need to be removed and readded for the changes to take
|
||||
effect. You can read more on the [Telegram Bot API docs][api-docs].
|
||||
|
||||
[api-docs]: https://core.telegram.org/bots/faq#what-messages-will-my-bot-get
|
||||
|
||||
## User and Chat ID size
|
||||
|
||||
- These types require up to 52 significant bits to store correctly, making a
|
||||
64-bit integer type required in most languages. They are already `int64` types
|
||||
in this library, but make sure you use correct types when saving them to a
|
||||
database or passing them to another language.
|
37
docs/getting-started/library-structure.md
Normal file
37
docs/getting-started/library-structure.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Library Structure
|
||||
|
||||
This library is generally broken into three components you need to understand.
|
||||
|
||||
## Configs
|
||||
|
||||
Configs are collections of fields related to a single request. For example, if
|
||||
one wanted to use the `sendMessage` endpoint, you could use the `MessageConfig`
|
||||
struct to configure the request. There is a one-to-one relationship between
|
||||
Telegram endpoints and configs. They generally have the naming pattern of
|
||||
removing the `send` prefix and they all end with the `Config` suffix. They
|
||||
generally implement the `Chattable` interface. If they can send files, they
|
||||
implement the `Fileable` interface.
|
||||
|
||||
## Helpers
|
||||
|
||||
Helpers are easier ways of constructing common Configs. Instead of having to
|
||||
create a `MessageConfig` struct and remember to set the `ChatID` and `Text`,
|
||||
you can use the `NewMessage` helper method. It takes the two required parameters
|
||||
for the request to succeed. You can then set fields on the resulting
|
||||
`MessageConfig` after it's creation. They are generally named the same as
|
||||
method names except with `send` replaced with `New`.
|
||||
|
||||
## Methods
|
||||
|
||||
Methods are used to send Configs after they are constructed. Generally,
|
||||
`Request` is the lowest level method you'll have to call. It accepts a
|
||||
`Chattable` parameter and knows how to upload files if needed. It returns an
|
||||
`APIResponse`, the most general return type from the Bot API. This method is
|
||||
called for any endpoint that doesn't have a more specific return type. For
|
||||
example, `setWebhook` only returns `true` or an error. Other methods may have
|
||||
more specific return types. The `getFile` endpoint returns a `File`. Almost
|
||||
every other method returns a `Message`, which you can use `Send` to obtain.
|
||||
|
||||
There's lower level methods such as `MakeRequest` which require an endpoint and
|
||||
parameters instead of accepting configs. These are primarily used internally.
|
||||
If you find yourself having to use them, please open an issue.
|
4
docs/internals/README.md
Normal file
4
docs/internals/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Internals
|
||||
|
||||
If you want to contribute to the project, here's some more information about
|
||||
the internal structure of the library.
|
197
docs/internals/adding-endpoints.md
Normal file
197
docs/internals/adding-endpoints.md
Normal file
@ -0,0 +1,197 @@
|
||||
# Adding Endpoints
|
||||
|
||||
This is mostly useful if you've managed to catch a new Telegram Bot API update
|
||||
before the library can get updated. It's also a great source of information
|
||||
about how the types work internally.
|
||||
|
||||
## Creating the Config
|
||||
|
||||
The first step in adding a new endpoint is to create a new Config type for it.
|
||||
These belong in `configs.go`.
|
||||
|
||||
Let's try and add the `deleteMessage` endpoint. We can see it requires two
|
||||
fields; `chat_id` and `message_id`. We can create a struct for these.
|
||||
|
||||
```go
|
||||
type DeleteMessageConfig struct {
|
||||
ChatID ???
|
||||
MessageID int
|
||||
}
|
||||
```
|
||||
|
||||
What type should `ChatID` be? Telegram allows specifying numeric chat IDs or
|
||||
channel usernames. Golang doesn't have union types, and interfaces are entirely
|
||||
untyped. This library solves this by adding two fields, a `ChatID` and a
|
||||
`ChannelUsername`. We can now write the struct as follows.
|
||||
|
||||
```go
|
||||
type DeleteMessageConfig struct {
|
||||
ChannelUsername string
|
||||
ChatID int64
|
||||
MessageID int
|
||||
}
|
||||
```
|
||||
|
||||
Note that `ChatID` is an `int64`. Telegram chat IDs can be greater than 32 bits.
|
||||
|
||||
Okay, we now have our struct. But we can't send it yet. It doesn't implement
|
||||
`Chattable` so it won't work with `Request` or `Send`.
|
||||
|
||||
### Making it `Chattable`
|
||||
|
||||
We can see that `Chattable` only requires a few methods.
|
||||
|
||||
```go
|
||||
type Chattable interface {
|
||||
params() (Params, error)
|
||||
method() string
|
||||
}
|
||||
```
|
||||
|
||||
`params` is the fields associated with the request. `method` is the endpoint
|
||||
that this Config is associated with.
|
||||
|
||||
Implementing the `method` is easy, so let's start with that.
|
||||
|
||||
```go
|
||||
func (config DeleteMessageConfig) method() string {
|
||||
return "deleteMessage"
|
||||
}
|
||||
```
|
||||
|
||||
Now we have to add the `params`. The `Params` type is an alias for
|
||||
`map[string]string`. Telegram expects only a single field for `chat_id`, so we
|
||||
have to determine what data to send.
|
||||
|
||||
We could use an if statement to determine which field to get the value from.
|
||||
However, as this is a relatively common operation, there's helper methods for
|
||||
`Params`. We can use the `AddFirstValid` method to go through each possible
|
||||
value and stop when it discovers a valid one. Before writing your own Config,
|
||||
it's worth taking a look through `params.go` to see what other helpers exist.
|
||||
|
||||
Now we can take a look at what a completed `params` method looks like.
|
||||
|
||||
```go
|
||||
func (config DeleteMessageConfig) params() (Params, error) {
|
||||
params := make(Params)
|
||||
|
||||
params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername)
|
||||
params.AddNonZero("message_id", config.MessageID)
|
||||
|
||||
return params, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Uploading Files
|
||||
|
||||
Let's imagine that for some reason deleting a message requires a document to be
|
||||
uploaded and an optional thumbnail for that document. To add file upload
|
||||
support we need to implement `Fileable`. This only requires one additional
|
||||
method.
|
||||
|
||||
```go
|
||||
type Fileable interface {
|
||||
Chattable
|
||||
files() []RequestFile
|
||||
}
|
||||
```
|
||||
|
||||
First, let's add some fields to store our files in. Most of the standard Configs
|
||||
have similar fields for their files.
|
||||
|
||||
```diff
|
||||
type DeleteMessageConfig struct {
|
||||
ChannelUsername string
|
||||
ChatID int64
|
||||
MessageID int
|
||||
+ Delete RequestFileData
|
||||
+ Thumb RequestFileData
|
||||
}
|
||||
```
|
||||
|
||||
Adding another method is pretty simple. We'll always add a file named `delete`
|
||||
and add the `thumb` file if we have one.
|
||||
|
||||
```go
|
||||
func (config DeleteMessageConfig) files() []RequestFile {
|
||||
files := []RequestFile{{
|
||||
Name: "delete",
|
||||
Data: config.Delete,
|
||||
}}
|
||||
|
||||
if config.Thumb != nil {
|
||||
files = append(files, RequestFile{
|
||||
Name: "thumb",
|
||||
Data: config.Thumb,
|
||||
})
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
```
|
||||
|
||||
And now our files will upload! It will transparently handle uploads whether File
|
||||
is a `FilePath`, `FileURL`, `FileBytes`, `FileReader`, or `FileID`.
|
||||
|
||||
### Base Configs
|
||||
|
||||
Certain Configs have repeated elements. For example, many of the items sent to a
|
||||
chat have `ChatID` or `ChannelUsername` fields, along with `ReplyToMessageID`,
|
||||
`ReplyMarkup`, and `DisableNotification`. Instead of implementing all of this
|
||||
code for each item, there's a `BaseChat` that handles it for your Config.
|
||||
Simply embed it in your struct to get all of those fields.
|
||||
|
||||
There's only a few fields required for the `MessageConfig` struct after
|
||||
embedding the `BaseChat` struct.
|
||||
|
||||
```go
|
||||
type MessageConfig struct {
|
||||
BaseChat
|
||||
Text string
|
||||
ParseMode string
|
||||
DisableWebPagePreview bool
|
||||
}
|
||||
```
|
||||
|
||||
It also inherits the `params` method from `BaseChat`. This allows you to call
|
||||
it, then you only have to add your new fields.
|
||||
|
||||
```go
|
||||
func (config MessageConfig) params() (Params, error) {
|
||||
params, err := config.BaseChat.params()
|
||||
if err != nil {
|
||||
return params, err
|
||||
}
|
||||
|
||||
params.AddNonEmpty("text", config.Text)
|
||||
// Add your other fields
|
||||
|
||||
return params, nil
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, there's a `BaseFile` struct for adding an associated file and
|
||||
`BaseEdit` struct for editing messages.
|
||||
|
||||
## Making it Friendly
|
||||
|
||||
After we've got a Config type, we'll want to make it more user-friendly. We can
|
||||
do this by adding a new helper to `helpers.go`. These are functions that take
|
||||
in the required data for the request to succeed and populate a Config.
|
||||
|
||||
Telegram only requires two fields to call `deleteMessage`, so this will be fast.
|
||||
|
||||
```go
|
||||
func NewDeleteMessage(chatID int64, messageID int) DeleteMessageConfig {
|
||||
return DeleteMessageConfig{
|
||||
ChatID: chatID,
|
||||
MessageID: messageID,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Sometimes it makes sense to add more helpers if there's methods where you have
|
||||
to set exactly one field. You can also add helpers that accept a `username`
|
||||
string for channels if it's a common operation.
|
||||
|
||||
And that's it! You've added a new method.
|
87
docs/internals/uploading-files.md
Normal file
87
docs/internals/uploading-files.md
Normal file
@ -0,0 +1,87 @@
|
||||
# Uploading Files
|
||||
|
||||
To make files work as expected, there's a lot going on behind the scenes. Make
|
||||
sure to read through the [Files](../getting-started/files.md) section in
|
||||
Getting Started first as we'll be building on that information.
|
||||
|
||||
This section only talks about file uploading. For non-uploaded files such as
|
||||
URLs and file IDs, you just need to pass a string.
|
||||
|
||||
## Fields
|
||||
|
||||
Let's start by talking about how the library represents files as part of a
|
||||
Config.
|
||||
|
||||
### Static Fields
|
||||
|
||||
Most endpoints use static file fields. For example, `sendPhoto` expects a single
|
||||
file named `photo`. All we have to do is set that single field with the correct
|
||||
value (either a string or multipart file). Methods like `sendDocument` take two
|
||||
file uploads, a `document` and a `thumb`. These are pretty straightforward.
|
||||
|
||||
Remembering that the `Fileable` interface only requires one method, let's
|
||||
implement it for `DocumentConfig`.
|
||||
|
||||
```go
|
||||
func (config DocumentConfig) files() []RequestFile {
|
||||
// We can have multiple files, so we'll create an array. We also know that
|
||||
// there always is a document file, so initialize the array with that.
|
||||
files := []RequestFile{{
|
||||
Name: "document",
|
||||
Data: config.File,
|
||||
}}
|
||||
|
||||
// We'll only add a file if we have one.
|
||||
if config.Thumb != nil {
|
||||
files = append(files, RequestFile{
|
||||
Name: "thumb",
|
||||
Data: config.Thumb,
|
||||
})
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
```
|
||||
|
||||
Telegram also supports the `attach://` syntax (discussed more later) for
|
||||
thumbnails, but there's no reason to make things more complicated.
|
||||
|
||||
### Dynamic Fields
|
||||
|
||||
Of course, not everything can be so simple. Methods like `sendMediaGroup`
|
||||
can accept many files, and each file can have custom markup. Using a static
|
||||
field isn't possible because we need to specify which field is attached to each
|
||||
item. Telegram introduced the `attach://` syntax for this.
|
||||
|
||||
Let's follow through creating a new media group with string and file uploads.
|
||||
|
||||
First, we start by creating some `InputMediaPhoto`.
|
||||
|
||||
```go
|
||||
photo := tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("tests/image.jpg"))
|
||||
url := tgbotapi.NewInputMediaPhoto(tgbotapi.FileURL("https://i.imgur.com/unQLJIb.jpg"))
|
||||
```
|
||||
|
||||
This created a new `InputMediaPhoto` struct, with a type of `photo` and the
|
||||
media interface that we specified.
|
||||
|
||||
We'll now create our media group with the photo and URL.
|
||||
|
||||
```go
|
||||
mediaGroup := NewMediaGroup(ChatID, []interface{}{
|
||||
photo,
|
||||
url,
|
||||
})
|
||||
```
|
||||
|
||||
A `MediaGroupConfig` stores all of the media in an array of interfaces. We now
|
||||
have all of the data we need to upload, but how do we figure out field names for
|
||||
uploads? We didn't specify `attach://unique-file` anywhere.
|
||||
|
||||
When the library goes to upload the files, it looks at the `params` and `files`
|
||||
for the Config. The params are generated by transforming the file into a value
|
||||
more suitable for uploading, file IDs and URLs are untouched but uploaded types
|
||||
are all changed into `attach://file-%d`. When collecting a list of files to
|
||||
upload, it names them the same way. This creates a nearly transparent way of
|
||||
handling multiple files in the background without the user having to consider
|
||||
what's going on.
|
927
helpers.go
Normal file
927
helpers.go
Normal file
@ -0,0 +1,927 @@
|
||||
package tgbotapi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// NewMessage creates a new Message.
|
||||
//
|
||||
// chatID is where to send it, text is the message text.
|
||||
func NewMessage(chatID int64, text string) MessageConfig {
|
||||
return MessageConfig{
|
||||
BaseChat: BaseChat{
|
||||
ChatID: chatID,
|
||||
ReplyToMessageID: 0,
|
||||
},
|
||||
Text: text,
|
||||
DisableWebPagePreview: false,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDeleteMessage creates a request to delete a message.
|
||||
func NewDeleteMessage(chatID int64, messageID int) DeleteMessageConfig {
|
||||
return DeleteMessageConfig{
|
||||
ChatID: chatID,
|
||||
MessageID: messageID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMessageToChannel creates a new Message that is sent to a channel
|
||||
// by username.
|
||||
//
|
||||
// username is the username of the channel, text is the message text,
|
||||
// and the username should be in the form of `@username`.
|
||||
func NewMessageToChannel(username string, text string) MessageConfig {
|
||||
return MessageConfig{
|
||||
BaseChat: BaseChat{
|
||||
ChannelUsername: username,
|
||||
},
|
||||
Text: text,
|
||||
}
|
||||
}
|
||||
|
||||
// NewForward creates a new forward.
|
||||
//
|
||||
// chatID is where to send it, fromChatID is the source chat,
|
||||
// and messageID is the ID of the original message.
|
||||
func NewForward(chatID int64, fromChatID int64, messageID int) ForwardConfig {
|
||||
return ForwardConfig{
|
||||
BaseChat: BaseChat{ChatID: chatID},
|
||||
FromChatID: fromChatID,
|
||||
MessageID: messageID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCopyMessage creates a new copy message.
|
||||
//
|
||||
// chatID is where to send it, fromChatID is the source chat,
|
||||
// and messageID is the ID of the original message.
|
||||
func NewCopyMessage(chatID int64, fromChatID int64, messageID int) CopyMessageConfig {
|
||||
return CopyMessageConfig{
|
||||
BaseChat: BaseChat{ChatID: chatID},
|
||||
FromChatID: fromChatID,
|
||||
MessageID: messageID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPhoto creates a new sendPhoto request.
|
||||
//
|
||||
// chatID is where to send it, file is a string path to the file,
|
||||
// FileReader, or FileBytes.
|
||||
//
|
||||
// Note that you must send animated GIFs as a document.
|
||||
func NewPhoto(chatID int64, file RequestFileData) PhotoConfig {
|
||||
return PhotoConfig{
|
||||
BaseFile: BaseFile{
|
||||
BaseChat: BaseChat{ChatID: chatID},
|
||||
File: file,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewPhotoToChannel creates a new photo uploader to send a photo to a channel.
|
||||
//
|
||||
// Note that you must send animated GIFs as a document.
|
||||
func NewPhotoToChannel(username string, file RequestFileData) PhotoConfig {
|
||||
return PhotoConfig{
|
||||
BaseFile: BaseFile{
|
||||
BaseChat: BaseChat{
|
||||
ChannelUsername: username,
|
||||
},
|
||||
File: file,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewAudio creates a new sendAudio request.
|
||||
func NewAudio(chatID int64, file RequestFileData) AudioConfig {
|
||||
return AudioConfig{
|
||||
BaseFile: BaseFile{
|
||||
BaseChat: BaseChat{ChatID: chatID},
|
||||
File: file,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewDocument creates a new sendDocument request.
|
||||
func NewDocument(chatID int64, file RequestFileData) DocumentConfig {
|
||||
return DocumentConfig{
|
||||
BaseFile: BaseFile{
|
||||
BaseChat: BaseChat{ChatID: chatID},
|
||||
File: file,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewSticker creates a new sendSticker request.
|
||||
func NewSticker(chatID int64, file RequestFileData) StickerConfig {
|
||||
return StickerConfig{
|
||||
BaseFile: BaseFile{
|
||||
BaseChat: BaseChat{ChatID: chatID},
|
||||
File: file,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewVideo creates a new sendVideo request.
|
||||
func NewVideo(chatID int64, file RequestFileData) VideoConfig {
|
||||
return VideoConfig{
|
||||
BaseFile: BaseFile{
|
||||
BaseChat: BaseChat{ChatID: chatID},
|
||||
File: file,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewAnimation creates a new sendAnimation request.
|
||||
func NewAnimation(chatID int64, file RequestFileData) AnimationConfig {
|
||||
return AnimationConfig{
|
||||
BaseFile: BaseFile{
|
||||
BaseChat: BaseChat{ChatID: chatID},
|
||||
File: file,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewVideoNote creates a new sendVideoNote request.
|
||||
//
|
||||
// chatID is where to send it, file is a string path to the file,
|
||||
// FileReader, or FileBytes.
|
||||
func NewVideoNote(chatID int64, length int, file RequestFileData) VideoNoteConfig {
|
||||
return VideoNoteConfig{
|
||||
BaseFile: BaseFile{
|
||||
BaseChat: BaseChat{ChatID: chatID},
|
||||
File: file,
|
||||
},
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
|
||||
// NewVoice creates a new sendVoice request.
|
||||
func NewVoice(chatID int64, file RequestFileData) VoiceConfig {
|
||||
return VoiceConfig{
|
||||
BaseFile: BaseFile{
|
||||
BaseChat: BaseChat{ChatID: chatID},
|
||||
File: file,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewMediaGroup creates a new media group. Files should be an array of
|
||||
// two to ten InputMediaPhoto or InputMediaVideo.
|
||||
func NewMediaGroup(chatID int64, files []interface{}) MediaGroupConfig {
|
||||
return MediaGroupConfig{
|
||||
ChatID: chatID,
|
||||
Media: files,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInputMediaPhoto creates a new InputMediaPhoto.
|
||||
func NewInputMediaPhoto(media RequestFileData) InputMediaPhoto {
|
||||
return InputMediaPhoto{
|
||||
BaseInputMedia{
|
||||
Type: "photo",
|
||||
Media: media,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewInputMediaVideo creates a new InputMediaVideo.
|
||||
func NewInputMediaVideo(media RequestFileData) InputMediaVideo {
|
||||
return InputMediaVideo{
|
||||
BaseInputMedia: BaseInputMedia{
|
||||
Type: "video",
|
||||
Media: media,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewInputMediaAnimation creates a new InputMediaAnimation.
|
||||
func NewInputMediaAnimation(media RequestFileData) InputMediaAnimation {
|
||||
return InputMediaAnimation{
|
||||
BaseInputMedia: BaseInputMedia{
|
||||
Type: "animation",
|
||||
Media: media,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewInputMediaAudio creates a new InputMediaAudio.
|
||||
func NewInputMediaAudio(media RequestFileData) InputMediaAudio {
|
||||
return InputMediaAudio{
|
||||
BaseInputMedia: BaseInputMedia{
|
||||
Type: "audio",
|
||||
Media: media,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewInputMediaDocument creates a new InputMediaDocument.
|
||||
func NewInputMediaDocument(media RequestFileData) InputMediaDocument {
|
||||
return InputMediaDocument{
|
||||
BaseInputMedia: BaseInputMedia{
|
||||
Type: "document",
|
||||
Media: media,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewContact allows you to send a shared contact.
|
||||
func NewContact(chatID int64, phoneNumber, firstName string) ContactConfig {
|
||||
return ContactConfig{
|
||||
BaseChat: BaseChat{
|
||||
ChatID: chatID,
|
||||
},
|
||||
PhoneNumber: phoneNumber,
|
||||
FirstName: firstName,
|
||||
}
|
||||
}
|
||||
|
||||
// NewLocation shares your location.
|
||||
//
|
||||
// chatID is where to send it, latitude and longitude are coordinates.
|
||||
func NewLocation(chatID int64, latitude float64, longitude float64) LocationConfig {
|
||||
return LocationConfig{
|
||||
BaseChat: BaseChat{
|
||||
ChatID: chatID,
|
||||
},
|
||||
Latitude: latitude,
|
||||
Longitude: longitude,
|
||||
}
|
||||
}
|
||||
|
||||
// NewVenue allows you to send a venue and its location.
|
||||
func NewVenue(chatID int64, title, address string, latitude, longitude float64) VenueConfig {
|
||||
return VenueConfig{
|
||||
BaseChat: BaseChat{
|
||||
ChatID: chatID,
|
||||
},
|
||||
Title: title,
|
||||
Address: address,
|
||||
Latitude: latitude,
|
||||
Longitude: longitude,
|
||||
}
|
||||
}
|
||||
|
||||
// NewChatAction sets a chat action.
|
||||
// Actions last for 5 seconds, or until your next action.
|
||||
//
|
||||
// chatID is where to send it, action should be set via Chat constants.
|
||||
func NewChatAction(chatID int64, action string) ChatActionConfig {
|
||||
return ChatActionConfig{
|
||||
BaseChat: BaseChat{ChatID: chatID},
|
||||
Action: action,
|
||||
}
|
||||
}
|
||||
|
||||
// NewUserProfilePhotos gets user profile photos.
|
||||
//
|
||||
// userID is the ID of the user you wish to get profile photos from.
|
||||
func NewUserProfilePhotos(userID int64) UserProfilePhotosConfig {
|
||||
return UserProfilePhotosConfig{
|
||||
UserID: userID,
|
||||
Offset: 0,
|
||||
Limit: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// NewUpdate gets updates since the last Offset.
|
||||
//
|
||||
// offset is the last Update ID to include.
|
||||
// You likely want to set this to the last Update ID plus 1.
|
||||
func NewUpdate(offset int) UpdateConfig {
|
||||
return UpdateConfig{
|
||||
Offset: offset,
|
||||
Limit: 0,
|
||||
Timeout: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// NewWebhook creates a new webhook.
|
||||
//
|
||||
// link is the url parsable link you wish to get the updates.
|
||||
func NewWebhook(link string) (WebhookConfig, error) {
|
||||
u, err := url.Parse(link)
|
||||
|
||||
if err != nil {
|
||||
return WebhookConfig{}, err
|
||||
}
|
||||
|
||||
return WebhookConfig{
|
||||
URL: u,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewWebhookWithCert creates a new webhook with a certificate.
|
||||
//
|
||||
// link is the url you wish to get webhooks,
|
||||
// file contains a string to a file, FileReader, or FileBytes.
|
||||
func NewWebhookWithCert(link string, file RequestFileData) (WebhookConfig, error) {
|
||||
u, err := url.Parse(link)
|
||||
|
||||
if err != nil {
|
||||
return WebhookConfig{}, err
|
||||
}
|
||||
|
||||
return WebhookConfig{
|
||||
URL: u,
|
||||
Certificate: file,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewInlineQueryResultArticle creates a new inline query article.
|
||||
func NewInlineQueryResultArticle(id, title, messageText string) InlineQueryResultArticle {
|
||||
return InlineQueryResultArticle{
|
||||
Type: "article",
|
||||
ID: id,
|
||||
Title: title,
|
||||
InputMessageContent: InputTextMessageContent{
|
||||
Text: messageText,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultArticleMarkdown creates a new inline query article with Markdown parsing.
|
||||
func NewInlineQueryResultArticleMarkdown(id, title, messageText string) InlineQueryResultArticle {
|
||||
return InlineQueryResultArticle{
|
||||
Type: "article",
|
||||
ID: id,
|
||||
Title: title,
|
||||
InputMessageContent: InputTextMessageContent{
|
||||
Text: messageText,
|
||||
ParseMode: "Markdown",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultArticleMarkdownV2 creates a new inline query article with MarkdownV2 parsing.
|
||||
func NewInlineQueryResultArticleMarkdownV2(id, title, messageText string) InlineQueryResultArticle {
|
||||
return InlineQueryResultArticle{
|
||||
Type: "article",
|
||||
ID: id,
|
||||
Title: title,
|
||||
InputMessageContent: InputTextMessageContent{
|
||||
Text: messageText,
|
||||
ParseMode: "MarkdownV2",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultArticleHTML creates a new inline query article with HTML parsing.
|
||||
func NewInlineQueryResultArticleHTML(id, title, messageText string) InlineQueryResultArticle {
|
||||
return InlineQueryResultArticle{
|
||||
Type: "article",
|
||||
ID: id,
|
||||
Title: title,
|
||||
InputMessageContent: InputTextMessageContent{
|
||||
Text: messageText,
|
||||
ParseMode: "HTML",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultGIF creates a new inline query GIF.
|
||||
func NewInlineQueryResultGIF(id, url string) InlineQueryResultGIF {
|
||||
return InlineQueryResultGIF{
|
||||
Type: "gif",
|
||||
ID: id,
|
||||
URL: url,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultCachedGIF create a new inline query with cached photo.
|
||||
func NewInlineQueryResultCachedGIF(id, gifID string) InlineQueryResultCachedGIF {
|
||||
return InlineQueryResultCachedGIF{
|
||||
Type: "gif",
|
||||
ID: id,
|
||||
GIFID: gifID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultMPEG4GIF creates a new inline query MPEG4 GIF.
|
||||
func NewInlineQueryResultMPEG4GIF(id, url string) InlineQueryResultMPEG4GIF {
|
||||
return InlineQueryResultMPEG4GIF{
|
||||
Type: "mpeg4_gif",
|
||||
ID: id,
|
||||
URL: url,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultCachedMPEG4GIF create a new inline query with cached MPEG4 GIF.
|
||||
func NewInlineQueryResultCachedMPEG4GIF(id, MPEG4GIFID string) InlineQueryResultCachedMPEG4GIF {
|
||||
return InlineQueryResultCachedMPEG4GIF{
|
||||
Type: "mpeg4_gif",
|
||||
ID: id,
|
||||
MPEG4FileID: MPEG4GIFID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultPhoto creates a new inline query photo.
|
||||
func NewInlineQueryResultPhoto(id, url string) InlineQueryResultPhoto {
|
||||
return InlineQueryResultPhoto{
|
||||
Type: "photo",
|
||||
ID: id,
|
||||
URL: url,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultPhotoWithThumb creates a new inline query photo.
|
||||
func NewInlineQueryResultPhotoWithThumb(id, url, thumb string) InlineQueryResultPhoto {
|
||||
return InlineQueryResultPhoto{
|
||||
Type: "photo",
|
||||
ID: id,
|
||||
URL: url,
|
||||
ThumbURL: thumb,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultCachedPhoto create a new inline query with cached photo.
|
||||
func NewInlineQueryResultCachedPhoto(id, photoID string) InlineQueryResultCachedPhoto {
|
||||
return InlineQueryResultCachedPhoto{
|
||||
Type: "photo",
|
||||
ID: id,
|
||||
PhotoID: photoID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultVideo creates a new inline query video.
|
||||
func NewInlineQueryResultVideo(id, url string) InlineQueryResultVideo {
|
||||
return InlineQueryResultVideo{
|
||||
Type: "video",
|
||||
ID: id,
|
||||
URL: url,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultCachedVideo create a new inline query with cached video.
|
||||
func NewInlineQueryResultCachedVideo(id, videoID, title string) InlineQueryResultCachedVideo {
|
||||
return InlineQueryResultCachedVideo{
|
||||
Type: "video",
|
||||
ID: id,
|
||||
VideoID: videoID,
|
||||
Title: title,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultCachedSticker create a new inline query with cached sticker.
|
||||
func NewInlineQueryResultCachedSticker(id, stickerID, title string) InlineQueryResultCachedSticker {
|
||||
return InlineQueryResultCachedSticker{
|
||||
Type: "sticker",
|
||||
ID: id,
|
||||
StickerID: stickerID,
|
||||
Title: title,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultAudio creates a new inline query audio.
|
||||
func NewInlineQueryResultAudio(id, url, title string) InlineQueryResultAudio {
|
||||
return InlineQueryResultAudio{
|
||||
Type: "audio",
|
||||
ID: id,
|
||||
URL: url,
|
||||
Title: title,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultCachedAudio create a new inline query with cached photo.
|
||||
func NewInlineQueryResultCachedAudio(id, audioID string) InlineQueryResultCachedAudio {
|
||||
return InlineQueryResultCachedAudio{
|
||||
Type: "audio",
|
||||
ID: id,
|
||||
AudioID: audioID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultVoice creates a new inline query voice.
|
||||
func NewInlineQueryResultVoice(id, url, title string) InlineQueryResultVoice {
|
||||
return InlineQueryResultVoice{
|
||||
Type: "voice",
|
||||
ID: id,
|
||||
URL: url,
|
||||
Title: title,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultCachedVoice create a new inline query with cached photo.
|
||||
func NewInlineQueryResultCachedVoice(id, voiceID, title string) InlineQueryResultCachedVoice {
|
||||
return InlineQueryResultCachedVoice{
|
||||
Type: "voice",
|
||||
ID: id,
|
||||
VoiceID: voiceID,
|
||||
Title: title,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultDocument creates a new inline query document.
|
||||
func NewInlineQueryResultDocument(id, url, title, mimeType string) InlineQueryResultDocument {
|
||||
return InlineQueryResultDocument{
|
||||
Type: "document",
|
||||
ID: id,
|
||||
URL: url,
|
||||
Title: title,
|
||||
MimeType: mimeType,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultCachedDocument create a new inline query with cached photo.
|
||||
func NewInlineQueryResultCachedDocument(id, documentID, title string) InlineQueryResultCachedDocument {
|
||||
return InlineQueryResultCachedDocument{
|
||||
Type: "document",
|
||||
ID: id,
|
||||
DocumentID: documentID,
|
||||
Title: title,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultLocation creates a new inline query location.
|
||||
func NewInlineQueryResultLocation(id, title string, latitude, longitude float64) InlineQueryResultLocation {
|
||||
return InlineQueryResultLocation{
|
||||
Type: "location",
|
||||
ID: id,
|
||||
Title: title,
|
||||
Latitude: latitude,
|
||||
Longitude: longitude,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineQueryResultVenue creates a new inline query venue.
|
||||
func NewInlineQueryResultVenue(id, title, address string, latitude, longitude float64) InlineQueryResultVenue {
|
||||
return InlineQueryResultVenue{
|
||||
Type: "venue",
|
||||
ID: id,
|
||||
Title: title,
|
||||
Address: address,
|
||||
Latitude: latitude,
|
||||
Longitude: longitude,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEditMessageText allows you to edit the text of a message.
|
||||
func NewEditMessageText(chatID int64, messageID int, text string) EditMessageTextConfig {
|
||||
return EditMessageTextConfig{
|
||||
BaseEdit: BaseEdit{
|
||||
ChatID: chatID,
|
||||
MessageID: messageID,
|
||||
},
|
||||
Text: text,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEditMessageTextAndMarkup allows you to edit the text and replymarkup of a message.
|
||||
func NewEditMessageTextAndMarkup(chatID int64, messageID int, text string, replyMarkup InlineKeyboardMarkup) EditMessageTextConfig {
|
||||
return EditMessageTextConfig{
|
||||
BaseEdit: BaseEdit{
|
||||
ChatID: chatID,
|
||||
MessageID: messageID,
|
||||
ReplyMarkup: &replyMarkup,
|
||||
},
|
||||
Text: text,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEditMessageCaption allows you to edit the caption of a message.
|
||||
func NewEditMessageCaption(chatID int64, messageID int, caption string) EditMessageCaptionConfig {
|
||||
return EditMessageCaptionConfig{
|
||||
BaseEdit: BaseEdit{
|
||||
ChatID: chatID,
|
||||
MessageID: messageID,
|
||||
},
|
||||
Caption: caption,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEditMessageReplyMarkup allows you to edit the inline
|
||||
// keyboard markup.
|
||||
func NewEditMessageReplyMarkup(chatID int64, messageID int, replyMarkup InlineKeyboardMarkup) EditMessageReplyMarkupConfig {
|
||||
return EditMessageReplyMarkupConfig{
|
||||
BaseEdit: BaseEdit{
|
||||
ChatID: chatID,
|
||||
MessageID: messageID,
|
||||
ReplyMarkup: &replyMarkup,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewRemoveKeyboard hides the keyboard, with the option for being selective
|
||||
// or hiding for everyone.
|
||||
func NewRemoveKeyboard(selective bool) ReplyKeyboardRemove {
|
||||
return ReplyKeyboardRemove{
|
||||
RemoveKeyboard: true,
|
||||
Selective: selective,
|
||||
}
|
||||
}
|
||||
|
||||
// NewKeyboardButton creates a regular keyboard button.
|
||||
func NewKeyboardButton(text string) KeyboardButton {
|
||||
return KeyboardButton{
|
||||
Text: text,
|
||||
}
|
||||
}
|
||||
|
||||
// NewKeyboardButtonContact creates a keyboard button that requests
|
||||
// user contact information upon click.
|
||||
func NewKeyboardButtonContact(text string) KeyboardButton {
|
||||
return KeyboardButton{
|
||||
Text: text,
|
||||
RequestContact: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewKeyboardButtonLocation creates a keyboard button that requests
|
||||
// user location information upon click.
|
||||
func NewKeyboardButtonLocation(text string) KeyboardButton {
|
||||
return KeyboardButton{
|
||||
Text: text,
|
||||
RequestLocation: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewKeyboardButtonRow creates a row of keyboard buttons.
|
||||
func NewKeyboardButtonRow(buttons ...KeyboardButton) []KeyboardButton {
|
||||
var row []KeyboardButton
|
||||
|
||||
row = append(row, buttons...)
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
// NewReplyKeyboard creates a new regular keyboard with sane defaults.
|
||||
func NewReplyKeyboard(rows ...[]KeyboardButton) ReplyKeyboardMarkup {
|
||||
var keyboard [][]KeyboardButton
|
||||
|
||||
keyboard = append(keyboard, rows...)
|
||||
|
||||
return ReplyKeyboardMarkup{
|
||||
ResizeKeyboard: true,
|
||||
Keyboard: keyboard,
|
||||
}
|
||||
}
|
||||
|
||||
// NewOneTimeReplyKeyboard creates a new one time keyboard.
|
||||
func NewOneTimeReplyKeyboard(rows ...[]KeyboardButton) ReplyKeyboardMarkup {
|
||||
markup := NewReplyKeyboard(rows...)
|
||||
markup.OneTimeKeyboard = true
|
||||
return markup
|
||||
}
|
||||
|
||||
// NewInlineKeyboardButtonData creates an inline keyboard button with text
|
||||
// and data for a callback.
|
||||
func NewInlineKeyboardButtonData(text, data string) InlineKeyboardButton {
|
||||
return InlineKeyboardButton{
|
||||
Text: text,
|
||||
CallbackData: &data,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineKeyboardButtonLoginURL creates an inline keyboard button with text
|
||||
// which goes to a LoginURL.
|
||||
func NewInlineKeyboardButtonLoginURL(text string, loginURL LoginURL) InlineKeyboardButton {
|
||||
return InlineKeyboardButton{
|
||||
Text: text,
|
||||
LoginURL: &loginURL,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineKeyboardButtonURL creates an inline keyboard button with text
|
||||
// which goes to a URL.
|
||||
func NewInlineKeyboardButtonURL(text, url string) InlineKeyboardButton {
|
||||
return InlineKeyboardButton{
|
||||
Text: text,
|
||||
URL: &url,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineKeyboardButtonSwitch creates an inline keyboard button with
|
||||
// text which allows the user to switch to a chat or return to a chat.
|
||||
func NewInlineKeyboardButtonSwitch(text, sw string) InlineKeyboardButton {
|
||||
return InlineKeyboardButton{
|
||||
Text: text,
|
||||
SwitchInlineQuery: &sw,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInlineKeyboardRow creates an inline keyboard row with buttons.
|
||||
func NewInlineKeyboardRow(buttons ...InlineKeyboardButton) []InlineKeyboardButton {
|
||||
var row []InlineKeyboardButton
|
||||
|
||||
row = append(row, buttons...)
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
// NewInlineKeyboardMarkup creates a new inline keyboard.
|
||||
func NewInlineKeyboardMarkup(rows ...[]InlineKeyboardButton) InlineKeyboardMarkup {
|
||||
var keyboard [][]InlineKeyboardButton
|
||||
|
||||
keyboard = append(keyboard, rows...)
|
||||
|
||||
return InlineKeyboardMarkup{
|
||||
InlineKeyboard: keyboard,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCallback creates a new callback message.
|
||||
func NewCallback(id, text string) CallbackConfig {
|
||||
return CallbackConfig{
|
||||
CallbackQueryID: id,
|
||||
Text: text,
|
||||
ShowAlert: false,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCallbackWithAlert creates a new callback message that alerts
|
||||
// the user.
|
||||
func NewCallbackWithAlert(id, text string) CallbackConfig {
|
||||
return CallbackConfig{
|
||||
CallbackQueryID: id,
|
||||
Text: text,
|
||||
ShowAlert: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewInvoice creates a new Invoice request to the user.
|
||||
func NewInvoice(chatID int64, title, description, payload, providerToken, startParameter, currency string, prices []LabeledPrice) InvoiceConfig {
|
||||
return InvoiceConfig{
|
||||
BaseChat: BaseChat{ChatID: chatID},
|
||||
Title: title,
|
||||
Description: description,
|
||||
Payload: payload,
|
||||
ProviderToken: providerToken,
|
||||
StartParameter: startParameter,
|
||||
Currency: currency,
|
||||
Prices: prices}
|
||||
}
|
||||
|
||||
// NewChatTitle allows you to update the title of a chat.
|
||||
func NewChatTitle(chatID int64, title string) SetChatTitleConfig {
|
||||
return SetChatTitleConfig{
|
||||
ChatID: chatID,
|
||||
Title: title,
|
||||
}
|
||||
}
|
||||
|
||||
// NewChatDescription allows you to update the description of a chat.
|
||||
func NewChatDescription(chatID int64, description string) SetChatDescriptionConfig {
|
||||
return SetChatDescriptionConfig{
|
||||
ChatID: chatID,
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
// NewChatPhoto allows you to update the photo for a chat.
|
||||
func NewChatPhoto(chatID int64, photo RequestFileData) SetChatPhotoConfig {
|
||||
return SetChatPhotoConfig{
|
||||
BaseFile: BaseFile{
|
||||
BaseChat: BaseChat{
|
||||
ChatID: chatID,
|
||||
},
|
||||
File: photo,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewDeleteChatPhoto allows you to delete the photo for a chat.
|
||||
func NewDeleteChatPhoto(chatID int64) DeleteChatPhotoConfig {
|
||||
return DeleteChatPhotoConfig{
|
||||
ChatID: chatID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPoll allows you to create a new poll.
|
||||
func NewPoll(chatID int64, question string, options ...string) SendPollConfig {
|
||||
return SendPollConfig{
|
||||
BaseChat: BaseChat{
|
||||
ChatID: chatID,
|
||||
},
|
||||
Question: question,
|
||||
Options: options,
|
||||
IsAnonymous: true, // This is Telegram's default.
|
||||
}
|
||||
}
|
||||
|
||||
// NewStopPoll allows you to stop a poll.
|
||||
func NewStopPoll(chatID int64, messageID int) StopPollConfig {
|
||||
return StopPollConfig{
|
||||
BaseEdit{
|
||||
ChatID: chatID,
|
||||
MessageID: messageID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewDice allows you to send a random dice roll.
|
||||
func NewDice(chatID int64) DiceConfig {
|
||||
return DiceConfig{
|
||||
BaseChat: BaseChat{
|
||||
ChatID: chatID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewDiceWithEmoji allows you to send a random roll of one of many types.
|
||||
//
|
||||
// Emoji may be 🎲 (1-6), 🎯 (1-6), or 🏀 (1-5).
|
||||
func NewDiceWithEmoji(chatID int64, emoji string) DiceConfig {
|
||||
return DiceConfig{
|
||||
BaseChat: BaseChat{
|
||||
ChatID: chatID,
|
||||
},
|
||||
Emoji: emoji,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBotCommandScopeDefault represents the default scope of bot commands.
|
||||
func NewBotCommandScopeDefault() BotCommandScope {
|
||||
return BotCommandScope{Type: "default"}
|
||||
}
|
||||
|
||||
// NewBotCommandScopeAllPrivateChats represents the scope of bot commands,
|
||||
// covering all private chats.
|
||||
func NewBotCommandScopeAllPrivateChats() BotCommandScope {
|
||||
return BotCommandScope{Type: "all_private_chats"}
|
||||
}
|
||||
|
||||
// NewBotCommandScopeAllGroupChats represents the scope of bot commands,
|
||||
// covering all group and supergroup chats.
|
||||
func NewBotCommandScopeAllGroupChats() BotCommandScope {
|
||||
return BotCommandScope{Type: "all_group_chats"}
|
||||
}
|
||||
|
||||
// NewBotCommandScopeAllChatAdministrators represents the scope of bot commands,
|
||||
// covering all group and supergroup chat administrators.
|
||||
func NewBotCommandScopeAllChatAdministrators() BotCommandScope {
|
||||
return BotCommandScope{Type: "all_chat_administrators"}
|
||||
}
|
||||
|
||||
// NewBotCommandScopeChat represents the scope of bot commands, covering a
|
||||
// specific chat.
|
||||
func NewBotCommandScopeChat(chatID int64) BotCommandScope {
|
||||
return BotCommandScope{
|
||||
Type: "chat",
|
||||
ChatID: chatID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBotCommandScopeChatAdministrators represents the scope of bot commands,
|
||||
// covering all administrators of a specific group or supergroup chat.
|
||||
func NewBotCommandScopeChatAdministrators(chatID int64) BotCommandScope {
|
||||
return BotCommandScope{
|
||||
Type: "chat_administrators",
|
||||
ChatID: chatID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBotCommandScopeChatMember represents the scope of bot commands, covering a
|
||||
// specific member of a group or supergroup chat.
|
||||
func NewBotCommandScopeChatMember(chatID, userID int64) BotCommandScope {
|
||||
return BotCommandScope{
|
||||
Type: "chat_member",
|
||||
ChatID: chatID,
|
||||
UserID: userID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewGetMyCommandsWithScope allows you to set the registered commands for a
|
||||
// given scope.
|
||||
func NewGetMyCommandsWithScope(scope BotCommandScope) GetMyCommandsConfig {
|
||||
return GetMyCommandsConfig{Scope: &scope}
|
||||
}
|
||||
|
||||
// NewGetMyCommandsWithScopeAndLanguage allows you to set the registered
|
||||
// commands for a given scope and language code.
|
||||
func NewGetMyCommandsWithScopeAndLanguage(scope BotCommandScope, languageCode string) GetMyCommandsConfig {
|
||||
return GetMyCommandsConfig{Scope: &scope, LanguageCode: languageCode}
|
||||
}
|
||||
|
||||
// NewSetMyCommands allows you to set the registered commands.
|
||||
func NewSetMyCommands(commands ...BotCommand) SetMyCommandsConfig {
|
||||
return SetMyCommandsConfig{Commands: commands}
|
||||
}
|
||||
|
||||
// NewSetMyCommandsWithScope allows you to set the registered commands for a given scope.
|
||||
func NewSetMyCommandsWithScope(scope BotCommandScope, commands ...BotCommand) SetMyCommandsConfig {
|
||||
return SetMyCommandsConfig{Commands: commands, Scope: &scope}
|
||||
}
|
||||
|
||||
// NewSetMyCommandsWithScopeAndLanguage allows you to set the registered commands for a given scope
|
||||
// and language code.
|
||||
func NewSetMyCommandsWithScopeAndLanguage(scope BotCommandScope, languageCode string, commands ...BotCommand) SetMyCommandsConfig {
|
||||
return SetMyCommandsConfig{Commands: commands, Scope: &scope, LanguageCode: languageCode}
|
||||
}
|
||||
|
||||
// NewDeleteMyCommands allows you to delete the registered commands.
|
||||
func NewDeleteMyCommands() DeleteMyCommandsConfig {
|
||||
return DeleteMyCommandsConfig{}
|
||||
}
|
||||
|
||||
// NewDeleteMyCommandsWithScope allows you to delete the registered commands for a given
|
||||
// scope.
|
||||
func NewDeleteMyCommandsWithScope(scope BotCommandScope) DeleteMyCommandsConfig {
|
||||
return DeleteMyCommandsConfig{Scope: &scope}
|
||||
}
|
||||
|
||||
// NewDeleteMyCommandsWithScopeAndLanguage allows you to delete the registered commands for a given
|
||||
// scope and language code.
|
||||
func NewDeleteMyCommandsWithScopeAndLanguage(scope BotCommandScope, languageCode string) DeleteMyCommandsConfig {
|
||||
return DeleteMyCommandsConfig{Scope: &scope, LanguageCode: languageCode}
|
||||
}
|
236
helpers_test.go
Normal file
236
helpers_test.go
Normal file
@ -0,0 +1,236 @@
|
||||
package tgbotapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewWebhook(t *testing.T) {
|
||||
result, err := NewWebhook("https://example.com/token")
|
||||
|
||||
if err != nil ||
|
||||
result.URL.String() != "https://example.com/token" ||
|
||||
result.Certificate != interface{}(nil) ||
|
||||
result.MaxConnections != 0 ||
|
||||
len(result.AllowedUpdates) != 0 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWebhookWithCert(t *testing.T) {
|
||||
exampleFile := FileID("123")
|
||||
result, err := NewWebhookWithCert("https://example.com/token", exampleFile)
|
||||
|
||||
if err != nil ||
|
||||
result.URL.String() != "https://example.com/token" ||
|
||||
result.Certificate != exampleFile ||
|
||||
result.MaxConnections != 0 ||
|
||||
len(result.AllowedUpdates) != 0 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInlineQueryResultArticle(t *testing.T) {
|
||||
result := NewInlineQueryResultArticle("id", "title", "message")
|
||||
|
||||
if result.Type != "article" ||
|
||||
result.ID != "id" ||
|
||||
result.Title != "title" ||
|
||||
result.InputMessageContent.(InputTextMessageContent).Text != "message" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInlineQueryResultArticleMarkdown(t *testing.T) {
|
||||
result := NewInlineQueryResultArticleMarkdown("id", "title", "*message*")
|
||||
|
||||
if result.Type != "article" ||
|
||||
result.ID != "id" ||
|
||||
result.Title != "title" ||
|
||||
result.InputMessageContent.(InputTextMessageContent).Text != "*message*" ||
|
||||
result.InputMessageContent.(InputTextMessageContent).ParseMode != "Markdown" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInlineQueryResultArticleHTML(t *testing.T) {
|
||||
result := NewInlineQueryResultArticleHTML("id", "title", "<b>message</b>")
|
||||
|
||||
if result.Type != "article" ||
|
||||
result.ID != "id" ||
|
||||
result.Title != "title" ||
|
||||
result.InputMessageContent.(InputTextMessageContent).Text != "<b>message</b>" ||
|
||||
result.InputMessageContent.(InputTextMessageContent).ParseMode != "HTML" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInlineQueryResultGIF(t *testing.T) {
|
||||
result := NewInlineQueryResultGIF("id", "google.com")
|
||||
|
||||
if result.Type != "gif" ||
|
||||
result.ID != "id" ||
|
||||
result.URL != "google.com" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInlineQueryResultMPEG4GIF(t *testing.T) {
|
||||
result := NewInlineQueryResultMPEG4GIF("id", "google.com")
|
||||
|
||||
if result.Type != "mpeg4_gif" ||
|
||||
result.ID != "id" ||
|
||||
result.URL != "google.com" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInlineQueryResultPhoto(t *testing.T) {
|
||||
result := NewInlineQueryResultPhoto("id", "google.com")
|
||||
|
||||
if result.Type != "photo" ||
|
||||
result.ID != "id" ||
|
||||
result.URL != "google.com" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInlineQueryResultPhotoWithThumb(t *testing.T) {
|
||||
result := NewInlineQueryResultPhotoWithThumb("id", "google.com", "thumb.com")
|
||||
|
||||
if result.Type != "photo" ||
|
||||
result.ID != "id" ||
|
||||
result.URL != "google.com" ||
|
||||
result.ThumbURL != "thumb.com" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInlineQueryResultVideo(t *testing.T) {
|
||||
result := NewInlineQueryResultVideo("id", "google.com")
|
||||
|
||||
if result.Type != "video" ||
|
||||
result.ID != "id" ||
|
||||
result.URL != "google.com" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInlineQueryResultAudio(t *testing.T) {
|
||||
result := NewInlineQueryResultAudio("id", "google.com", "title")
|
||||
|
||||
if result.Type != "audio" ||
|
||||
result.ID != "id" ||
|
||||
result.URL != "google.com" ||
|
||||
result.Title != "title" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInlineQueryResultVoice(t *testing.T) {
|
||||
result := NewInlineQueryResultVoice("id", "google.com", "title")
|
||||
|
||||
if result.Type != "voice" ||
|
||||
result.ID != "id" ||
|
||||
result.URL != "google.com" ||
|
||||
result.Title != "title" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInlineQueryResultDocument(t *testing.T) {
|
||||
result := NewInlineQueryResultDocument("id", "google.com", "title", "mime/type")
|
||||
|
||||
if result.Type != "document" ||
|
||||
result.ID != "id" ||
|
||||
result.URL != "google.com" ||
|
||||
result.Title != "title" ||
|
||||
result.MimeType != "mime/type" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInlineQueryResultLocation(t *testing.T) {
|
||||
result := NewInlineQueryResultLocation("id", "name", 40, 50)
|
||||
|
||||
if result.Type != "location" ||
|
||||
result.ID != "id" ||
|
||||
result.Title != "name" ||
|
||||
result.Latitude != 40 ||
|
||||
result.Longitude != 50 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInlineKeyboardButtonLoginURL(t *testing.T) {
|
||||
result := NewInlineKeyboardButtonLoginURL("text", LoginURL{
|
||||
URL: "url",
|
||||
ForwardText: "ForwardText",
|
||||
BotUsername: "username",
|
||||
RequestWriteAccess: false,
|
||||
})
|
||||
|
||||
if result.Text != "text" ||
|
||||
result.LoginURL.URL != "url" ||
|
||||
result.LoginURL.ForwardText != "ForwardText" ||
|
||||
result.LoginURL.BotUsername != "username" ||
|
||||
result.LoginURL.RequestWriteAccess != false {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEditMessageText(t *testing.T) {
|
||||
edit := NewEditMessageText(ChatID, ReplyToMessageID, "new text")
|
||||
|
||||
if edit.Text != "new text" ||
|
||||
edit.BaseEdit.ChatID != ChatID ||
|
||||
edit.BaseEdit.MessageID != ReplyToMessageID {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEditMessageCaption(t *testing.T) {
|
||||
edit := NewEditMessageCaption(ChatID, ReplyToMessageID, "new caption")
|
||||
|
||||
if edit.Caption != "new caption" ||
|
||||
edit.BaseEdit.ChatID != ChatID ||
|
||||
edit.BaseEdit.MessageID != ReplyToMessageID {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEditMessageReplyMarkup(t *testing.T) {
|
||||
markup := InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]InlineKeyboardButton{
|
||||
{
|
||||
{Text: "test"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
edit := NewEditMessageReplyMarkup(ChatID, ReplyToMessageID, markup)
|
||||
|
||||
if edit.ReplyMarkup.InlineKeyboard[0][0].Text != "test" ||
|
||||
edit.BaseEdit.ChatID != ChatID ||
|
||||
edit.BaseEdit.MessageID != ReplyToMessageID {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNewDice(t *testing.T) {
|
||||
dice := NewDice(42)
|
||||
|
||||
if dice.ChatID != 42 ||
|
||||
dice.Emoji != "" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDiceWithEmoji(t *testing.T) {
|
||||
dice := NewDiceWithEmoji(42, "🏀")
|
||||
|
||||
if dice.ChatID != 42 ||
|
||||
dice.Emoji != "🏀" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
27
log.go
Normal file
27
log.go
Normal file
@ -0,0 +1,27 @@
|
||||
package tgbotapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
stdlog "log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// BotLogger is an interface that represents the required methods to log data.
|
||||
//
|
||||
// Instead of requiring the standard logger, we can just specify the methods we
|
||||
// use and allow users to pass anything that implements these.
|
||||
type BotLogger interface {
|
||||
Println(v ...interface{})
|
||||
Printf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
var log BotLogger = stdlog.New(os.Stderr, "", stdlog.LstdFlags)
|
||||
|
||||
// SetLogger specifies the logger that the package should use.
|
||||
func SetLogger(logger BotLogger) error {
|
||||
if logger == nil {
|
||||
return errors.New("logger is nil")
|
||||
}
|
||||
log = logger
|
||||
return nil
|
||||
}
|
97
params.go
Normal file
97
params.go
Normal file
@ -0,0 +1,97 @@
|
||||
package tgbotapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Params represents a set of parameters that gets passed to a request.
|
||||
type Params map[string]string
|
||||
|
||||
// AddNonEmpty adds a value if it not an empty string.
|
||||
func (p Params) AddNonEmpty(key, value string) {
|
||||
if value != "" {
|
||||
p[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// AddNonZero adds a value if it is not zero.
|
||||
func (p Params) AddNonZero(key string, value int) {
|
||||
if value != 0 {
|
||||
p[key] = strconv.Itoa(value)
|
||||
}
|
||||
}
|
||||
|
||||
// AddNonZero64 is the same as AddNonZero except uses an int64.
|
||||
func (p Params) AddNonZero64(key string, value int64) {
|
||||
if value != 0 {
|
||||
p[key] = strconv.FormatInt(value, 10)
|
||||
}
|
||||
}
|
||||
|
||||
// AddBool adds a value of a bool if it is true.
|
||||
func (p Params) AddBool(key string, value bool) {
|
||||
if value {
|
||||
p[key] = strconv.FormatBool(value)
|
||||
}
|
||||
}
|
||||
|
||||
// AddNonZeroFloat adds a floating point value that is not zero.
|
||||
func (p Params) AddNonZeroFloat(key string, value float64) {
|
||||
if value != 0 {
|
||||
p[key] = strconv.FormatFloat(value, 'f', 6, 64)
|
||||
}
|
||||
}
|
||||
|
||||
// AddInterface adds an interface if it is not nil and can be JSON marshalled.
|
||||
func (p Params) AddInterface(key string, value interface{}) error {
|
||||
if value == nil || (reflect.ValueOf(value).Kind() == reflect.Ptr && reflect.ValueOf(value).IsNil()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p[key] = string(b)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddFirstValid attempts to add the first item that is not a default value.
|
||||
//
|
||||
// For example, AddFirstValid(0, "", "test") would add "test".
|
||||
func (p Params) AddFirstValid(key string, args ...interface{}) error {
|
||||
for _, arg := range args {
|
||||
switch v := arg.(type) {
|
||||
case int:
|
||||
if v != 0 {
|
||||
p[key] = strconv.Itoa(v)
|
||||
return nil
|
||||
}
|
||||
case int64:
|
||||
if v != 0 {
|
||||
p[key] = strconv.FormatInt(v, 10)
|
||||
return nil
|
||||
}
|
||||
case string:
|
||||
if v != "" {
|
||||
p[key] = v
|
||||
return nil
|
||||
}
|
||||
case nil:
|
||||
default:
|
||||
b, err := json.Marshal(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p[key] = string(b)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
93
params_test.go
Normal file
93
params_test.go
Normal file
@ -0,0 +1,93 @@
|
||||
package tgbotapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func assertLen(t *testing.T, params Params, l int) {
|
||||
actual := len(params)
|
||||
if actual != l {
|
||||
t.Fatalf("Incorrect number of params, expected %d but found %d\n", l, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEq(t *testing.T, a interface{}, b interface{}) {
|
||||
if a != b {
|
||||
t.Fatalf("Values did not match, a: %v, b: %v\n", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddNonEmpty(t *testing.T) {
|
||||
params := make(Params)
|
||||
params.AddNonEmpty("value", "value")
|
||||
assertLen(t, params, 1)
|
||||
assertEq(t, params["value"], "value")
|
||||
params.AddNonEmpty("test", "")
|
||||
assertLen(t, params, 1)
|
||||
assertEq(t, params["test"], "")
|
||||
}
|
||||
|
||||
func TestAddNonZero(t *testing.T) {
|
||||
params := make(Params)
|
||||
params.AddNonZero("value", 1)
|
||||
assertLen(t, params, 1)
|
||||
assertEq(t, params["value"], "1")
|
||||
params.AddNonZero("test", 0)
|
||||
assertLen(t, params, 1)
|
||||
assertEq(t, params["test"], "")
|
||||
}
|
||||
|
||||
func TestAddNonZero64(t *testing.T) {
|
||||
params := make(Params)
|
||||
params.AddNonZero64("value", 1)
|
||||
assertLen(t, params, 1)
|
||||
assertEq(t, params["value"], "1")
|
||||
params.AddNonZero64("test", 0)
|
||||
assertLen(t, params, 1)
|
||||
assertEq(t, params["test"], "")
|
||||
}
|
||||
|
||||
func TestAddBool(t *testing.T) {
|
||||
params := make(Params)
|
||||
params.AddBool("value", true)
|
||||
assertLen(t, params, 1)
|
||||
assertEq(t, params["value"], "true")
|
||||
params.AddBool("test", false)
|
||||
assertLen(t, params, 1)
|
||||
assertEq(t, params["test"], "")
|
||||
}
|
||||
|
||||
func TestAddNonZeroFloat(t *testing.T) {
|
||||
params := make(Params)
|
||||
params.AddNonZeroFloat("value", 1)
|
||||
assertLen(t, params, 1)
|
||||
assertEq(t, params["value"], "1.000000")
|
||||
params.AddNonZeroFloat("test", 0)
|
||||
assertLen(t, params, 1)
|
||||
assertEq(t, params["test"], "")
|
||||
}
|
||||
|
||||
func TestAddInterface(t *testing.T) {
|
||||
params := make(Params)
|
||||
data := struct {
|
||||
Name string `json:"name"`
|
||||
}{
|
||||
Name: "test",
|
||||
}
|
||||
params.AddInterface("value", data)
|
||||
assertLen(t, params, 1)
|
||||
assertEq(t, params["value"], `{"name":"test"}`)
|
||||
params.AddInterface("test", nil)
|
||||
assertLen(t, params, 1)
|
||||
assertEq(t, params["test"], "")
|
||||
}
|
||||
|
||||
func TestAddFirstValid(t *testing.T) {
|
||||
params := make(Params)
|
||||
params.AddFirstValid("value", 0, "", "test")
|
||||
assertLen(t, params, 1)
|
||||
assertEq(t, params["value"], "test")
|
||||
params.AddFirstValid("value2", 3, "test")
|
||||
assertLen(t, params, 2)
|
||||
assertEq(t, params["value2"], "3")
|
||||
}
|
317
passport.go
Normal file
317
passport.go
Normal file
@ -0,0 +1,317 @@
|
||||
package tgbotapi
|
||||
|
||||
// PassportRequestInfoConfig allows you to request passport info
|
||||
type PassportRequestInfoConfig struct {
|
||||
BotID int `json:"bot_id"`
|
||||
Scope *PassportScope `json:"scope"`
|
||||
Nonce string `json:"nonce"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
|
||||
// PassportScopeElement supports using one or one of several elements.
|
||||
type PassportScopeElement interface {
|
||||
ScopeType() string
|
||||
}
|
||||
|
||||
// PassportScope is the requested scopes of data.
|
||||
type PassportScope struct {
|
||||
V int `json:"v"`
|
||||
Data []PassportScopeElement `json:"data"`
|
||||
}
|
||||
|
||||
// PassportScopeElementOneOfSeveral allows you to request any one of the
|
||||
// requested documents.
|
||||
type PassportScopeElementOneOfSeveral struct {
|
||||
}
|
||||
|
||||
// ScopeType is the scope type.
|
||||
func (eo *PassportScopeElementOneOfSeveral) ScopeType() string {
|
||||
return "one_of"
|
||||
}
|
||||
|
||||
// PassportScopeElementOne requires the specified element be provided.
|
||||
type PassportScopeElementOne struct {
|
||||
Type string `json:"type"` // One of “personal_details”, “passport”, “driver_license”, “identity_card”, “internal_passport”, “address”, “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration”, “temporary_registration”, “phone_number”, “email”
|
||||
Selfie bool `json:"selfie"`
|
||||
Translation bool `json:"translation"`
|
||||
NativeNames bool `json:"native_name"`
|
||||
}
|
||||
|
||||
// ScopeType is the scope type.
|
||||
func (eo *PassportScopeElementOne) ScopeType() string {
|
||||
return "one"
|
||||
}
|
||||
|
||||
type (
|
||||
// PassportData contains information about Telegram Passport data shared with
|
||||
// the bot by the user.
|
||||
PassportData struct {
|
||||
// Array with information about documents and other Telegram Passport
|
||||
// elements that was shared with the bot
|
||||
Data []EncryptedPassportElement `json:"data"`
|
||||
|
||||
// Encrypted credentials required to decrypt the data
|
||||
Credentials *EncryptedCredentials `json:"credentials"`
|
||||
}
|
||||
|
||||
// PassportFile represents a file uploaded to Telegram Passport. Currently, all
|
||||
// Telegram Passport files are in JPEG format when decrypted and don't exceed
|
||||
// 10MB.
|
||||
PassportFile struct {
|
||||
// Unique identifier for this file
|
||||
FileID string `json:"file_id"`
|
||||
|
||||
FileUniqueID string `json:"file_unique_id"`
|
||||
|
||||
// File size
|
||||
FileSize int `json:"file_size"`
|
||||
|
||||
// Unix time when the file was uploaded
|
||||
FileDate int64 `json:"file_date"`
|
||||
}
|
||||
|
||||
// EncryptedPassportElement contains information about documents or other
|
||||
// Telegram Passport elements shared with the bot by the user.
|
||||
EncryptedPassportElement struct {
|
||||
// Element type.
|
||||
Type string `json:"type"`
|
||||
|
||||
// Base64-encoded encrypted Telegram Passport element data provided by
|
||||
// the user, available for "personal_details", "passport",
|
||||
// "driver_license", "identity_card", "identity_passport" and "address"
|
||||
// types. Can be decrypted and verified using the accompanying
|
||||
// EncryptedCredentials.
|
||||
Data string `json:"data,omitempty"`
|
||||
|
||||
// User's verified phone number, available only for "phone_number" type
|
||||
PhoneNumber string `json:"phone_number,omitempty"`
|
||||
|
||||
// User's verified email address, available only for "email" type
|
||||
Email string `json:"email,omitempty"`
|
||||
|
||||
// Array of encrypted files with documents provided by the user,
|
||||
// available for "utility_bill", "bank_statement", "rental_agreement",
|
||||
// "passport_registration" and "temporary_registration" types. Files can
|
||||
// be decrypted and verified using the accompanying EncryptedCredentials.
|
||||
Files []PassportFile `json:"files,omitempty"`
|
||||
|
||||
// Encrypted file with the front side of the document, provided by the
|
||||
// user. Available for "passport", "driver_license", "identity_card" and
|
||||
// "internal_passport". The file can be decrypted and verified using the
|
||||
// accompanying EncryptedCredentials.
|
||||
FrontSide *PassportFile `json:"front_side,omitempty"`
|
||||
|
||||
// Encrypted file with the reverse side of the document, provided by the
|
||||
// user. Available for "driver_license" and "identity_card". The file can
|
||||
// be decrypted and verified using the accompanying EncryptedCredentials.
|
||||
ReverseSide *PassportFile `json:"reverse_side,omitempty"`
|
||||
|
||||
// Encrypted file with the selfie of the user holding a document,
|
||||
// provided by the user; available for "passport", "driver_license",
|
||||
// "identity_card" and "internal_passport". The file can be decrypted
|
||||
// and verified using the accompanying EncryptedCredentials.
|
||||
Selfie *PassportFile `json:"selfie,omitempty"`
|
||||
}
|
||||
|
||||
// EncryptedCredentials contains data required for decrypting and
|
||||
// authenticating EncryptedPassportElement. See the Telegram Passport
|
||||
// Documentation for a complete description of the data decryption and
|
||||
// authentication processes.
|
||||
EncryptedCredentials struct {
|
||||
// Base64-encoded encrypted JSON-serialized data with unique user's
|
||||
// payload, data hashes and secrets required for EncryptedPassportElement
|
||||
// decryption and authentication
|
||||
Data string `json:"data"`
|
||||
|
||||
// Base64-encoded data hash for data authentication
|
||||
Hash string `json:"hash"`
|
||||
|
||||
// Base64-encoded secret, encrypted with the bot's public RSA key,
|
||||
// required for data decryption
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// PassportElementError represents an error in the Telegram Passport element
|
||||
// which was submitted that should be resolved by the user.
|
||||
PassportElementError interface{}
|
||||
|
||||
// PassportElementErrorDataField represents an issue in one of the data
|
||||
// fields that was provided by the user. The error is considered resolved
|
||||
// when the field's value changes.
|
||||
PassportElementErrorDataField struct {
|
||||
// Error source, must be data
|
||||
Source string `json:"source"`
|
||||
|
||||
// The section of the user's Telegram Passport which has the error, one
|
||||
// of "personal_details", "passport", "driver_license", "identity_card",
|
||||
// "internal_passport", "address"
|
||||
Type string `json:"type"`
|
||||
|
||||
// Name of the data field which has the error
|
||||
FieldName string `json:"field_name"`
|
||||
|
||||
// Base64-encoded data hash
|
||||
DataHash string `json:"data_hash"`
|
||||
|
||||
// Error message
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// PassportElementErrorFrontSide represents an issue with the front side of
|
||||
// a document. The error is considered resolved when the file with the front
|
||||
// side of the document changes.
|
||||
PassportElementErrorFrontSide struct {
|
||||
// Error source, must be front_side
|
||||
Source string `json:"source"`
|
||||
|
||||
// The section of the user's Telegram Passport which has the issue, one
|
||||
// of "passport", "driver_license", "identity_card", "internal_passport"
|
||||
Type string `json:"type"`
|
||||
|
||||
// Base64-encoded hash of the file with the front side of the document
|
||||
FileHash string `json:"file_hash"`
|
||||
|
||||
// Error message
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// PassportElementErrorReverseSide represents an issue with the reverse side
|
||||
// of a document. The error is considered resolved when the file with reverse
|
||||
// side of the document changes.
|
||||
PassportElementErrorReverseSide struct {
|
||||
// Error source, must be reverse_side
|
||||
Source string `json:"source"`
|
||||
|
||||
// The section of the user's Telegram Passport which has the issue, one
|
||||
// of "driver_license", "identity_card"
|
||||
Type string `json:"type"`
|
||||
|
||||
// Base64-encoded hash of the file with the reverse side of the document
|
||||
FileHash string `json:"file_hash"`
|
||||
|
||||
// Error message
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// PassportElementErrorSelfie represents an issue with the selfie with a
|
||||
// document. The error is considered resolved when the file with the selfie
|
||||
// changes.
|
||||
PassportElementErrorSelfie struct {
|
||||
// Error source, must be selfie
|
||||
Source string `json:"source"`
|
||||
|
||||
// The section of the user's Telegram Passport which has the issue, one
|
||||
// of "passport", "driver_license", "identity_card", "internal_passport"
|
||||
Type string `json:"type"`
|
||||
|
||||
// Base64-encoded hash of the file with the selfie
|
||||
FileHash string `json:"file_hash"`
|
||||
|
||||
// Error message
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// PassportElementErrorFile represents an issue with a document scan. The
|
||||
// error is considered resolved when the file with the document scan changes.
|
||||
PassportElementErrorFile struct {
|
||||
// Error source, must be a file
|
||||
Source string `json:"source"`
|
||||
|
||||
// The section of the user's Telegram Passport which has the issue, one
|
||||
// of "utility_bill", "bank_statement", "rental_agreement",
|
||||
// "passport_registration", "temporary_registration"
|
||||
Type string `json:"type"`
|
||||
|
||||
// Base64-encoded file hash
|
||||
FileHash string `json:"file_hash"`
|
||||
|
||||
// Error message
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// PassportElementErrorFiles represents an issue with a list of scans. The
|
||||
// error is considered resolved when the list of files containing the scans
|
||||
// changes.
|
||||
PassportElementErrorFiles struct {
|
||||
// Error source, must be files
|
||||
Source string `json:"source"`
|
||||
|
||||
// The section of the user's Telegram Passport which has the issue, one
|
||||
// of "utility_bill", "bank_statement", "rental_agreement",
|
||||
// "passport_registration", "temporary_registration"
|
||||
Type string `json:"type"`
|
||||
|
||||
// List of base64-encoded file hashes
|
||||
FileHashes []string `json:"file_hashes"`
|
||||
|
||||
// Error message
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Credentials contains encrypted data.
|
||||
Credentials struct {
|
||||
Data SecureData `json:"secure_data"`
|
||||
// Nonce the same nonce given in the request
|
||||
Nonce string `json:"nonce"`
|
||||
}
|
||||
|
||||
// SecureData is a map of the fields and their encrypted values.
|
||||
SecureData map[string]*SecureValue
|
||||
// PersonalDetails *SecureValue `json:"personal_details"`
|
||||
// Passport *SecureValue `json:"passport"`
|
||||
// InternalPassport *SecureValue `json:"internal_passport"`
|
||||
// DriverLicense *SecureValue `json:"driver_license"`
|
||||
// IdentityCard *SecureValue `json:"identity_card"`
|
||||
// Address *SecureValue `json:"address"`
|
||||
// UtilityBill *SecureValue `json:"utility_bill"`
|
||||
// BankStatement *SecureValue `json:"bank_statement"`
|
||||
// RentalAgreement *SecureValue `json:"rental_agreement"`
|
||||
// PassportRegistration *SecureValue `json:"passport_registration"`
|
||||
// TemporaryRegistration *SecureValue `json:"temporary_registration"`
|
||||
|
||||
// SecureValue contains encrypted values for a SecureData item.
|
||||
SecureValue struct {
|
||||
Data *DataCredentials `json:"data"`
|
||||
FrontSide *FileCredentials `json:"front_side"`
|
||||
ReverseSide *FileCredentials `json:"reverse_side"`
|
||||
Selfie *FileCredentials `json:"selfie"`
|
||||
Translation []*FileCredentials `json:"translation"`
|
||||
Files []*FileCredentials `json:"files"`
|
||||
}
|
||||
|
||||
// DataCredentials contains information required to decrypt data.
|
||||
DataCredentials struct {
|
||||
// DataHash checksum of encrypted data
|
||||
DataHash string `json:"data_hash"`
|
||||
// Secret of encrypted data
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// FileCredentials contains information required to decrypt files.
|
||||
FileCredentials struct {
|
||||
// FileHash checksum of encrypted data
|
||||
FileHash string `json:"file_hash"`
|
||||
// Secret of encrypted data
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// PersonalDetails https://core.telegram.org/passport#personaldetails
|
||||
PersonalDetails struct {
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
MiddleName string `json:"middle_name"`
|
||||
BirthDate string `json:"birth_date"`
|
||||
Gender string `json:"gender"`
|
||||
CountryCode string `json:"country_code"`
|
||||
ResidenceCountryCode string `json:"residence_country_code"`
|
||||
FirstNameNative string `json:"first_name_native"`
|
||||
LastNameNative string `json:"last_name_native"`
|
||||
MiddleNameNative string `json:"middle_name_native"`
|
||||
}
|
||||
|
||||
// IDDocumentData https://core.telegram.org/passport#iddocumentdata
|
||||
IDDocumentData struct {
|
||||
DocumentNumber string `json:"document_no"`
|
||||
ExpiryDate string `json:"expiry_date"`
|
||||
}
|
||||
)
|
BIN
tests/audio.mp3
Normal file
BIN
tests/audio.mp3
Normal file
Binary file not shown.
18
tests/cert.pem
Normal file
18
tests/cert.pem
Normal file
@ -0,0 +1,18 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC0zCCAbugAwIBAgIJAPYfllX657axMA0GCSqGSIb3DQEBCwUAMAAwHhcNMTUx
|
||||
MTIxMTExMDQxWhcNMjUwODIwMTExMDQxWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||
AQ8AMIIBCgKCAQEAoMMSIIgYx8pT8Kz1O8Ukd/JVyqBQYRSo0enqEzo7295VROXq
|
||||
TUthbEbdi0OczUfl4IsAWppOSRrDwEguJZ0cJ/r6IxGsbrCdQr2MjgiomYtAXKKQ
|
||||
GAGL5Wls+AzcRNV0OszVJzkDNFYZzgNejyitGJSNEQMyU8r2gyPyIWP9MQKQst8y
|
||||
Mg91R/7l9jwf6AWwNxykZlYZurtsQ6XsBPZpF9YOFL7vZYPhKUFiNEm+74RpojC7
|
||||
Gt6nztYAUI2V/F+1uoXAr8nLpbj9SD0VSwyZLRG1uIVLBzhb0lfOIzAvJ45EKki9
|
||||
nejyoXfH1U5+iMzdSAdcy3MCBhpEZwJPqhDqeQIDAQABo1AwTjAdBgNVHQ4EFgQU
|
||||
JE0RLM+ohLnlDz0Qk0McCxtDK2MwHwYDVR0jBBgwFoAUJE0RLM+ohLnlDz0Qk0Mc
|
||||
CxtDK2MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAEmgME00JYuYZ
|
||||
4wNaGrJskZ05ZnP+TXJusmBui9ToQ4UoykuyY5rsdGQ3SdzXPLdmd2nfMsw63iK2
|
||||
D7rjcH/rmn6fRccZqN0o0SXd/EuHeIoeW1Xnnivbt71b6mcOAeNg1UsMYxnMAVl0
|
||||
ywdkta8gURltagSfXoUbqlnSxn/zCwqaxxcQXA/CnunvRsFtQrwWjDBPg/BPULHX
|
||||
DEh2AactGtnGqEZ5iap/VCOVnmL6iPdJ1x5UIF/gS6U96wL+GHfcs1jCvPg+GEwR
|
||||
3inh9oTXG9L21ge4lbGiPUIMBjtVcB3bXuQbOfec9Cr3ZhcQeZj680BIRxD/pNpA
|
||||
O/XeCfjfkw==
|
||||
-----END CERTIFICATE-----
|
BIN
tests/image.jpg
Normal file
BIN
tests/image.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 84 KiB |
28
tests/key.pem
Normal file
28
tests/key.pem
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQCgwxIgiBjHylPw
|
||||
rPU7xSR38lXKoFBhFKjR6eoTOjvb3lVE5epNS2FsRt2LQ5zNR+XgiwBamk5JGsPA
|
||||
SC4lnRwn+vojEaxusJ1CvYyOCKiZi0BcopAYAYvlaWz4DNxE1XQ6zNUnOQM0VhnO
|
||||
A16PKK0YlI0RAzJTyvaDI/IhY/0xApCy3zIyD3VH/uX2PB/oBbA3HKRmVhm6u2xD
|
||||
pewE9mkX1g4Uvu9lg+EpQWI0Sb7vhGmiMLsa3qfO1gBQjZX8X7W6hcCvyculuP1I
|
||||
PRVLDJktEbW4hUsHOFvSV84jMC8njkQqSL2d6PKhd8fVTn6IzN1IB1zLcwIGGkRn
|
||||
Ak+qEOp5AgMBAAECggEBAJ/dPCJzlEjhL5XPONLmGXzZ1Gx5/VR86eBMv0O9jhb3
|
||||
wk2QYO3aPxggZGD/rGcKz1L6hzCR77WM0wpb/N/Um1I6pxHGmnU8VjYvLh10CM0f
|
||||
h7JWyfnFV+ubagxFJamhpkJuvKyTaldaI7EU8qxj47Xky18Wka53z6nbTgXcW8Sm
|
||||
V4CJy9OHNgKJQnylX6zOAaxVngSGde3xLslLjsYK4w9b2+OkCSUST2XXdo+ZLXxl
|
||||
cs0lEPFRM1Xh9/E6UrDrJMHHzio53L/W/+a8sIar1upgSY52pyD/tA7VSrAJ9nYC
|
||||
RezOU81VTLfMO+TYmgZzSUQJYh0cR4yqJe+wgl4U550CgYEA1EcS6Z+PO5Pr3u2+
|
||||
XevawSAal6y9ONkkdOoASC977W37nn0E1wlQo41dR6DESCJfiSMeN0KbmXj5Wnc/
|
||||
ADu+73iGwC90G9Qs9sjD7KAFBJvuj0V8hxvpWRdIBBbf7rlOj3CV0iXRYjkJbyJa
|
||||
cxuR0kiv4gTWmm5Cq+5ir8t1Oc8CgYEAwd+xOaDerNR481R+QmENFw+oR2EVMq3Q
|
||||
B/vinLK0PemQWrh32iBcd+vhSilOSQtUm1nko1jLK8C4s8X2vZYua4m5tcK9VqCt
|
||||
maCCq/ffxzsoW/GN8japnduz+qA+hKWJzW/aYR8tsOeqzjVqj4iIqPI4HuokrDi/
|
||||
UD/QLgq5UTcCgYEAk2ZC0Kx15dXB7AtDq63xOTcUoAtXXRkSgohV58npEKXVGWkQ
|
||||
Kk0SjG7Fvc35XWlY0z3qZk6/AuOIqfOxcHUMEPatAtgwlH5RNo+T1EQNF/U6wotq
|
||||
e9q6vp026XgEyJwt29Y+giy2ZrDaRywgiFs1d0H3t0bKyXMUopQmPJFXdesCgYEA
|
||||
psCxXcDpZjxGX/zPsGZrbOdxtRtisTlg0k0rp93pO8tV90HtDHeDMT54g2ItzJPr
|
||||
TMev6XOpJNPZyf6+8GhpOuO2EQkT85u2VYoCeslz95gBabvFfIzZrUZYcnw76bm8
|
||||
YjAP5DN+CEfq2PyG0Df+W1ojPSvlKSCSJQMOG1vr81cCgYEAkjPY5WR99uJxYBNI
|
||||
OTFMSkETgDUbPXBu/E/h5Dtn79v8Moj9FvC7+q6sg9qXhrGhfK2xDev3/sTrbS/E
|
||||
Gcf8UNIne3AXsoAS8MtkOwJXHkYaTIboIYgDX4LlDmbGQlIRaWgyh2POI6VtjLBT
|
||||
ms6AdsdpIB6As9xNUBUwj/RnTZQ=
|
||||
-----END PRIVATE KEY-----
|
BIN
tests/video.mp4
Normal file
BIN
tests/video.mp4
Normal file
Binary file not shown.
BIN
tests/videonote.mp4
Normal file
BIN
tests/videonote.mp4
Normal file
Binary file not shown.
BIN
tests/voice.ogg
Normal file
BIN
tests/voice.ogg
Normal file
Binary file not shown.
381
types_test.go
Normal file
381
types_test.go
Normal file
@ -0,0 +1,381 @@
|
||||
package tgbotapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestUserStringWith(t *testing.T) {
|
||||
user := User{
|
||||
ID: 0,
|
||||
FirstName: "Test",
|
||||
LastName: "Test",
|
||||
UserName: "",
|
||||
LanguageCode: "en",
|
||||
IsBot: false,
|
||||
}
|
||||
|
||||
if user.String() != "Test Test" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserStringWithUserName(t *testing.T) {
|
||||
user := User{
|
||||
ID: 0,
|
||||
FirstName: "Test",
|
||||
LastName: "Test",
|
||||
UserName: "@test",
|
||||
LanguageCode: "en",
|
||||
}
|
||||
|
||||
if user.String() != "@test" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageTime(t *testing.T) {
|
||||
message := Message{Date: 0}
|
||||
|
||||
date := time.Unix(0, 0)
|
||||
if message.Time() != date {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageIsCommandWithCommand(t *testing.T) {
|
||||
message := Message{Text: "/command"}
|
||||
message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}}
|
||||
|
||||
if !message.IsCommand() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCommandWithText(t *testing.T) {
|
||||
message := Message{Text: "some text"}
|
||||
|
||||
if message.IsCommand() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCommandWithEmptyText(t *testing.T) {
|
||||
message := Message{Text: ""}
|
||||
|
||||
if message.IsCommand() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandWithCommand(t *testing.T) {
|
||||
message := Message{Text: "/command"}
|
||||
message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}}
|
||||
|
||||
if message.Command() != "command" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandWithEmptyText(t *testing.T) {
|
||||
message := Message{Text: ""}
|
||||
|
||||
if message.Command() != "" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandWithNonCommand(t *testing.T) {
|
||||
message := Message{Text: "test text"}
|
||||
|
||||
if message.Command() != "" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandWithBotName(t *testing.T) {
|
||||
message := Message{Text: "/command@testbot"}
|
||||
message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 16}}
|
||||
|
||||
if message.Command() != "command" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandWithAtWithBotName(t *testing.T) {
|
||||
message := Message{Text: "/command@testbot"}
|
||||
message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 16}}
|
||||
|
||||
if message.CommandWithAt() != "command@testbot" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageCommandArgumentsWithArguments(t *testing.T) {
|
||||
message := Message{Text: "/command with arguments"}
|
||||
message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}}
|
||||
if message.CommandArguments() != "with arguments" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageCommandArgumentsWithMalformedArguments(t *testing.T) {
|
||||
message := Message{Text: "/command-without argument space"}
|
||||
message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}}
|
||||
if message.CommandArguments() != "without argument space" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageCommandArgumentsWithoutArguments(t *testing.T) {
|
||||
message := Message{Text: "/command"}
|
||||
if message.CommandArguments() != "" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageCommandArgumentsForNonCommand(t *testing.T) {
|
||||
message := Message{Text: "test text"}
|
||||
if message.CommandArguments() != "" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageEntityParseURLGood(t *testing.T) {
|
||||
entity := MessageEntity{URL: "https://www.google.com"}
|
||||
|
||||
if _, err := entity.ParseURL(); err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageEntityParseURLBad(t *testing.T) {
|
||||
entity := MessageEntity{URL: ""}
|
||||
|
||||
if _, err := entity.ParseURL(); err == nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatIsPrivate(t *testing.T) {
|
||||
chat := Chat{ID: 10, Type: "private"}
|
||||
|
||||
if !chat.IsPrivate() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatIsGroup(t *testing.T) {
|
||||
chat := Chat{ID: 10, Type: "group"}
|
||||
|
||||
if !chat.IsGroup() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatIsChannel(t *testing.T) {
|
||||
chat := Chat{ID: 10, Type: "channel"}
|
||||
|
||||
if !chat.IsChannel() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatIsSuperGroup(t *testing.T) {
|
||||
chat := Chat{ID: 10, Type: "supergroup"}
|
||||
|
||||
if !chat.IsSuperGroup() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageEntityIsMention(t *testing.T) {
|
||||
entity := MessageEntity{Type: "mention"}
|
||||
|
||||
if !entity.IsMention() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageEntityIsHashtag(t *testing.T) {
|
||||
entity := MessageEntity{Type: "hashtag"}
|
||||
|
||||
if !entity.IsHashtag() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageEntityIsBotCommand(t *testing.T) {
|
||||
entity := MessageEntity{Type: "bot_command"}
|
||||
|
||||
if !entity.IsCommand() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageEntityIsUrl(t *testing.T) {
|
||||
entity := MessageEntity{Type: "url"}
|
||||
|
||||
if !entity.IsURL() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageEntityIsEmail(t *testing.T) {
|
||||
entity := MessageEntity{Type: "email"}
|
||||
|
||||
if !entity.IsEmail() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageEntityIsBold(t *testing.T) {
|
||||
entity := MessageEntity{Type: "bold"}
|
||||
|
||||
if !entity.IsBold() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageEntityIsItalic(t *testing.T) {
|
||||
entity := MessageEntity{Type: "italic"}
|
||||
|
||||
if !entity.IsItalic() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageEntityIsCode(t *testing.T) {
|
||||
entity := MessageEntity{Type: "code"}
|
||||
|
||||
if !entity.IsCode() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageEntityIsPre(t *testing.T) {
|
||||
entity := MessageEntity{Type: "pre"}
|
||||
|
||||
if !entity.IsPre() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageEntityIsTextLink(t *testing.T) {
|
||||
entity := MessageEntity{Type: "text_link"}
|
||||
|
||||
if !entity.IsTextLink() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileLink(t *testing.T) {
|
||||
file := File{FilePath: "test/test.txt"}
|
||||
|
||||
if file.Link("token") != "https://api.telegram.org/file/bottoken/test/test.txt" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure all configs are sendable
|
||||
var (
|
||||
_ Chattable = AnimationConfig{}
|
||||
_ Chattable = AnswerWebAppQueryConfig{}
|
||||
_ Chattable = AudioConfig{}
|
||||
_ Chattable = BanChatMemberConfig{}
|
||||
_ Chattable = BanChatSenderChatConfig{}
|
||||
_ Chattable = CallbackConfig{}
|
||||
_ Chattable = ChatActionConfig{}
|
||||
_ Chattable = ChatAdministratorsConfig{}
|
||||
_ Chattable = ChatInfoConfig{}
|
||||
_ Chattable = ChatInviteLinkConfig{}
|
||||
_ Chattable = CloseConfig{}
|
||||
_ Chattable = ContactConfig{}
|
||||
_ Chattable = CopyMessageConfig{}
|
||||
_ Chattable = CreateChatInviteLinkConfig{}
|
||||
_ Chattable = DeleteChatPhotoConfig{}
|
||||
_ Chattable = DeleteChatStickerSetConfig{}
|
||||
_ Chattable = DeleteMessageConfig{}
|
||||
_ Chattable = DeleteMyCommandsConfig{}
|
||||
_ Chattable = DeleteWebhookConfig{}
|
||||
_ Chattable = DocumentConfig{}
|
||||
_ Chattable = EditChatInviteLinkConfig{}
|
||||
_ Chattable = EditMessageCaptionConfig{}
|
||||
_ Chattable = EditMessageLiveLocationConfig{}
|
||||
_ Chattable = EditMessageMediaConfig{}
|
||||
_ Chattable = EditMessageReplyMarkupConfig{}
|
||||
_ Chattable = EditMessageTextConfig{}
|
||||
_ Chattable = FileConfig{}
|
||||
_ Chattable = ForwardConfig{}
|
||||
_ Chattable = GameConfig{}
|
||||
_ Chattable = GetChatMemberConfig{}
|
||||
_ Chattable = GetChatMenuButtonConfig{}
|
||||
_ Chattable = GetGameHighScoresConfig{}
|
||||
_ Chattable = GetMyDefaultAdministratorRightsConfig{}
|
||||
_ Chattable = InlineConfig{}
|
||||
_ Chattable = InvoiceConfig{}
|
||||
_ Chattable = KickChatMemberConfig{}
|
||||
_ Chattable = LeaveChatConfig{}
|
||||
_ Chattable = LocationConfig{}
|
||||
_ Chattable = LogOutConfig{}
|
||||
_ Chattable = MediaGroupConfig{}
|
||||
_ Chattable = MessageConfig{}
|
||||
_ Chattable = PhotoConfig{}
|
||||
_ Chattable = PinChatMessageConfig{}
|
||||
_ Chattable = PreCheckoutConfig{}
|
||||
_ Chattable = PromoteChatMemberConfig{}
|
||||
_ Chattable = RestrictChatMemberConfig{}
|
||||
_ Chattable = RevokeChatInviteLinkConfig{}
|
||||
_ Chattable = SendPollConfig{}
|
||||
_ Chattable = SetChatDescriptionConfig{}
|
||||
_ Chattable = SetChatMenuButtonConfig{}
|
||||
_ Chattable = SetChatPhotoConfig{}
|
||||
_ Chattable = SetChatTitleConfig{}
|
||||
_ Chattable = SetGameScoreConfig{}
|
||||
_ Chattable = SetMyDefaultAdministratorRightsConfig{}
|
||||
_ Chattable = ShippingConfig{}
|
||||
_ Chattable = StickerConfig{}
|
||||
_ Chattable = StopMessageLiveLocationConfig{}
|
||||
_ Chattable = StopPollConfig{}
|
||||
_ Chattable = UnbanChatMemberConfig{}
|
||||
_ Chattable = UnbanChatSenderChatConfig{}
|
||||
_ Chattable = UnpinChatMessageConfig{}
|
||||
_ Chattable = UpdateConfig{}
|
||||
_ Chattable = UserProfilePhotosConfig{}
|
||||
_ Chattable = VenueConfig{}
|
||||
_ Chattable = VideoConfig{}
|
||||
_ Chattable = VideoNoteConfig{}
|
||||
_ Chattable = VoiceConfig{}
|
||||
_ Chattable = WebhookConfig{}
|
||||
)
|
||||
|
||||
// Ensure all Fileable types are correct.
|
||||
var (
|
||||
_ Fileable = (*PhotoConfig)(nil)
|
||||
_ Fileable = (*AudioConfig)(nil)
|
||||
_ Fileable = (*DocumentConfig)(nil)
|
||||
_ Fileable = (*StickerConfig)(nil)
|
||||
_ Fileable = (*VideoConfig)(nil)
|
||||
_ Fileable = (*AnimationConfig)(nil)
|
||||
_ Fileable = (*VideoNoteConfig)(nil)
|
||||
_ Fileable = (*VoiceConfig)(nil)
|
||||
_ Fileable = (*SetChatPhotoConfig)(nil)
|
||||
_ Fileable = (*EditMessageMediaConfig)(nil)
|
||||
_ Fileable = (*SetChatPhotoConfig)(nil)
|
||||
_ Fileable = (*UploadStickerConfig)(nil)
|
||||
_ Fileable = (*NewStickerSetConfig)(nil)
|
||||
_ Fileable = (*AddStickerConfig)(nil)
|
||||
_ Fileable = (*MediaGroupConfig)(nil)
|
||||
_ Fileable = (*WebhookConfig)(nil)
|
||||
_ Fileable = (*SetStickerSetThumbConfig)(nil)
|
||||
)
|
||||
|
||||
// Ensure all RequestFileData types are correct.
|
||||
var (
|
||||
_ RequestFileData = (*FilePath)(nil)
|
||||
_ RequestFileData = (*FileBytes)(nil)
|
||||
_ RequestFileData = (*FileReader)(nil)
|
||||
_ RequestFileData = (*FileURL)(nil)
|
||||
_ RequestFileData = (*FileID)(nil)
|
||||
_ RequestFileData = (*fileAttach)(nil)
|
||||
)
|
Loading…
Reference in New Issue
Block a user