Create a serverless Telegram bot using Go and Vercel
Tags: serverless, faas, go, bots
Published: 2020-11-13 07:15:33

---
title: Create a serverless Telegram bot using Go and Vercel
published: true
description: A step by step guide to creating a single function that can act as a Telegram bot, and deploy it to Vercel
tags: serverless, FaaS, golang, bots
cover_image: https://live.staticflickr.com/65535/50596338682_a2a5f8d069_o_d.jpg
---
So you want to create a [Telegram bot](https://core.telegram.org/bots) and host it somewhere you don't have to pay much attention to. Here's a quick and (somewhat) dirty solution for you, using [Go](https://golang.org) and [Vercel](https://vercel.com), the serverless hosting solution.

## Do your homework

Well, you already know you have to open your Vercel (and also GitHub) account (there's a [good intro to it](https://dev.to/sumitkolhe/going-serveless-with-vercel-5b4o)), [obtain a token](https://telegra.ph/Awesome-Telegram-Bot-11-11), and so on, and also create [the basic Vercel layout](https://vercel.com/docs/serverless-functions/supported-languages#go) Won't get into that, I told you it was going to be fast.

## Here's the function

What I wanted to do is to have a Telegram bot that told my [students](https://jj.github.io/IV) how long until the next assignment. Incidentally, I wanted to provide them with an example of serverless functions, so I ate my own dogfood and eventually crafted this

```go
package handler

import (
	"fmt"
	"net/http"
	"time"
	"encoding/json"
	"github.com/go-telegram-bot-api/telegram-bot-api"
	"log"
	"io/ioutil"
)

type Hito struct {
	URI  string
	Title string
	fecha time.Time
}

type Response struct {
	Msg string `json:"text"`
	ChatID int64 `json:"chat_id"`
	Method string `json:"method"`
}

var hitos = []Hito {
	Hito {
		URI: "0.Repositorio",
		Title: "Datos básicos y repo",
		fecha: time.Date(2020, time.September, 29, 11, 30, 0, 0, time.UTC),
	},
	Hito {
		URI: "1.Infraestructura",
		Title: "HUs y entidad principal",
		fecha: time.Date(2020, time.October, 6, 11, 30, 0, 0, time.UTC),
	},
	Hito {
		URI: "2.Tests",
		Title: "Tests iniciales",
		fecha: time.Date(2020, time.October, 16, 11, 30, 0, 0, time.UTC),
	},
	Hito {
		URI: "3.Contenedores",
		Title: "Contenedores",
		fecha: time.Date(2020, time.October, 26, 11, 30, 0, 0, time.UTC),
	},
	Hito {
		URI: "4.CI",
		Title: "Integración continua",
		fecha: time.Date(2020, time.November, 6, 23, 59, 0, 0, time.UTC),
	},
	Hito {
		URI: "5.Serverless",
		Title: "Trabajando con funciones serverless",
		fecha: time.Date(2020, time.November, 24, 11, 30, 0, 0, time.UTC),
	},

}


func Handler(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	body, _ := ioutil.ReadAll(r.Body)
	var update tgbotapi.Update
	if err := json.Unmarshal(body,&update); err != nil {
		log.Fatal("Error en el update →", err)
	}
	log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text)
	currentTime := time.Now()
	var next int
	var queda time.Duration
	for indice, hito := range hitos {
		if ( hito.fecha.After( currentTime ) ) {
			next = indice
			queda = hito.fecha.Sub( currentTime )
		}
	}
	if update.Message.IsCommand() {
		text := ""
		if ( next == 0 ) {
			text = "Ninguna entrega próxima"
		} else {

			switch update.Message.Command() {
			case "kk":
				text = queda.String()
			case "kekeda":
				text = fmt.Sprintf( "→ Próximo hito %s\n🔗 https://jj.github.io/IV/documentos/proyecto/%s\n📅 %s",
					hitos[next].Title,
					hitos[next].URI,
					hitos[next].fecha.String(),
				)
			default:
				text = "Usa /kk para lo que queda para el próximo hito, /kekeda para + detalle"
			}
		}
		data := Response{ Msg: text,
			Method: "sendMessage",
			ChatID: update.Message.Chat.ID }

		msg, _ := json.Marshal( data )
		log.Printf("Response %s", string(msg))
		w.Header().Add("Content-Type", "application/json")
		fmt.Fprintf(w,string(msg))
	}
}
```
This will have to go into an [`api/kekeda_iv.go`](https://github.com/JJ/vercel-kekeda) file, which you will eventually deploy running `vercel`.

First, I needed to put all data into the same file. Go packages are tricky to handle external modules, so it does not really matter. All dates are into its own data structure. I can just change that data structure and redeploy every time I need to add a new date.

The package needs to be `handler`, and Vercel is going to call the `Handler` function, receiving a request in `r` and a pointer to a [`http.ResponseWriter`](https://golang.org/pkg/net/http/) in `w`. We'll need to read from the later, write to the former.

Fortunately, Telegram has a way of binding, through the token, a bot to an endpoint via [webhooks](https://core.telegram.org/bots/webhooks). That's what we're going to do. Instead of having our program poll Telegram for updates, Telegram is going to dutifully call that endpoint with a payload. We obtain that payload in these bunch of lines:

```go
defer r.Body.Close()
	body, _ := ioutil.ReadAll(r.Body)
	var update tgbotapi.Update
	if err := json.Unmarshal(body,&update); err != nil {
		log.Fatal("Error en el update →", err)
	}
```

Telegram will invoke our endpoint with a JSON payload, so we read it from the body and convert it to a [`tgbotapi.Update`](https://github.com/go-telegram-bot-api/telegram-bot-api) data structure provided by the Go Telegram Bot API. That's quite convenient, and is in fact the only thing we use from that library.

A big part of the rest of the function is our "business" logic: find out the next date, and put the index to the array in a variable we can use later.

And then comes the Telegram logic: we only get into it if there's actually a Telegram "slash" command, of which we define two. That's pretty much boilerplate, get more info, for instance in [this tutorial](https://medium.com/swlh/build-a-telegram-bot-in-go-in-9-minutes-e06ad38acef1). By the end of that, we have the message we have to send into the `text` variable.

## Call back

So we need to create a response, which we do in the last few lines:

```go
data := Response{ Msg: text,
			Method: "sendMessage",
			ChatID: update.Message.Chat.ID }

		msg, _ := json.Marshal( data )
		log.Printf("Response %s", string(msg))
		w.Header().Add("Content-Type", "application/json")
		fmt.Fprintf(w,string(msg))
```

We need to put the response into a JSON message; so we first create a `Response` struct. That struct needs to have a `Method` field that will tell Telegram what to do, a `Msg` with the message, and a `ChatID` that will tell Telegram where to send it. You can put more things into that, but that's the bare essentials. In the next line we convert it to a JSON `[]byte`, which we stringify and log.

Next line's the catch. You need to tell the type of the response, so you set the header to the correct type: `w.Header().Add("Content-Type", "application/json")`

> This took me a while and [this question in StackOverflow](https://stackoverflow.com/questions/64809915/telegram-bot-in-go-not-receiving-the-answer-included-in-a-responsewriter) to realize. Problem is, there's no log or response-to-response to check if something goes wrong.

Finally, you print the JSON to that writer. And that's it!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/navffc5li9sthlvq0x3g.png)

> Provided you have deployed it, and set the hook. You need  set the hook to a vercel endpoint just the once.

If you need to create more bots, just deploy more functions. Vercel has a generous free tier, so it's ideal for the kind of purposes I'm using it, teaching people.