{"id":15564157,"url":"https://github.com/and3rson/telemux","last_synced_at":"2025-09-11T07:32:00.633Z","repository":{"id":37067794,"uuid":"379249132","full_name":"and3rson/telemux","owner":"and3rson","description":"Flexible message router add-on for go-telegram-bot-api/telegram-bot-api. This is to go-telegram-bot-api as gorilla/mux is to net/http.","archived":false,"fork":false,"pushed_at":"2022-06-17T21:38:36.000Z","size":283,"stargazers_count":42,"open_issues_count":2,"forks_count":6,"subscribers_count":4,"default_branch":"v2","last_synced_at":"2025-01-02T08:03:48.114Z","etag":null,"topics":["go","go-library","go-telegram","go-telegram-bot-api","golang","telegram","telegram-bot"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/and3rson.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-06-22T11:44:26.000Z","updated_at":"2024-10-02T11:48:25.000Z","dependencies_parsed_at":"2022-06-24T21:39:36.979Z","dependency_job_id":null,"html_url":"https://github.com/and3rson/telemux","commit_stats":null,"previous_names":[],"tags_count":56,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/and3rson%2Ftelemux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/and3rson%2Ftelemux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/and3rson%2Ftelemux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/and3rson%2Ftelemux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/and3rson","download_url":"https://codeload.github.com/and3rson/telemux/tar.gz/refs/heads/v2","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":232622286,"owners_count":18551769,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","go-library","go-telegram","go-telegram-bot-api","golang","telegram","telegram-bot"],"created_at":"2024-10-02T16:38:55.238Z","updated_at":"2025-01-05T17:35:17.429Z","avatar_url":"https://github.com/and3rson.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# telemux\nFlexible message router add-on for [go-telegram-bot-api](https://github.com/go-telegram-bot-api/telegram-bot-api) library.\n\n[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct-single.svg)](https://stand-with-ukraine.pp.ua/)\n\n[![GitHub tag](https://img.shields.io/github/tag/and3rson/telemux.svg?sort=semver)](https://GitHub.com/and3rson/telemux/tags/) [![Go Reference](https://pkg.go.dev/badge/github.com/and3rson/telemux.svg)](https://pkg.go.dev/github.com/and3rson/telemux) [![Build Status](https://travis-ci.com/and3rson/telemux.svg?branch=main)](https://travis-ci.com/and3rson/telemux) [![Maintainability](https://api.codeclimate.com/v1/badges/63d82ddd3151594c3765/maintainability)](https://codeclimate.com/github/and3rson/telemux/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/63d82ddd3151594c3765/test_coverage)](https://codeclimate.com/github/and3rson/telemux/test_coverage) [![Go Report Card](https://goreportcard.com/badge/github.com/and3rson/telemux)](https://goreportcard.com/report/github.com/and3rson/telemux) [![stability-stable](https://img.shields.io/badge/stability-stable-green.svg)](https://github.com/emersion/stability-badges#stable) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fand3rson%2Ftelemux.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fand3rson%2Ftelemux?ref=badge_shield)\n\n![Screenshot](./sample_screenshot.png)\n\n\u003c!-- TOC generated with https://luciopaiva.com/markdown-toc/ --\u003e\n# Table of contents\n\n- [Motivation](#motivation)\n- [Features](#features)\n- [Minimal example](#minimal-example)\n- [Documentation](#documentation)\n- [Changelog](#changelog)\n- [Terminology](#terminology)\n  - [Mux](#mux)\n  - [Handlers \u0026 filters](#handlers--filters)\n    - [Combining filters](#combining-filters)\n    - [Reusable handler functions](#reusable-handler-functions)\n  - [Conversations \u0026 persistence](#conversations--persistence)\n  - [Error handling](#error-handling)\n- [Tips \u0026 common pitfalls](#tips--common-pitfalls)\n  - [tgbotapi.Update vs tm.Update confusion](#tgbotapiupdate-vs-tmupdate-confusion)\n  - [Getting user/chat/message object from update](#getting-userchatmessage-object-from-update)\n  - [Properly filtering updates](#properly-filtering-updates)\n\n# Motivation\n\nThis library serves as an addition to the [go-telegram-bot-api](https://github.com/go-telegram-bot-api/telegram-bot-api) library.\nI strongly recommend you to take a look at it since telemux is mostly an extension to it.\n\nPatterns such as handlers, persistence \u0026 filters were inspired by a wonderful [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) library.\n\nThis project is in early beta stage. Contributions are welcome! Feel free to submit an issue if you have any questions, suggestions or simply want to help.\n\n# Features\n\n- Extension for [go-telegram-bot-api](https://github.com/go-telegram-bot-api/telegram-bot-api) library, meaning you'll still use all of its features\n- Designed with statelessness in mind\n- Extensible handler configuration inspired by [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) library\n- Conversations (aka Dialogs) based on finite-state machines (see [./examples/album_conversation/main.go](./examples/album_conversation/main.go))\n- Pluggable persistence for conversations. E. g. you can use database to store the states \u0026 intermediate values of conversations (see [./examples/album_conversation/main.go](./examples/album_conversation/main.go) and [./persistence.go](./persistence.go))\n- Support for GORM as a persistence backend via ![gormpersistence](./gormpersistence) module\n- Flexible handler filtering. E. g. `And(Or(HasText(), HasPhoto()), IsPrivate())` will only accept direct messages containing photo or text (see [./filters.go](./filters.go))\n\n# Minimal example\n\n```go\npackage main\n\nimport (\n    tm \"github.com/and3rson/telemux/v2\"\n    tgbotapi \"github.com/go-telegram-bot-api/telegram-bot-api/v5\"\n    \"os\"\n)\n\nfunc main() {\n    // This part is a boilerplate from go-telegram-bot-api library.\n    bot, _ := tgbotapi.NewBotAPI(os.Getenv(\"TG_TOKEN\"))\n    bot.Debug = true\n    u := tgbotapi.NewUpdate(0)\n    u.Timeout = 60\n    updates, _ := bot.GetUpdatesChan(u)\n\n    // Create a multiplexer with two handlers: one for command and one for all messages.\n    // If a handler cannot handle the update (fails the filter),\n    // multiplexer will proceed to the next handler.\n    mux := tm.NewMux().\n        AddHandler(tm.NewCommandHandler(\n            \"start\",\n            func(u *tm.Update) {\n                bot.Send(tgbotapi.NewMessage(u.Message.Chat.ID, \"Hello! Say something. :)\"))\n            },\n        )).\n        AddHandler(tm.NewHandler(\n            tm.Any(),\n            func(u *tm.Update) {\n                bot.Send(tgbotapi.NewMessage(u.Message.Chat.ID, \"You said: \"+u.Message.Text))\n            },\n        ))\n    // Dispatch all telegram updates to multiplexer\n    for update := range updates {\n        mux.Dispatch(bot, update)\n    }\n}\n```\n\n# Documentation\n\nThe documentation is available [here](https://pkg.go.dev/github.com/and3rson/telemux).\n\nExamples are available [here](./examples/).\n\n# Changelog\n\nChangelog is available [here](./CHANGELOG.md).\n\n# Terminology\n\n## Mux\nMux (multiplexer) is a \"router\" for instances of `tgbotapi.Update`.\n\nIt allows you to register handlers and will take care to choose an appropriate handler based on the incoming update.\n\nIn order to work, you must dispatch messages (that come from go-telegram-bot-api channel):\n\n```go\nmux := tm.NewMux()\n// ...\n// add handlers to mux here\n// ...\nupdates, _ := bot.GetUpdatesChan(u)\nfor update := range updates {\n    mux.Dispatch(bot, update)\n}\n```\n\nYou can also nest Mux instances:\n\n```go\n// See \"Handlers \u0026 filters\" section below for more info on filters.\nmux_a := tm.NewMux().\n    SetGlobalFilter(tm.IsPrivate()).\n    AddHandler(/* ... */).\n    AddHandler(/* ... */)\nmux_b := tm.NewMux().\n    SetGlobalFilter(tm.IsGroupOrSuperGroup()).\n    AddHandler(/* ... */).\n    AddHandler(/* ... */)\nmux = tm.NewMux().\n    AddMux(mux_a).\n    AddMux(mux_b).\n    AddHandler(/* ... */)\n```\n\n## Handlers \u0026 filters\n\nHandler consists of filter and handle-function.\n\nHandler's filter decides whether this handler can handle the incoming update.\nIf so, handle-function is called. Otherwise multiplexer will proceed to the next handler.\n\nFilters are divided in two groups: content filters (starting with \"Has\", such as `HasPhoto()`, `HasAudio()`, `HasSticker()` etc)\nand update type filters (starting with \"Is\", such as `IsEditedMessage()`, `IsInlineQuery()` or `IsGroupOrSuperGroup()`).\n\nThere is also a special filter `Any()` which makes handler accept all updates.\n\nFilters can also be applied to the Mux instance using `mux.SetGlobalFilter(filter)`.\nSuch filters will be called for every update before any other filters.\n\nGeneric handlers can be created with `tm.NewHandler` function, however there are shortcuts for adding update-type-specific handlers:\n\n```go\ntm.NewMessageHandler(tm.HasPhoto(), func(u *tm.Update) { /* ... */ })\n# ...equals to: tm.NewHandler(tm.And(tm.IsMessage(), tm.HasPhoto()), func(u *tm.Update) { /* ... */ })\n\ntm.NewCommandHandler(\"start\", tm.IsPrivate(), func(u *tm.Update) { /* ... */ })\n# ...equals to: tm.NewHandler(tm.And(tm.IsCommandMessage(\"start\"), tm.IsPrivate()), func(u *tm.Update) { /* ... */ })\n\ntm.NewCallbackQueryHandler(nil, func(u *tm.Update) { /* ... */ })\n# ...equals to: tm.NewHandler(tm.IsCallbackQuery(), func(u *tm.Update) { /* ... */ })\n\n# etc.\n```\n\n### Combining filters\n\nFilters can be chained using `And`, `Or`, and `Not` meta-filters. For example:\n\n```go\nmux := tm.NewMux()\n\n// Add handler that accepts photos sent to the bot in a private chat:\nmux.AddHandler(And(tm.IsPrivate(), tm.HasPhoto()), func(u *tm.Update) { /* ... */ })\n\n// Add handler that accepts photos and text messages:\nmux.AddHandler(Or(tm.HasText(), tm.HasPhoto()), func(u *tm.Update) { /* ... */ })\n\n// Since filters are plain functions, you can easily implement them yourself.\n// Below we add handler that allows onle a specific user to call \"/restart\" command:\nmux.AddHandler(tm.NewHandler(\n    tm.And(tm.IsCommandMessage(\"restart\"), func(u *tm.Update) bool {\n        return u.Message.From.ID == 3442691337\n    }),\n    func(u *tm.Update) { /* ... */ },\n))\n```\n\n### Reusable handler functions\n\n`mux.NewHandler` can accept more than one handler function. They are all executed sequentially. The chain can\nbe interrupted by any of them by calling `u.Consume()`.\n\nHere is an example:\n\n```go\nmux.AddHandler(tm.NewHandler(\n    tm.IsCommandMessage(\"do_work\"),\n    func(u *tm.Update) {\n        // Perform necessary check\n        if u.EffectiveUser().ID != 3442691337 { // Boilerplate code that will be copy-pasted way too much\n            u.Bot.Send(tgbotapi.Message(u.EffectiveChat().ID, \"You are not allowed to ask me to work!\"))\n            // Stop handling\n            return\n        }\n        // Perform another check\n        if !u.EffectiveChat().IsPrivate() { // Another boilerplate code\n            u.Bot.Send(tgbotapi.Message(u.EffectiveChat().ID, \"I do not accept commands in group chats. Send me a PM.\"))\n            // Stop handling\n            return\n        }\n        // All checks passed, do some actual work\n        // ...\n    },\n))\n```\n\nTo avoid repeating boilerplate checks like `if user is not \"3442691337\" then send error and stop`, you can move them to separate functions.\n\nThe above code can be rewritten as follows:\n\n```go\n// CheckAdmin is a reusable handler that not only checks for user's ID but marks update as processed as well\nfunc CheckAdmin(u *tm.Update) {\n    if u.EffectiveUser().ID != 3442691337 {\n        u.Bot.Send(tgbotapi.Message(u.EffectiveChat().ID, \"You are not allowed to ask me to work!\"))\n        u.Consume() // Mark update as consumed. Following handler functions will not be called.\n    }\n}\n\n// CheckPrivate is a reusable handler that not only checks for private chat but marks update as processed as well\nfunc CheckPrivate(u *tm.Update) {\n    if !u.EffectiveChat().IsPrivate() {\n        u.Bot.Send(tgbotapi.Message(u.EffectiveChat().ID, \"I do not accept commands in group chats. Send me a PM.\"))\n        u.Consume() // Mark update as consumed. Following handler functions will not be called.\n    }\n}\n\n// ...\n\nmux.AddHandler(tm.NewHandler(\n    tm.IsCommandMessage(\"do_work\"),\n    CheckAdmin,\n    CheckPrivate,\n    func(u *tm.Update) {\n        // Do actual work\n    },\n))\nmux.AddHandler(tm.NewHandler(\n    tm.IsCommandMessage(\"other_command\")),\n    func(u *tm.Update) {\n        // This handler will not fire if one of previous handlers (CheckAdmin or CheckPrivate) consumed the update.\n    },\n))\n```\n\nYou can implement some more complex handler functions, for example, a parametrized one:\n\n```go\n// CheckUserID checks for specific user ID and marks update processed if check fails\nfunc RequireUserID(userID int) tm.HandleFunc {\n    return func(u *tm.Update) {\n        if u.EffectuveUser().ID != userID {\n            u.Bot.Send(tgbotapi.NewMessage(u.EffectiveChat().ID, \"Sorry, I don't know you!\"))\n            u.Consume()\n        }\n    }\n}\n\n// ...\n\nmux.AddHandler(tm.NewHandler(\n    tm.IsCommandMessage(\"do_work\"),\n    RequireUserID(3442691337),\n    func(u *tm.Update) {\n        // Do actual work\n    },\n))\n```\n\n\n## Conversations \u0026 persistence\n\nConversations are handlers on steroids based on the finite-state machine pattern.\n\nThey allow you to have complex dialog interactions with different handlers.\n\nPersistence interface tells conversation where to store \u0026 how to retrieve the current state of the conversation, i. e. which \"step\" the given user is currently at.\n\nTo create a ConversationHandler you need to provide the following:\n\n- `conversationID string` - identifier that distinguishes this conversation from the others.\n\n    The main goal of this identifier is to allow persistence to keep track of different conversation states independently without mixing them together.\n\n- `persistence Persistence` - defines where to store conversation state \u0026 intermediate inputs from the user.\n\n    Without persistence, a conversation would not be able to \"remember\" what \"step\" the user is at.\n\n    Persistence is also useful when you want to collect some data from the user step-by-step).\n\n    Two convenient implementations of `Persistence` are available out of the box: `LocalPersistence` \u0026 `FilePersistence`.\n\n    Telemux also supports GORM persistence. If you use GORM, you can store conversation states \u0026 data in your database by using `GORMPersistence` from a ![gormpersistence](./gormpersistence) module.\n\n- `states StateMap` - defines what handlers to use in which state.\n\n    States are usually strings like \"upload_photo\", \"send_confirmation\", \"wait_for_text\" and describe the \"step\" the user is currently at.\n    Empty string (`\"\"`) should be used as an initial/final state (i. e. if the conversation has not started yet or has already finished.)\n\n    For each state you must provide a slice with at least one Handler. If none of the handlers can handle the update, the default handlers are attempted (see below).\n\n    In order to switch to a different state your Handler must call `u.PersistenceContext.SetState(\"STATE_NAME\") ` replacing STATE_NAME with the name of the state you want to switch into.\n\n    Conversation data can be accessed with `u.PersistenceContext.GetData()` and updated with `u.PersistenceContext.SetData(newData)`.\n\n\n- `defaults []*Handler` - these handlers are \"appended\" to every state.\n\n    Useful to handle commands such as \"/cancel\" or to display some default message.\n\nSee [./examples/album_conversation/main.go](./examples/album_conversation/main.go) for a conversation example.\n\n## Error handling\n\nBy default, panics in handlers are propagated all the way to the top (`Dispatch` method).\n\nIn order to intercept all panics in your handlers globally and handle them gracefully, register your function using `SetRecover`:\n\n```go\nmux := tm.NewMux()\n# ...\nmux.SetRecover(func(u *tm.Update, err error, stackTrace string) {\n    fmt.Printf(\"An error occurred: %s\\n\\nStack trace:\\n%s\", err, stackTrace)\n})\n```\n\n# Tips \u0026 common pitfalls\n\n## tgbotapi.Update vs tm.Update confusion\n\nSince `Update` struct from go-telegram-bot-api already provides most of the functionality, telemux implements its own `Update` struct\nwhich embeds the `Update` from go-telegram-bot-api. Main reason for this is to add some extra convenient methods and include Bot instance\nwith every update.\n\n## Getting user/chat/message object from update\n\nWhen having handlers for wide filters (e. g. `Or(And(HasText(), IsEditedMessage()), IsInlineQuery())`) you may often fall\nin situations when you need to check for multiple user/chat/message attributes. In such situations sender's data may\nbe in one of few places depending on which update has arrived: `u.Message.From`, `u.EditedMessage.From`, or `u.InlineQuery.From`.\nSimilar issue applies to fetching actual chat info or message object from an update.\n\nIn such cases it's highly recommended to use functions such as `EffectiveChat()` (see the [update](./update.go) module for more info):\n\n```go\n// Bad:\nfmt.Println(u.Message.Chat.ID) // u.Message may be nil\n\n// Better, but not so DRY:\nchatId int64\nif u.Message != nil {\n    chatId = u.Message.Chat.ID\n} else if u.EditedMessage != nil {\n    chatId = u.EditedMessage.Chat.ID\n} else if u.CallbackQuery != nil {\n    chatId = u.CallbackQuery.Chat.ID\n} // And so on... Duh.\nfmt.Println(chatId)\n\n// Best:\nchat := u.EffectiveChat()\nif chat != nil {\n    fmt.Println(chat.ID)\n}\n```\n\n## Properly filtering updates\n\nKeep in mind that using content filters such as `HasText()`, `HasPhoto()`, `HasLocation()`, `HasVoice()` etc does not guarantee\nthat the `Update` describes an actual new message. In fact, an `Update` also happens when a user edits his message!\nThus your handler will be executed even if a user just edited one of his messages.\n\nTo avoid situations like these, make sure to use filters such as `IsMessage()`, `IsEditedMessage()`, `IsCallbackQuery()` etc\nin conjunction with content filters. For example:\n\n```go\ntm.NewHandler(HasText(), func(u *tm.Update) { /* ... */ }) // Will handle new messages, updated messages, channel posts \u0026 channel post edits which contain text\ntm.NewHandler(And(IsMessage(), HasText()), func(u *tm.Update) { /* ... */ }) // Will handle new messages that contain text\ntm.NewHandler(And(IsEditedMessage(), HasText()), func(u *tm.Update) { /* ... */ }) // Will handle edited that which contain text\n```\n\nThe only exceptions are `IsCommandMessage(\"...\")` and `IsAnyCommandMessage()` filters. Since it does not make sense to react to edited messages that contain\ncommands, this filter also checks if the update designates a new message and not an edited message, inline query, callback query etc.\nThis means you can safely use `IsCommandMessage(\"my_command\")` without joining it with the `IsMessage()` filter:\n\n```go\nIsCommandMessage(\"my_command\") // OK: IsCommand() already checks for IsMessage()\nAnd(IsCommandMessage(\"start\"), IsMessage()) // IsMessage() is unnecessary\nAnd(IsCommandMessage(\"start\"), Not(IsEditedMessage())) // Not(IsEditedMessage()) is unnecessary\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fand3rson%2Ftelemux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fand3rson%2Ftelemux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fand3rson%2Ftelemux/lists"}