telegram

package
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: AGPL-3.0 Imports: 26 Imported by: 0

Documentation

Overview

Package telegram is a self-contained Telegram Bot API client plus the panel's admin bot: it long-polls for commands (view/add/remove users) and pushes scheduled backups to the linked admin chat. It deliberately depends only on the standard library — the Bot API is plain HTTP+JSON — so the panel keeps its minimal dependency surface.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BroadcastButtonRows added in v1.2.0

func BroadcastButtonRows(buttons []model.BroadcastButton) [][]InlineButton

BroadcastButtonRows renders the URL buttons for a broadcast (exported so a test send from the panel renders them identically to the real run).

func BroadcastMediaDir added in v1.2.0

func BroadcastMediaDir(dataDir string) string

BroadcastMediaDir is where the panel stores an uploaded attachment until the first recipient turns it into a Telegram file_id.

func BroadcastMediaPath added in v1.2.0

func BroadcastMediaPath(dataDir string, id int64) string

BroadcastMediaPath is the attachment's file for one broadcast.

func IsUnreachable added in v1.2.0

func IsUnreachable(err error) bool

IsUnreachable reports whether Telegram refused permanently because it will never deliver to that chat — the user blocked the bot, never started it, or the account is gone. Exported so the panel can turn it into an instruction the operator can act on instead of a raw API error.

func RetryAfter added in v1.2.0

func RetryAfter(err error) (time.Duration, bool)

RetryAfter reports the cool-off Telegram requested, if err is a 429.

func SendBackup

func SendBackup(ctx context.Context, client *Client, chats []int64, dataDir string,
	manifest backup.Manifest, checkpoint func() error, caption string) error

SendBackup checkpoints the DB, builds a fresh tar.gz of the data directory, and uploads it to each chat as a document. It's shared by the scheduler and the panel's "send test backup" action. A per-chat send failure doesn't abort the rest; the first error is returned.

func UserBotLink(botUsername string) string

UserBotLink is the public bot URL (open /start, no payload).

func UserDeepLink(botUsername, linkCode string) string

UserDeepLink builds a t.me link that binds an existing panel user via a one-time, short-lived bind code (see model.TelegramLinkCodeTTL).

Types

type APIError added in v1.2.0

type APIError struct {
	Code        int
	Description string
	RetryAfter  time.Duration
}

APIError is a non-OK Bot API reply. RetryAfter is the cool-off Telegram asks for on a 429; callers use it to back off for exactly as long as told rather than guessing.

func (*APIError) Error added in v1.2.0

func (e *APIError) Error() string

type BotCommand added in v1.2.0

type BotCommand struct {
	Command     string `json:"command"`     // without the leading slash
	Description string `json:"description"` // shown next to it in the menu
}

BotCommand is one entry of the bot's command menu.

type BroadcastService added in v1.2.0

type BroadcastService struct {
	// contains filtered or unexported fields
}

BroadcastService delivers mass messages through the user bot. It is a poller over the store, not a queue in memory: the recipient list lives in broadcast_targets, so a restart mid-run resumes instead of losing or repeating anything.

Pacing is left entirely to the package rate limiter (ratelimit.go) — every send goes through Client.send, which reserves a slot and honours a 429's retry_after. A second regulator here would only let the two of them together exceed the ceiling each was written to respect.

func NewBroadcast added in v1.2.0

func NewBroadcast(st *store.Store, dataDir string) *BroadcastService

NewBroadcast builds the delivery worker. Call Run to start it.

func (*BroadcastService) Run added in v1.2.0

func (s *BroadcastService) Run(ctx context.Context)

Run delivers running broadcasts until ctx is cancelled.

type CallbackQuery

type CallbackQuery struct {
	ID      string   `json:"id"`
	From    *User    `json:"from"`
	Message *Message `json:"message"`
	Data    string   `json:"data"`
}

CallbackQuery is an inline-button tap (used for the delete confirmation).

type Chat

type Chat struct {
	ID      int64  `json:"id"`
	Type    string `json:"type"`     // "private" | "group" | "supergroup" | "channel"
	Title   string `json:"title"`    // group name (empty for private chats)
	IsForum bool   `json:"is_forum"` // supergroup with Topics enabled
}

