Initial work on improving docs.

bot-api-6.1
Syfaro 2020-07-26 17:22:16 -05:00
parent 2f7211a708
commit 5be25266b5
12 changed files with 674 additions and 0 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.idea/
coverage.out
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"

15
docs/SUMMARY.md 100644
View File

@ -0,0 +1,15 @@
# Summary
- [Getting Started](./getting-started/README.md)
* [Library Structure](./getting-started/library-structure.md)
* [Files](./getting-started/files.md)
- [Examples](./examples/README.md)
* [Command Handling](./examples/command-handling.md)
* [Keyboard](./examples/keyboard.md)
- [Change Log]()
# Contributing
- [Internals](./internals/README.md)
* [Adding Endpoints](./internals/adding-endpoints.md)
* [Uploading Files](./internals/uploading-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"
"github.com/go-telegram-bot-api/telegram-bot-api"
)
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,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"
"github.com/go-telegram-bot-api/telegram-bot-api"
)
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@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"
"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,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.