NicoNex / echotron

An elegant and concurrent library for the Telegram bot API in Go.
https://t.me/s/echotronnews
GNU Lesser General Public License v3.0
372 stars 26 forks source link
awesome-go bot go golang golang-library library telegram telegram-api telegram-bot telegram-bot-api
logo

Language PkgGoDev Go Report Card codecov License Mentioned in Awesome Go Telegram

Echotron is an elegant and concurrent library for the Telegram bot API in Go.

Fetch with

go get github.com/NicoNex/echotron/v3

Example

Simplest implementations

Long polling

package main

import "github.com/NicoNex/echotron/v3"

const token = "MY TELEGRAM TOKEN"

func main() {
    api := echotron.NewAPI(token)

    for u := range echotron.PollingUpdates(token) {
        if u.Message.Text == "/start" {
            api.SendMessage("Hello world", u.ChatID(), nil)
        }
    }
}

Webhook

package main

import "github.com/NicoNex/echotron/v3"

const token = "MY TELEGRAM TOKEN"

func main() {
    api := echotron.NewAPI(token)

    for u := range echotron.WebhookUpdates("https://example.com:443/my_token", token) {
        if u.Message.Text == "/start" {
            api.SendMessage("Hello world", u.ChatID(), nil)
        }
    }
}

For more scalable and recommended implementations see the other examples.

Long Polling

package main

import (
    "log"
    "time"

    "github.com/NicoNex/echotron/v3"
)

// Struct useful for managing internal states in your bot, but it could be of
// any type such as `type bot int64` if you only need to store the chatID.
type bot struct {
    chatID int64
    echotron.API
}

const token = "MY TELEGRAM TOKEN"

// This function needs to be of type 'echotron.NewBotFn' and is called by
// the echotron dispatcher upon any new message from a chatID that has never
// interacted with the bot before.
// This means that echotron keeps one instance of the echotron.Bot implementation
// for each chat where the bot is used.
func newBot(chatID int64) echotron.Bot {
    return &bot{
        chatID,
        echotron.NewAPI(token),
    }
}

// This method is needed to implement the echotron.Bot interface.
func (b *bot) Update(update *echotron.Update) {
    if update.Message.Text == "/start" {
        b.SendMessage("Hello world", b.chatID, nil)
    }
}

func main() {
    // This is the entry point of echotron library.
    dsp := echotron.NewDispatcher(token, newBot)
    for {
        log.Println(dsp.Poll())
        // In case of connection issues wait 5 seconds before trying to reconnect.
        time.Sleep(5 * time.Second)
    }
}

Design

Echotron makes a new instance of the struct bot for each open chat with a Telegram user, channel or group. This allows to:

Please note that the the aforementioned behaviour is dictated by the echotron.Dispatcher object whose usage is not mandatory and for special needs can be ignored and implemented in different ways still keeping all the methods in the echotron.API object.

Echotron is designed to be as similar to the official Telegram API as possible, but there are some things to take into account before starting to work with this library.

Additional examples

Functional approach to state management

package main

import (
    "log"
    "strings"

    "github.com/NicoNex/echotron/v3"
)

// Recursive type definition of the bot state function.
type stateFn func(*echotron.Update) stateFn

type bot struct {
    chatID int64
    state  stateFn
    name   string
    echotron.API
}

const token = "MY TELEGRAM TOKEN"

func newBot(chatID int64) echotron.Bot {
    bot := &bot{
        chatID: chatID,
        API:    echotron.NewAPI(token),
    }
    // We set the default state to the bot.handleMessage method.
    bot.state = bot.handleMessage
    return bot
}

func (b *bot) Update(update *echotron.Update) {
    // Here we execute the current state and set the next one.
    b.state = b.state(update)
}

func (b *bot) handleMessage(update *echotron.Update) stateFn {
    if strings.HasPrefix(update.Message.Text, "/set_name") {
        b.SendMessage("Send me my new name!", b.chatID, nil)
        // Here we return b.handleName since next time we receive a message it
        // will be the new name.
        return b.handleName
    }
    return b.handleMessage
}

func (b *bot) handleName(update *echotron.Update) stateFn {
    b.name = update.Message.Text
    b.SendMessage(fmt.Sprintf("My new name is %q", b.name), b.chatID, nil)
    // Here we return b.handleMessage since the next time we receive a message
    // it will be handled in the default way.
    return b.handleMessage
}

func main() {
    dsp := echotron.NewDispatcher(token, newBot)
    log.Println(dsp.Poll())
}

Self destruction for lower memory footprint

package main

import (
    "log"
    "time"

    "github.com/NicoNex/echotron/v3"
)

type bot struct {
    chatID int64
    echotron.API
}

const token = "MY TELEGRAM TOKEN"

var dsp *echotron.Dispatcher

func newBot(chatID int64) echotron.Bot {
    bot := &bot{
        chatID,
        echotron.NewAPI(token),
    }
    go bot.selfDestruct(time.After(time.Hour))
    return bot
}

func (b *bot) selfDestruct(timech <-chan time.Time) {
    <-timech
    b.SendMessage("goodbye", b.chatID, nil)
    dsp.DelSession(b.chatID)
}

func (b *bot) Update(update *echotron.Update) {
    if update.Message.Text == "/start" {
        b.SendMessage("Hello world", b.chatID, nil)
    }
}

func main() {
    dsp = echotron.NewDispatcher(token, newBot)
    log.Println(dsp.Poll())
}

Webhook

package main

import "github.com/NicoNex/echotron/v3"

type bot struct {
    chatID int64
    echotron.API
}

const token = "MY TELEGRAM TOKEN"

func newBot(chatID int64) echotron.Bot {
    return &bot{
        chatID,
        echotron.NewAPI(token),
    }
}

func (b *bot) Update(update *echotron.Update) {
    if update.Message.Text == "/start" {
        b.SendMessage("Hello world", b.chatID, nil)
    }
}

func main() {
    dsp := echotron.NewDispatcher(token, newBot)
    dsp.ListenWebhook("https://example.com:443/my_bot_token")
}

Webhook with a custom http.Server

This is an example for a custom http.Server which handles your own specified routes and also the webhook route which is specified by ListenWebhook.

package main

import (
    "github.com/NicoNex/echotron/v3"

    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
)

type bot struct {
    chatID int64
    echotron.API
}

const token = "MY TELEGRAM TOKEN"

func newBot(chatID int64) echotron.Bot {
    return &bot{
        chatID,
        echotron.NewAPI(token),
    }
}

func (b *bot) Update(update *echotron.Update) {
    if update.Message.Text == "/start" {
        b.SendMessage("Hello world", b.chatID, nil)
    }
}

func main() {
    termChan := make(chan os.Signal, 1) // Channel for terminating the app via os.Interrupt signal
    signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM)

    mux := http.NewServeMux()
    mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
        // Handle user login
    })
    mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
        // Handle user logout
    })
    mux.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
        // Tell something about your awesome telegram bot
    })

    // Set custom http.Server
    server := &http.Server{Addr: ":8080", Handler: mux}

    go func() {
        <-termChan
        // Perform some cleanup..
        if err := server.Shutdown(context.Background()); err != nil {
            log.Print(err)
        }
    }()

    // Capture the interrupt signal for app termination handling
    dsp := echotron.NewDispatcher(token, newBot)
    dsp.SetHTTPServer(server)
    // Start your custom http.Server with a registered /my_bot_token handler.
    log.Println(dsp.ListenWebhook("https://example.com/my_bot_token"))
}