type ChatMember added in v1.2.0

type ChatMember struct {
	Status          string `json:"status"` // "creator" | "administrator" | "member" | ...
	CanManageTopics bool   `json:"can_manage_topics"`
}

ChatMember is the subset of a member record the support-group check reads: the bot must be an administrator with CanManageTopics to open a thread per user.

type ChatMemberUpdated added in v1.2.0

type ChatMemberUpdated struct {
	Chat          Chat       `json:"chat"`
	NewChatMember ChatMember `json:"new_chat_member"`
}

ChatMemberUpdated is a change to somebody's membership of a chat.

func (*ChatMemberUpdated) InChat added in v1.2.0

func (u *ChatMemberUpdated) InChat() bool

InChat reports whether the subject is still a member after this change.

func (*ChatMemberUpdated) IsAdmin added in v1.2.0

func (u *ChatMemberUpdated) IsAdmin() bool

IsAdmin reports whether the subject can act as an administrator.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to one bot (identified by its token). It's cheap to construct and safe for use from the single bot loop plus the occasional handler-driven send.

func NewClient

func NewClient(token string) *Client

NewClient builds a client for the given bot token. The HTTP timeout comfortably exceeds the long-poll window so GetUpdates isn't cut off mid-wait.

func (*Client) AnswerCallback

func (c *Client) AnswerCallback(ctx context.Context, id, text string) error

AnswerCallback acknowledges a button tap so Telegram stops the spinner.

func (*Client) CopyMessage added in v1.2.0

func (c *Client) CopyMessage(ctx context.Context, toChatID, fromChatID, messageID int64) error

CopyMessage sends a copy of a message with no "forwarded from" header — used for the admin's reply, so the user sees it as coming from the bot rather than from a group they can't see.

func (*Client) CreateForumTopic added in v1.2.0

func (c *Client) CreateForumTopic(ctx context.Context, chatID int64, name string) (int64, error)

CreateForumTopic opens a new topic in a forum supergroup and returns its message_thread_id.

func (*Client) DeleteForumTopic added in v1.2.0

func (c *Client) DeleteForumTopic(ctx context.Context, chatID, threadID int64) error

DeleteForumTopic removes a topic. Used to clean up after opening one that could not be recorded: an unrecorded topic is unreachable forever, and Telegram offers no way to list a bot's topics to find it later.

func (*Client) EditMenu

func (c *Client) EditMenu(ctx context.Context, chatID, messageID int64, html string, rows [][]InlineButton) error

EditMenu replaces the text + inline keyboard of an existing message, so the bot's menus navigate in place instead of stacking new messages.

func (*Client) ForwardMessage added in v1.2.0

func (c *Client) ForwardMessage(ctx context.Context, toChatID, threadID, fromChatID, messageID int64) error

ForwardMessage forwards a message into a chat, optionally into a forum topic (threadID 0 = the General thread). Forwarding keeps the "from" attribution.

func (*Client) GetChat added in v1.2.0

func (c *Client) GetChat(ctx context.Context, chatID int64) (*Chat, error)

GetChat returns a chat's record — the support-group check reads Type and IsForum from it.

func (*Client) GetChatMember added in v1.2.0

func (c *Client) GetChatMember(ctx context.Context, chatID, userID int64) (*ChatMember, error)

GetChatMember returns one member's record — used to confirm the bot itself is an administrator of the support group.

func (*Client) GetMe

func (c *Client) GetMe(ctx context.Context) (*User, error)

GetMe returns the bot's own account (used to show its @username in the panel).

func (*Client) GetUpdates

func (c *Client) GetUpdates(ctx context.Context, offset int64, timeout int) ([]Update, error)

GetUpdates long-polls for messages and callback queries past offset.

func (*Client) GetUpdatesFor added in v1.2.0

func (c *Client) GetUpdatesFor(ctx context.Context, offset int64, timeout int, allowed []string) ([]Update, error)

GetUpdatesFor is GetUpdates for a bot that needs a different update set.

