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 ¶
- func BroadcastButtonRows(buttons []model.BroadcastButton) [][]InlineButton
- func BroadcastMediaDir(dataDir string) string
- func BroadcastMediaPath(dataDir string, id int64) string
- func IsUnreachable(err error) bool
- func RetryAfter(err error) (time.Duration, bool)
- func SendBackup(ctx context.Context, client *Client, chats []int64, dataDir string, ...) error
- func UserBotLink(botUsername string) string
- func UserDeepLink(botUsername, linkCode string) string
- type APIError
- type BotCommand
- type BroadcastService
- type CallbackQuery
- type Chat
- type ChatMember
- type ChatMemberUpdated
- type Client
- func (c *Client) AnswerCallback(ctx context.Context, id, text string) error
- func (c *Client) CopyMessage(ctx context.Context, toChatID, fromChatID, messageID int64) error
- func (c *Client) CreateForumTopic(ctx context.Context, chatID int64, name string) (int64, error)
- func (c *Client) DeleteForumTopic(ctx context.Context, chatID, threadID int64) error
- func (c *Client) EditMenu(ctx context.Context, chatID, messageID int64, html string, ...) error
- func (c *Client) ForwardMessage(ctx context.Context, toChatID, threadID, fromChatID, messageID int64) error
- func (c *Client) GetChat(ctx context.Context, chatID int64) (*Chat, error)
- func (c *Client) GetChatMember(ctx context.Context, chatID, userID int64) (*ChatMember, error)
- func (c *Client) GetMe(ctx context.Context) (*User, error)
- func (c *Client) GetUpdates(ctx context.Context, offset int64, timeout int) ([]Update, error)
- func (c *Client) GetUpdatesFor(ctx context.Context, offset int64, timeout int, allowed []string) ([]Update, error)
- func (c *Client) PinChatMessage(ctx context.Context, chatID, messageID int64) error
- func (c *Client) ReopenForumTopic(ctx context.Context, chatID, threadID int64) error
- func (c *Client) SendDocument(ctx context.Context, chatID int64, filename, caption string, r io.Reader) error
- func (c *Client) SendDocumentID(ctx context.Context, chatID int64, fileID, caption string, ...) error
- func (c *Client) SendMenu(ctx context.Context, chatID int64, html string, rows [][]InlineButton) error
- func (c *Client) SendMessage(ctx context.Context, chatID int64, html string) error
- func (c *Client) SendPhoto(ctx context.Context, chatID int64, filename, caption string, r io.Reader) error
- func (c *Client) SendPhotoID(ctx context.Context, chatID int64, fileID, caption string, ...) error
- func (c *Client) SendTopic(ctx context.Context, chatID, threadID int64, html string) (int64, error)
- func (c *Client) SetMyCommands(ctx context.Context, cmds []BotCommand) error
- func (c *Client) UploadDocument(ctx context.Context, chatID int64, filename, caption string, ...) (string, error)
- func (c *Client) UploadPhoto(ctx context.Context, chatID int64, filename, caption string, ...) (string, error)
- type Document
- type InlineButton
- type Message
- type Panel
- type PhotoSize
- type Service
- type SupportService
- type Update
- type User
- type UserService
- type WebAppInfo
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
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
BroadcastMediaPath is the attachment's file for one broadcast.
func IsUnreachable ¶ added in v1.2.0
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
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 ¶ added in v0.3.0
UserBotLink is the public bot URL (open /start, no payload).
func UserDeepLink ¶ added in v0.3.0
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
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.
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 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 ¶
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 ¶
AnswerCallback acknowledges a button tap so Telegram stops the spinner.
func (*Client) CopyMessage ¶ added in v1.2.0
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
CreateForumTopic opens a new topic in a forum supergroup and returns its message_thread_id.
func (*Client) DeleteForumTopic ¶ added in v1.2.0
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
GetChat returns a chat's record — the support-group check reads Type and IsForum from it.
func (*Client) GetChatMember ¶ added in v1.2.0
GetChatMember returns one member's record — used to confirm the bot itself is an administrator of the support group.
func (*Client) GetMe ¶
GetMe returns the bot's own account (used to show its @username in the panel).
func (*Client) GetUpdates ¶
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
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
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 (*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 ¶
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
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 (*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 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
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
IsForumService reports whether this is topic housekeeping rather than something a person wrote.
func (*Message) MediaFileID ¶ added in v1.2.0
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.
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.