loading...
Cover image for Create a serverless Telegram bot using Go and Vercel

Create a serverless Telegram bot using Go and Vercel

jj profile image Juan Julián Merelo Guervós ・5 min read

So you want to create a Telegram bot 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 and Vercel, 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, obtain a token, and so on, and also create the basic Vercel layout 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 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

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))
    }
}
Enter fullscreen mode Exit fullscreen mode

This will have to go into an api/kekeda_iv.go 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 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. 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:

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)
    }
Enter fullscreen mode Exit fullscreen mode

Telegram will invoke our endpoint with a JSON payload, so we read it from the body and convert it to a tgbotapi.Update 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. 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:

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))
Enter fullscreen mode Exit fullscreen mode

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

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.

Discussion

pic
Editor guide