diff --git a/.gitignore b/.gitignore index fb5a5e8..eb7a23b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ coverage.out tmp/ +book/ diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..841d5ba --- /dev/null +++ b/book.toml @@ -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" diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 0000000..81e3837 --- /dev/null +++ b/docs/SUMMARY.md @@ -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) diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..83fffa1 --- /dev/null +++ b/docs/changelog.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 diff --git a/docs/examples/README.md b/docs/examples/README.md new file mode 100644 index 0000000..8a6157c --- /dev/null +++ b/docs/examples/README.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. diff --git a/docs/examples/command-handling.md b/docs/examples/command-handling.md new file mode 100644 index 0000000..d1d8b29 --- /dev/null +++ b/docs/examples/command-handling.md @@ -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) + } + } +} +``` diff --git a/docs/examples/inline-keyboard.md b/docs/examples/inline-keyboard.md new file mode 100644 index 0000000..e14ee63 --- /dev/null +++ b/docs/examples/inline-keyboard.md @@ -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) + } + } + } +} +``` diff --git a/docs/examples/keyboard.md b/docs/examples/keyboard.md new file mode 100644 index 0000000..d67a915 --- /dev/null +++ b/docs/examples/keyboard.md @@ -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) + } + } +} +``` diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md new file mode 100644 index 0000000..25c0f92 --- /dev/null +++ b/docs/getting-started/README.md @@ -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. diff --git a/docs/getting-started/files.md b/docs/getting-started/files.md new file mode 100644 index 0000000..952f1f4 --- /dev/null +++ b/docs/getting-started/files.md @@ -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, +} +``` diff --git a/docs/getting-started/important-notes.md b/docs/getting-started/important-notes.md new file mode 100644 index 0000000..d6388aa --- /dev/null +++ b/docs/getting-started/important-notes.md @@ -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. diff --git a/docs/getting-started/library-structure.md b/docs/getting-started/library-structure.md new file mode 100644 index 0000000..56ea6df --- /dev/null +++ b/docs/getting-started/library-structure.md @@ -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. diff --git a/docs/internals/README.md b/docs/internals/README.md new file mode 100644 index 0000000..1d7db5d --- /dev/null +++ b/docs/internals/README.md @@ -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. diff --git a/docs/internals/adding-endpoints.md b/docs/internals/adding-endpoints.md new file mode 100644 index 0000000..a1dc6ab --- /dev/null +++ b/docs/internals/adding-endpoints.md @@ -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. diff --git a/docs/internals/uploading-files.md b/docs/internals/uploading-files.md new file mode 100644 index 0000000..0d222c1 --- /dev/null +++ b/docs/internals/uploading-files.md @@ -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.