copy from 537c005

This commit is contained in:
Кобелев Андрей Андреевич 2022-06-03 11:47:26 +03:00
commit f86a67eb8f
36 changed files with 10747 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.idea/
coverage.out
tmp/
book/

21
LICENSE.txt Normal file
View 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
View 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
View 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
View 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("<", "&lt;", ">", "&gt;", "&", "&amp;")
} 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

File diff suppressed because it is too large Load Diff

2568
configs.go Normal file

File diff suppressed because it is too large Load Diff

17
docs/SUMMARY.md Normal file
View 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
View 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
View 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.

View 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)
}
}
}
```

View 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
View 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)
}
}
}
```

View 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.

View 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,
}
```

View 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.

View 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
View 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.

View 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.

View 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.

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.belvedersky.ru/common/tgbotapi
go 1.18

927
helpers.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

18
tests/cert.pem Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

28
tests/key.pem Normal file
View 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

Binary file not shown.

BIN
tests/videonote.mp4 Normal file

Binary file not shown.

BIN
tests/voice.ogg Normal file

Binary file not shown.

3299
types.go Normal file

File diff suppressed because it is too large Load Diff

381
types_test.go Normal file
View 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)
)