func (*Client) PinChatMessage added in v1.2.0

func (c *Client) PinChatMessage(ctx context.Context, chatID, messageID int64) error

PinChatMessage pins a message (the user card at the top of their topic). Pinned silently — the admins are already notified by the forwarded message itself.

func (*Client) ReopenForumTopic added in v1.2.0

func (c *Client) ReopenForumTopic(ctx context.Context, chatID, threadID int64) error

ReopenForumTopic re-opens a closed topic. Admins close a thread as the natural "handled" gesture, and the relay keeps one thread per user forever — without a reopen, that user's support would be dead from then on.

func (*Client) SendDocument

func (c *Client) SendDocument(ctx context.Context, chatID int64, filename, caption string, r io.Reader) error

SendDocument uploads a file to a chat as a document, with an optional caption.

func (*Client) SendDocumentID added in v1.2.0

func (c *Client) SendDocumentID(ctx context.Context, chatID int64, fileID, caption string, rows [][]InlineButton) error

func (*Client) SendMenu

func (c *Client) SendMenu(ctx context.Context, chatID int64, html string, rows [][]InlineButton) error

SendMenu sends an HTML message with an inline keyboard (rows of buttons). Pass a nil/empty rows for a plain message.

func (*Client) SendMessage

func (c *Client) SendMessage(ctx context.Context, chatID int64, html string) error

SendMessage sends a plain HTML-formatted message (no buttons). HTML parse mode lets the bot bold headers; all dynamic text must be escaped by the caller (esc).

func (*Client) SendPhoto

func (c *Client) SendPhoto(ctx context.Context, chatID int64, filename, caption string, r io.Reader) error

SendPhoto uploads an image to a chat (shown inline), with an optional HTML caption. Used to deliver the subscription QR code.

func (*Client) SendPhotoID added in v1.2.0

func (c *Client) SendPhotoID(ctx context.Context, chatID int64, fileID, caption string, rows [][]InlineButton) error

SendPhotoID / SendDocumentID send an already-uploaded file by its file_id, with an optional HTML caption and inline keyboard.

func (*Client) SendTopic added in v1.2.0

func (c *Client) SendTopic(ctx context.Context, chatID, threadID int64, html string) (int64, error)

SendTopic posts an HTML message into a forum topic and returns its message ID (so the caller can pin it). threadID 0 posts to the General thread.

func (*Client) SetMyCommands added in v1.2.0

func (c *Client) SetMyCommands(ctx context.Context, cmds []BotCommand) error

SetMyCommands publishes the bot's command menu. Without it a command nobody was told about is a command nobody uses — which matters for an opt-out that has to be findable to count as one.

func (*Client) UploadDocument added in v1.2.0

func (c *Client) UploadDocument(ctx context.Context, chatID int64, filename, caption string, rows [][]InlineButton, r io.Reader) (string, error)

func (*Client) UploadPhoto added in v1.2.0

func (c *Client) UploadPhoto(ctx context.Context, chatID int64, filename, caption string, rows [][]InlineButton, r io.Reader) (string, error)

UploadPhoto / UploadDocument send a file and return the file_id Telegram assigned it, so the same file can be re-sent to everyone else with SendPhotoID / SendDocumentID instead of being uploaded again per recipient.

type Document added in v1.2.0

type Document struct {
	FileID string `json:"file_id"`
}

type InlineButton

type InlineButton struct {
	Text         string      `json:"text"`
	CallbackData string      `json:"callback_data,omitempty"`
	URL          string      `json:"url,omitempty"`     // URL button (mutually exclusive with callback_data)
	WebApp       *WebAppInfo `json:"web_app,omitempty"` // opens a Mini App in-place (private chats only)
}

InlineButton is one inline-keyboard button (label + callback payload).

type Message

