Merge pull request #434 from go-telegram-bot-api/develop-docs
Add improved documentationbot-api-6.1
commit
fb1de2fb48
|
@ -1,3 +1,4 @@
|
||||||
.idea/
|
.idea/
|
||||||
coverage.out
|
coverage.out
|
||||||
tmp/
|
tmp/
|
||||||
|
book/
|
||||||
|
|
|
@ -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"
|
|
@ -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)
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
## v5
|
||||||
|
|
||||||
|
**Work In Progress**
|
||||||
|
|
||||||
|
- 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
|
|
@ -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.
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,112 @@
|
||||||
|
# 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@develop
|
||||||
|
```
|
||||||
|
|
||||||
|
It's currently suggested to use the develop branch. While there may be breaking
|
||||||
|
changes, it has a number of features not yet available on master.
|
||||||
|
|
||||||
|
## 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.
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Files
|
||||||
|
|
||||||
|
Telegram supports specifying files in many different formats. In order to
|
||||||
|
accommodate them all, there are multiple structs and type aliases required.
|
||||||
|
|
||||||
|
| Type | Description |
|
||||||
|
| ------------ | ------------------------------------------------------------------------- |
|
||||||
|
| `string` | Used as 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. |
|
||||||
|
|
||||||
|
## `string`
|
||||||
|
|
||||||
|
A path to a local file.
|
||||||
|
|
||||||
|
```go
|
||||||
|
file := "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,
|
||||||
|
}
|
||||||
|
```
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1,195 @@
|
||||||
|
# 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 interface{}
|
||||||
|
+ Thumb interface{}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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",
|
||||||
|
File: config.Delete,
|
||||||
|
}}
|
||||||
|
|
||||||
|
if config.Thumb != nil {
|
||||||
|
files = append(files, RequestFile{
|
||||||
|
Name: "thumb",
|
||||||
|
File: config.Thumb,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And now our files will upload! It will transparently handle uploads whether File is a string with a path to a file, `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.
|
|
@ -0,0 +1,108 @@
|
||||||
|
# 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",
|
||||||
|
File: config.File,
|
||||||
|
}}
|
||||||
|
|
||||||
|
// We'll only add a file if we have one.
|
||||||
|
if config.Thumb != nil {
|
||||||
|
files = append(files, RequestFile{
|
||||||
|
Name: "thumb",
|
||||||
|
File: 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("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.
|
||||||
|
|
||||||
|
## Library Processing
|
||||||
|
|
||||||
|
If at some point in the future new upload types are required, let's talk about
|
||||||
|
where the current types are used.
|
||||||
|
|
||||||
|
Upload types are defined in `configs.go`. Where possible, type aliases are
|
||||||
|
preferred. Structs can be used when multiple fields are required.
|
||||||
|
|
||||||
|
The main usage of the upload types happens in `UploadFiles`. It switches on each
|
||||||
|
file's type in order to determine how to upload it. Files that aren't uploaded
|
||||||
|
(file IDs, URLs) are converted back into strings and passed through as strings
|
||||||
|
into the correct field. Uploaded types are processed as needed (opening files,
|
||||||
|
etc.) and written into the form using a copy approach in a goroutine to reduce
|
||||||
|
memory usage.
|
||||||
|
|
||||||
|
In addition to `UploadFiles`, there's more processing of upload types in the
|
||||||
|
`prepareInputMediaParam` and `prepareInputMediaFile` functions. These look at
|
||||||
|
the `InputMedia` types to determine which files are uploaded and which are
|
||||||
|
passed through as strings. They only need to be aware of which files need to be
|
||||||
|
replaced with `attach://` fields.
|
Loading…
Reference in New Issue