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

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.

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.

TypeDescription
stringUsed as a local path to a file
FileIDExisting file ID on Telegram's servers
FileURLURL to file, must be served with expected MIME type
FileReaderUse 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.

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.

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

Work In Progress

  • Remove all methods that return (APIResponse, error).
  • Remove all New*Upload and New*Share methods, replace with New*.
    • Use different file types 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.

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

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.

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

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.

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.