type Message struct {
	MessageID       int64       `json:"message_id"`
	From            *User       `json:"from"`
	Chat            Chat        `json:"chat"`
	Text            string      `json:"text"`
	MessageThreadID int64       `json:"message_thread_id"`
	Caption         string      `json:"caption"`
	Photo           []PhotoSize `json:"photo"`
	Document        *Document   `json:"document"`

	// Forum service messages. Telegram emits one of these when an admin creates,
	// renames, closes or reopens a topic; they carry a real sender and the topic's
	// thread id, so without them housekeeping looks exactly like a reply to relay.
	ForumTopicCreated  *struct{} `json:"forum_topic_created"`
	ForumTopicEdited   *struct{} `json:"forum_topic_edited"`
	ForumTopicClosed   *struct{} `json:"forum_topic_closed"`
	ForumTopicReopened *struct{} `json:"forum_topic_reopened"`
}

Message is the subset of a Telegram message the bot acts on. MessageThreadID is the forum topic a group message belongs to — the support relay keys on it to tell which user's thread an admin is answering in.

func (*Message) Body added in v1.2.0

func (m *Message) Body() string

Body is the text a message carries, wherever it lives — plain text for a text message, the caption for one carrying media.

func (*Message) IsForumService added in v1.2.0

func (m *Message) IsForumService() bool

IsForumService reports whether this is topic housekeeping rather than something a person wrote.

func (*Message) MediaFileID added in v1.2.0

func (m *Message) MediaFileID() string

MediaFileID returns the id of the file this message carries, or "" when it has none. For photos Telegram sends every rendition it made; the largest is the one worth re-sending, and the order it arrives in is documented only loosely, so the biggest is picked rather than the last.

type Panel

type Panel interface {
	ListUsers() ([]model.User, error)
	CreateUser(ctx context.Context, name string, dataLimit, expireAt int64) (*model.User, error)
	DeleteUser(ctx context.Context, id int64) error
	SetUserEnabled(ctx context.Context, id int64, enabled bool) error
	ResetTraffic(ctx context.Context, id int64) error
	BackupManifest() backup.Manifest
	Location() *time.Location

	// Billing (no-op surface unless tariffs are enabled).
	ListTariffPlans(includeDisabled bool) ([]model.TariffPlan, error)
	ApplyPlanToUser(ctx context.Context, userID, planID int64, extendFromCurrent bool) error
	PlanName(planID int64) string
	RequestPlanPayment(ctx context.Context, userID, planID int64) (*model.PaymentOrder, string, error)
	// CreateRegisteredUser signs a new user up (active, for the open/invite modes).
	CreateRegisteredUser(ctx context.Context, name string) (*model.User, error)
	// RequestRegistration records a moderated signup (no user yet); ApproveRegistration
	// Request creates the user, RejectRegistrationRequest drops it.
	RequestRegistration(ctx context.Context, chatID int64, name string) (bool, error)
	RegistrationPending(chatID int64) bool
	ApproveRegistrationRequest(ctx context.Context, reqID int64) error
	RejectRegistrationRequest(ctx context.Context, reqID int64) error
	// ActivePaidPlan reports the user's active paid plan (nil = none), and
	// CancelUserPlan drops it to the free plan. Together they gate plan switching:
	// while a paid plan is active only renewal or cancellation is allowed.
	ActivePaidPlan(u model.User) *model.TariffPlan
	CancelUserPlan(ctx context.Context, userID int64) error

	// Automatic payment providers (no-op surface unless configured).
	PaymentMethods() []string
	ProviderLabel(key string) string
	StartPlanPayment(ctx context.Context, userID, planID int64, provider string) (*model.PaymentOrder, error)
	SetUserNotifier(fn func(chatID int64, html string))
	SetAdminNotifier(fn func(html string))
	SetAdminModerationNotifier(fn func(reqID int64, name, plan string))

	// Audit hooks for the actions the bots perform directly on the store.
	// (Unlinking is deliberately absent: it is an operator action in the panel, not
	// something a user can do to themselves from the bot.)
	AuditTelegramLinked(ctx context.Context, id int64, username string)
}

Panel is the slice of the core Manager the bot drives. Defining it here (rather than importing core) keeps the dependency one-way — core never imports telegram — and makes the bot trivially testable with a fake. Every mutating method takes a context: it carries the actor for the audit log, so a change made from a bot is attributed to the Telegram admin (or the VPN user) who made it rather than to "system". See the actor package.

