Getting Started
This library is designed as a simple wrapper around the Telegram Bot API. It's encouraged to read Telegram's docs first to get an understanding of what Bots are capable of doing. They also provide some good approaches to solve common problems.
Installing
go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5
A Simple Bot
To walk through the basics, let's create a simple echo bot that replies to your messages repeating what you said. Make sure you get an API token from @Botfather before continuing.
Let's start by constructing a new BotAPI.
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
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.
// 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)
}
}
Congratulations! 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.
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.
Files
Telegram supports specifying files in many different formats. In order to accommodate them all, there are multiple structs and type aliases required.
All of these types implement the RequestFileData
interface.
Type | Description |
---|---|
FilePath | A local path to a file |
FileID | Existing file ID on Telegram's servers |
FileURL | URL to file, must be served with expected MIME type |
FileReader | Use an io.Reader to provide a file. Lazily read to save memory. |
FileBytes | []byte containing file data. Prefer to use FileReader to save memory. |
FilePath
A path to a local file.
file := tgbotapi.FilePath("tests/image.jpg")
FileID
An ID previously uploaded to Telegram. IDs may only be reused by the same bot that received them. Additionally, thumbnail IDs cannot be reused.
file := tgbotapi.FileID("AgACAgIAAxkDAALesF8dCjAAAa_…")
FileURL
A URL to an existing resource. It must be served with a correct MIME type to work as expected.
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.
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.
var data []byte
file := tgbotapi.FileBytes{
Name: "image.jpg",
Bytes: data,
}
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 addUpdateTypeChatMember
to yourAllowedUpdates
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 for more details.
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 re-added for the changes to take effect. You can read more on the Telegram Bot API docs.
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.
Examples
With a better understanding of how the library works, let's look at some more examples showing off some of Telegram's features.
Command Handling
This is a simple example of changing behavior based on a provided command.
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)
}
}
}
Keyboard
This bot shows a numeric keyboard when you send a "open" message and hides it when you send "close" message.
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)
}
}
}
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.
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)
}
}
}
}
Change Log
v5.4.0
- Remove all methods that return
(APIResponse, error)
.- Use the
Request
method instead. - For more information, see Library Structure.
- Use the
- Remove all
New*Upload
andNew*Share
methods, replace withNew*
.- Use different file types to specify if upload or share.
- Rename
UploadFile
toUploadFiles
, 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.
Internals
If you want to contribute to the project, here's some more information about the internal structure of the library.
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.
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.
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.
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.
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.
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.
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.
type DeleteMessageConfig struct {
ChannelUsername string
ChatID int64
MessageID int
+ Delete RequestFileData
+ Thumb RequestFileData
}
Adding another method is pretty simple. We'll always add a file named delete
and add the thumb
file if we have one.
func (config DeleteMessageConfig) files() []RequestFile {
files := []RequestFile{{
Name: "delete",
Data: config.Delete,
}}
if config.Thumb != nil {
files = append(files, RequestFile{
Name: "thumb",
Data: config.Thumb,
})
}
return files
}
And now our files will upload! It will transparently handle uploads whether File
is a FilePath
, FileURL
, FileBytes
, FileReader
, or FileID
.
Base Configs
Certain Configs have repeated elements. For example, many of the items sent to a
chat have ChatID
or ChannelUsername
fields, along with ReplyToMessageID
,
ReplyMarkup
, and DisableNotification
. Instead of implementing all of this
code for each item, there's a BaseChat
that handles it for your Config.
Simply embed it in your struct to get all of those fields.
There's only a few fields required for the MessageConfig
struct after
embedding the BaseChat
struct.
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.
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.
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.
Uploading Files
To make files work as expected, there's a lot going on behind the scenes. Make sure to read through the Files 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
.
func (config DocumentConfig) files() []RequestFile {
// We can have multiple files, so we'll create an array. We also know that
// there always is a document file, so initialize the array with that.
files := []RequestFile{{
Name: "document",
Data: config.File,
}}
// We'll only add a file if we have one.
if config.Thumb != nil {
files = append(files, RequestFile{
Name: "thumb",
Data: config.Thumb,
})
}
return files
}
Telegram also supports the attach://
syntax (discussed more later) for
thumbnails, but there's no reason to make things more complicated.
Dynamic Fields
Of course, not everything can be so simple. Methods like sendMediaGroup
can accept many files, and each file can have custom markup. Using a static
field isn't possible because we need to specify which field is attached to each
item. Telegram introduced the attach://
syntax for this.
Let's follow through creating a new media group with string and file uploads.
First, we start by creating some InputMediaPhoto
.
photo := tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("tests/image.jpg"))
url := tgbotapi.NewInputMediaPhoto(tgbotapi.FileURL("https://i.imgur.com/unQLJIb.jpg"))
This created a new InputMediaPhoto
struct, with a type of photo
and the
media interface that we specified.
We'll now create our media group with the photo and URL.
mediaGroup := NewMediaGroup(ChatID, []interface{}{
photo,
url,
})
A MediaGroupConfig
stores all the media in an array of interfaces. We now
have all 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.