Merge pull request #434 from go-telegram-bot-api/develop-docs

Add improved documentation
bot-api-6.1
Syfaro 2021-06-08 09:57:22 -07:00 committed by GitHub
commit fb1de2fb48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 831 additions and 0 deletions

1
.gitignore vendored
View File

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

9
book.toml 100644
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"

17
docs/SUMMARY.md 100644
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)

19
docs/changelog.md 100644
View File

@ -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

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

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

View File

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

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.

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

View File

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