type PhotoSize added in v1.2.0

type PhotoSize struct {
	FileID string `json:"file_id"`
	Width  int    `json:"width"`
	Height int    `json:"height"`
}

PhotoSize / Document carry the file_id Telegram assigns to an uploaded file. Re-sending by that id costs no upload, which is the difference between one transfer and one per recipient when the same image goes out to a whole audience.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is the running bot: it long-polls for updates and pushes scheduled backups. The whole UI is inline buttons — the only text a user ever types is "/start <code>" to link, and (when prompted) a new user's name. It re-reads settings every cycle, so enabling/disabling, rotating the token, or changing the schedule all take effect without a restart.

func New

func New(panel Panel, st *store.Store, dataDir string) *Service

New builds the bot service. It does not start polling — call Run.

func (*Service) Run

func (s *Service) Run(ctx context.Context)

Run drives the bot until ctx is cancelled: it long-polls for updates and, in a sibling goroutine, fires scheduled backups. When the bot is disabled or has no token it idles, re-checking periodically.

type SupportService added in v1.2.0

type SupportService struct {
	// contains filtered or unexported fields
}

SupportService is the support relay: a third bot that carries messages between a user's private chat and a per-user topic in the operator's forum supergroup.

It is deliberately a separate bot from the user bot. Inside the user bot every incoming message would need a "support request, or just tapping around the menu?" decision; here there is no menu, no plans and no registration, so everything sent is unambiguously a request and nothing has to be guessed. Relaying by message id also means screenshots, documents and voice notes pass through without the bot parsing a single attachment.

func NewSupport added in v1.2.0

func NewSupport(panel Panel, st *store.Store) *SupportService

NewSupport builds the support relay bot. Call Run to start polling.

func (*SupportService) Run added in v1.2.0

func (s *SupportService) Run(ctx context.Context)

Run long-polls the support bot until ctx is cancelled. Settings are re-read every cycle, so enabling/disabling or rotating the token takes effect without a restart.

Polling starts as soon as a TOKEN exists — before support is enabled and before a group is chosen. That ordering is the whole point: the bot has to be listening to notice which groups it was added to, and requiring the group first made the one piece of information the operator was missing impossible for the bot to supply. Relaying still waits until support is fully configured.

type Update

type Update struct {
	UpdateID int64          `json:"update_id"`
	Message  *Message       `json:"message"`
	Callback *CallbackQuery `json:"callback_query"`
	// MyChatMember fires when the bot itself is added to, promoted in, or removed
	// from a chat. It is how the support bot learns a group's id without anyone
	// having to look one up by hand. Delivered only when asked for explicitly.
	MyChatMember *ChatMemberUpdated `json:"my_chat_member"`
}

Update is one polled event: a message or a callback-query (inline button tap).

type User

type User struct {
	ID        int64  `json:"id"`
	Username  string `json:"username"`
	FirstName string `json:"first_name"`
	// LangCode is the IETF tag of the client's interface language. Nothing reads it
	// yet — the panel has no i18n — but it is recorded per subscriber so the data
	// exists if broadcasts ever need to be segmented by language.
	LangCode string `json:"language_code"`
}

User / Chat carry only the identifiers the bot needs.

type UserService added in v0.3.0

type UserService struct {
	// contains filtered or unexported fields
}

UserService is the public VPN user bot: open registration, personal subscription menu, and optional deep-link binding for accounts created in the panel.

func NewUser added in v0.3.0

func NewUser(panel Panel, st *store.Store) *UserService

NewUser builds the public user bot. Call Run to start polling.

func (*UserService) Run added in v0.3.0

func (s *UserService) Run(ctx context.Context)

Run long-polls the user bot until ctx is cancelled.

type WebAppInfo added in v0.11.0

type WebAppInfo struct {
	URL string `json:"url"`
}

WebAppInfo points a web_app button at an HTTPS page opened inside Telegram as a Mini App. Inline web_app buttons need no @BotFather domain setup, but work only in private chats and require an https:// URL.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL