Documentation
¶
Overview ¶
Package telegram provides Telegram bot integration.
Index ¶
- Constants
- Variables
- func CleanupMedia(maxAge time.Duration) (int, error)
- func DeletePlan(slugPrefix string) (string, error)
- func DownloadDocument(bot *Bot, chatID int64, fileID, fileName string) (string, error)
- func DownloadPhoto(bot *Bot, chatID int64, fileIDs []string) (string, error)
- func DownloadVoice(bot *Bot, chatID int64, fileID string) (string, error)
- func EscapeMarkdown(text string) string
- func FormatResponse(text string) ([]string, error)
- func IsFatalAPIError(err error) bool
- func MediaDir() (string, error)
- func MostRecentPlan() (string, string, error)
- func ReadPlan(slugPrefix string) (string, string, error)
- func ResolveMediaPath(path string) (string, error)
- func RetryWithBackoff(fn func() error, maxAttempts int, baseDelay time.Duration) error
- func Slugify(desc string) string
- func ValidateConfig(cfg TelegramConfig) error
- type Bot
- func (b *Bot) AnswerCallbackQuery(callbackID string, text string, showAlert bool) error
- func (b *Bot) CheckDailyBudget(tokens int64) error
- func (b *Bot) DailyTokenUsage() (used int64, limit int64)
- func (b *Bot) DeleteMessage(chatID int64, messageID int) error
- func (b *Bot) DownloadFile(filePath string) ([]byte, error)
- func (b *Bot) EditMessageText(chatID int64, messageID int, text string, opts *SendOpts) error
- func (b *Bot) GetFile(fileID string) (*File, error)
- func (b *Bot) GetMe() (*User, error)
- func (b *Bot) GetUpdates(offset int, timeout int) ([]Update, error)
- func (b *Bot) GetUpdatesContext(ctx context.Context, offset int, timeout int) ([]Update, error)
- func (b *Bot) SendChatAction(chatID int64, action string) error
- func (b *Bot) SendDocument(chatID int64, path string, caption string, opts *SendOpts) (*Message, error)
- func (b *Bot) SendMessage(chatID int64, text string, opts *SendOpts) (*Message, error)
- func (b *Bot) SendMessageContext(ctx context.Context, chatID int64, text string, opts *SendOpts) (*Message, error)
- func (b *Bot) SendPhoto(chatID int64, path string, caption string, opts *SendOpts) (*Message, error)
- func (b *Bot) SendVoice(chatID int64, path string, caption string, opts *SendOpts) (*Message, error)
- func (b *Bot) SetDailyTokenBudget(budget int64)
- func (b *Bot) SetFallbackURLs(urls []string) error
- func (b *Bot) SetFileBaseURL(url string)
- func (b *Bot) SetLogger(l Logger)
- func (b *Bot) SetMyCommands(commands []BotCommand) error
- func (b *Bot) StopRetries()
- type BotCommand
- type CallbackQuery
- type Chat
- type ChatMember
- type ChatSession
- type CommandDescriptor
- type Document
- type FallbackTransport
- type File
- type FileResponse
- type ForwardOrigin
- type GetUpdatesResponse
- type Handler
- func (h *Handler) DeleteApprover(chatID int64)
- func (h *Handler) GetApprover(chatID int64) *TelegramApprover
- func (h *Handler) HandleUpdate(upd Update)
- func (h *Handler) SendResponse(chatID int64, text string, replyToMessageID int)
- func (h *Handler) SetApprover(chatID int64, a *TelegramApprover)
- func (h *Handler) SetLogger(l Logger)
- type HandlerConfig
- type HealthServer
- type InlineKeyboardButton
- type InlineKeyboardMarkup
- type LogLevel
- type Logger
- type Message
- type MessageEntity
- type PhotoSize
- type PlanInfo
- type Poller
- type SendMessageResponse
- type SendOpts
- type SessionInfo
- type SessionManager
- func (sm *SessionManager) AppendMessage(chatID int64, role string, content string) error
- func (sm *SessionManager) ArchiveAndDelete(chatID int64) error
- func (sm *SessionManager) Delete(chatID int64) error
- func (sm *SessionManager) DeleteClarifyChannel(chatID int64)
- func (sm *SessionManager) GetClarifyChannel(chatID int64) (chan string, bool)
- func (sm *SessionManager) GetOrCreate(chatID int64) (*ChatSession, error)
- func (sm *SessionManager) ListSessions(limit int) ([]SessionInfo, error)
- func (sm *SessionManager) Load(chatID int64) (*ChatSession, error)
- func (sm *SessionManager) PrunePlans(days int) (int, error)
- func (sm *SessionManager) PruneSessions(days int) (int, error)
- func (sm *SessionManager) ResumeSession(chatID int64, sessionID string) (*ChatSession, error)
- func (sm *SessionManager) Save(chatID int64, messages []llm.Message) error
- func (sm *SessionManager) SetClarifyChannel(chatID int64, ch chan string)
- type TelegramApprover
- func (a *TelegramApprover) Cancel()
- func (a *TelegramApprover) HandleCallback(data string, userID int64) bool
- func (a *TelegramApprover) IsTrusted(cls danger.RiskClass) bool
- func (a *TelegramApprover) PromptCommand(cls danger.RiskClass, cmd, description string) error
- func (a *TelegramApprover) PromptOperation(op danger.ToolOperation) error
- func (a *TelegramApprover) ResetTrust()
- func (a *TelegramApprover) SetLogger(l Logger)
- func (a *TelegramApprover) SetTrustAll(enabled bool)
- type TelegramConfig
- type TelegramError
- type Update
- type UpdateResponse
- type User
- type UserProfilePhotos
- type Voice
- type WebhookInfo
Constants ¶
const ( ParseModeMarkdownV2 = "MarkdownV2" ParseModeHTML = "HTML" )
Parse mode constants for SendOpts.
const DefaultMaxDownloadSize = 5 * 1024 * 1024
DefaultMaxDownloadSize is the per-file cap for Telegram downloads (5 MiB) when the operator does not configure an explicit value.
Variables ¶
var DefaultCommands []CommandDescriptor
DefaultCommands is the built-in list of slash commands. Populated via init() to avoid initialization cycle with handler functions that reference the variable.
Functions ¶
func CleanupMedia ¶
CleanupMedia removes media files older than maxAge from the downloaded media directory (~/.odek/media/). Returns the number of files removed. Non-existent directories and subdirectories are silently skipped.
func DeletePlan ¶
DeletePlan removes a plan file by slug prefix match. Uses the same matching logic as ReadPlan. Returns the slug of the deleted plan.
func DownloadDocument ¶
DownloadDocument downloads a document/file from Telegram and saves it to the media directory. Returns the local file path. The filename is prefixed with the chat ID so per-chat quotas can be enforced.
func DownloadPhoto ¶
DownloadPhoto downloads the largest available size of a photo and saves it to the media directory. Uses the last (largest) PhotoSize in the slice. Returns the local file path. Saved as "photo_chat<chatID>_<fileID>.jpg".
func DownloadVoice ¶
DownloadVoice downloads a voice message from Telegram and saves it to the media directory. Returns the local file path. The file is saved as "voice_chat<chatID>_<fileID>.ogg" using a content-hash-safe truncation of the fileID.
func EscapeMarkdown ¶
EscapeMarkdown escapes all reserved MarkdownV2 characters. Characters inside code spans (`...`) and code blocks (```...```) are NOT escaped.
func FormatResponse ¶
FormatResponse converts odek markdown output to Telegram MarkdownV2. It splits the result into chunks of at most 4096 bytes at paragraph boundaries.
func IsFatalAPIError ¶
IsFatalAPIError reports whether a Telegram API error is fatal (should not be retried). Errors with status codes 401 (Unauthorized), 403 (Forbidden), and 409 (Conflict — duplicate polling instance) are fatal. Uses errors.As for type-safe code extraction instead of string matching.
func MediaDir ¶
MediaDir returns the directory where downloaded media files are stored. Defaults to ~/.odek/media/. Creates the directory if it doesn't exist.
func MostRecentPlan ¶
MostRecentPlan returns the slug and full content of the most recently modified plan file. Returns an error if no plans exist.
func ReadPlan ¶
ReadPlan loads a plan by slug prefix match. Returns the slug, content, and any error. If multiple plans match the prefix, the first exact match is preferred, then the first prefix match. Returns an error if no match is found.
func ResolveMediaPath ¶ added in v1.8.0
ResolveMediaPath validates and resolves an agent-supplied media path before it is uploaded to Telegram.
Allowed base directories are:
- the current working directory,
- the odek media directory (~/.odek/media), and
- the system temporary directory.
The input path is expanded to an absolute, cleaned path, any symlinks are resolved, and the final resolved path must be a regular file inside one of the allowed base directories. The final path component itself must not be a symlink. This prevents a prompt-injected agent from exfiltrating arbitrary files such as /home/user/.ssh/id_rsa via MEDIA:... or send_message(file=...).
func RetryWithBackoff ¶
RetryWithBackoff retries the given function up to maxAttempts times with exponential backoff starting at baseDelay. Returns nil on success or the last error if all attempts fail.
func Slugify ¶
Slugify converts a description into a filesystem-safe slug. Rules: lowercase, max 60 chars, only [a-z0-9] and hyphens, multiple hyphens collapsed, no leading/trailing hyphens.
func ValidateConfig ¶
func ValidateConfig(cfg TelegramConfig) error
ValidateConfig checks that the configuration values are within acceptable ranges and returns an error describing the first problem found.
Types ¶
type Bot ¶
type Bot struct {
Token string
BaseURL string
FileBaseURL string
Client *http.Client
DailyTokenBudget int64
MaxDownloadSize int64 // 0 = unlimited; >0 = per-file byte cap
MediaQuotaPerChat int64 // 0 = disabled; >0 = per-chat quota in bytes
// contains filtered or unexported fields
}
Bot represents a Telegram Bot API client.
func NewBot ¶
NewBot creates a new Bot with the given token and a default HTTP client with a 60-second timeout (generous for long-polling getUpdates calls).
func (*Bot) AnswerCallbackQuery ¶
AnswerCallbackQuery sends an answer to a callback query.
func (*Bot) CheckDailyBudget ¶
CheckDailyBudget reads the current daily token usage tracking file, adds the given number of tokens, and returns an error if the total exceeds the configured DailyTokenBudget. If the budget is zero (unset), no check is performed and nil is returned.
The read-modify-write cycle is protected by an advisory file lock so concurrent odek processes and goroutines cannot clobber the counter.
func (*Bot) DailyTokenUsage ¶
DailyTokenUsage returns the current token usage and budget limit. Returns (0, 0) when the budget is not configured.
func (*Bot) DeleteMessage ¶
DeleteMessage deletes a message previously sent by the bot. Requires the bot to have can_delete_messages permission in groups/supergroups.
func (*Bot) DownloadFile ¶
DownloadFile downloads a file from Telegram's file server and returns its raw bytes. If MaxDownloadSize is set (>0), the read is capped and an error is returned when the file exceeds the limit.
func (*Bot) EditMessageText ¶
EditMessageText edits a previously sent message in the given chat. The messageID must identify an existing message sent by the bot. Supports SendOpts for parse_mode. Returns an error if the message hasn't changed (Telegram "Bad Request: message is not modified").
func (*Bot) GetFile ¶
GetFile returns basic information about a file and prepares it for downloading.
func (*Bot) GetUpdates ¶
GetUpdates retrieves incoming updates using long polling (no context). Deprecated: Use GetUpdatesContext for context-aware cancellation.
func (*Bot) GetUpdatesContext ¶
GetUpdates retrieves incoming updates using long polling with context support. When ctx is cancelled, the HTTP request is aborted immediately.
func (*Bot) SendChatAction ¶
SendChatAction tells the user that the bot is doing something on their behalf (e.g., "typing"). The action is shown as a status in the chat for ~5 seconds or until the next message is sent. Callers should re-send every 4 seconds for long-running operations.
func (*Bot) SendDocument ¶
func (b *Bot) SendDocument(chatID int64, path string, caption string, opts *SendOpts) (*Message, error)
SendDocument sends a document from a file path to the specified chat. opts may contain ReplyToMessageID to reply to a specific message.
func (*Bot) SendMessage ¶
SendMessage sends a text message to the specified chat.
func (*Bot) SendMessageContext ¶ added in v1.2.0
func (b *Bot) SendMessageContext(ctx context.Context, chatID int64, text string, opts *SendOpts) (*Message, error)
SendMessageContext is like SendMessage but aborts the request (and its retry backoff) when ctx is cancelled — used by the scheduler so a stuck delivery doesn't block graceful shutdown.
func (*Bot) SendPhoto ¶
func (b *Bot) SendPhoto(chatID int64, path string, caption string, opts *SendOpts) (*Message, error)
SendPhoto sends a photo from a file path to the specified chat. opts may contain ReplyToMessageID to reply to a specific message.
func (*Bot) SendVoice ¶
func (b *Bot) SendVoice(chatID int64, path string, caption string, opts *SendOpts) (*Message, error)
SendVoice sends a voice note from a file path to the specified chat. opts may contain ReplyToMessageID to reply to a specific message.
func (*Bot) SetDailyTokenBudget ¶
SetDailyTokenBudget sets the daily token usage budget for the bot. When non-zero, CheckDailyBudget will reject token usage that exceeds this limit within a calendar day.
func (*Bot) SetFallbackURLs ¶
SetFallbackURLs configures fallback Telegram API endpoints to try if the primary endpoint is unreachable. Each URL should be a base API URL such as "https://api.telegram.org" (without the /bot<token> suffix). The fallback transport rewrites the host on each request, keeping the original path (which includes the token).
Fallback URLs are validated: they must be HTTPS telegram.org hosts or loopback addresses. This prevents the bot token from leaking to arbitrary third-party endpoints.
func (*Bot) SetFileBaseURL ¶
SetFileBaseURL overrides the file download base URL (defaults to https://api.telegram.org/file/bot<token>). Tests can use this to point file downloads at a test server.
func (*Bot) SetLogger ¶
SetLogger sets the logger for this bot. If nil, a NopLogger is used (no-op).
func (*Bot) SetMyCommands ¶
func (b *Bot) SetMyCommands(commands []BotCommand) error
SetMyCommands sets the list of the bot's commands.
func (*Bot) StopRetries ¶ added in v0.54.0
func (b *Bot) StopRetries()
StopRetries signals any in-flight doJSON retry loops to abort. Safe to call multiple times. After calling, doJSON will return a cancelled error instead of sleeping through the full backoff.
type BotCommand ¶
type BotCommand struct {
Command string `json:"command,omitempty"`
Description string `json:"description,omitempty"`
}
BotCommand represents a bot command.
func CommandDescriptors ¶
func CommandDescriptors() []BotCommand
CommandDescriptors returns a slice of BotCommand suitable for the Telegram SetMyCommands API.
type CallbackQuery ¶
type CallbackQuery struct {
ID string `json:"id,omitempty"`
From *User `json:"from,omitempty"`
Message *Message `json:"message,omitempty"`
Data string `json:"data,omitempty"`
}
CallbackQuery represents an incoming callback query from a callback button in an inline keyboard.
type Chat ¶
type Chat struct {
ID int64 `json:"id"`
Type string `json:"type,omitempty"`
Title string `json:"title,omitempty"`
Username string `json:"username,omitempty"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
}
Chat represents a Telegram chat.
type ChatMember ¶
type ChatMember struct {
Status string `json:"status,omitempty"`
User *User `json:"user,omitempty"`
}
ChatMember is a placeholder for future chat member information.
type ChatSession ¶
type ChatSession struct {
ChatID int64
SessionID string
Messages []llm.Message
CreatedAt time.Time
LastActive time.Time
TurnCount int
}
ChatSession represents a single Telegram chat's agent conversation.
type CommandDescriptor ¶
type CommandDescriptor struct {
Command string
Description string
Handler func(args string) (string, error)
}
CommandDescriptor describes a slash command and its handler.
func FindCommand ¶
func FindCommand(name string) *CommandDescriptor
FindCommand returns the command descriptor with the matching name, or nil.
type Document ¶
type Document struct {
FileID string `json:"file_id,omitempty"`
FileUniqueID string `json:"file_unique_id,omitempty"`
FileName string `json:"file_name,omitempty"`
MimeType string `json:"mime_type,omitempty"`
FileSize int `json:"file_size,omitempty"`
}
Document represents a general file (as opposed to photos, voice messages, etc.).
type FallbackTransport ¶
type FallbackTransport struct {
PrimaryURL string
FallbackURLs []string
Timeout time.Duration
Client *http.Client
}
FallbackTransport is an http.RoundTripper that tries multiple Telegram API endpoints in order, falling back to the next on failure.
func NewFallbackTransport ¶
func NewFallbackTransport(fallbackURLs []string) (*FallbackTransport, error)
NewFallbackTransport creates a FallbackTransport with the given fallback URLs. The primary URL defaults to https://api.telegram.org and the timeout defaults to 30 seconds.
It returns an error if any fallback URL is untrusted, because the bot token is sent in the request path and untrusted endpoints would receive it.
func (*FallbackTransport) Do ¶
Do tries the request against each configured URL (primary first, then fallbacks) and returns the first successful response or a combined error.
func (*FallbackTransport) RoundTrip ¶
RoundTrip implements http.RoundTripper. It tries the request against each configured URL (primary first, then fallbacks) and returns the first successful response or a combined error.
func (*FallbackTransport) TestEndpoints ¶
func (ft *FallbackTransport) TestEndpoints() map[string]string
TestEndpoints pings the /getMe endpoint on each configured URL and returns a map of URL to status ("ok" or "error: ...").
func (*FallbackTransport) WrapBot ¶
func (ft *FallbackTransport) WrapBot(bot *Bot) *Bot
WrapBot wraps the given Bot so that all API calls go through this FallbackTransport. It replaces the bot's HTTP client transport, allowing the transport to rewrite the API endpoint on each request.
type File ¶
type File struct {
FileID string `json:"file_id,omitempty"`
FileUniqueID string `json:"file_unique_id,omitempty"`
FileSize int `json:"file_size,omitempty"`
FilePath string `json:"file_path,omitempty"`
}
File represents a file ready to be downloaded from Telegram.
type FileResponse ¶
FileResponse is the Telegram API response for getFile.
type ForwardOrigin ¶ added in v1.8.0
type ForwardOrigin struct {
Type string `json:"type,omitempty"`
SenderUser *User `json:"sender_user,omitempty"`
SenderUserName string `json:"sender_user_name,omitempty"`
Chat *Chat `json:"chat,omitempty"`
MessageID int `json:"message_id,omitempty"`
}
ForwardOrigin describes the original sender of a forwarded message. It is present when a message was forwarded from another chat or user.
type GetUpdatesResponse ¶
GetUpdatesResponse is the Telegram API response for getUpdates.
type Handler ¶
type Handler struct {
Bot *Bot
Config HandlerConfig
// OnTextMessage is called when a plain text message is received.
// forwarded is true when the message was forwarded from another chat or
// user; callers should treat the text as crossing an external trust boundary.
// Returns the response text (may be empty).
// Should run asynchronously if it starts the agent loop — callers
// should dispatch to a goroutine to avoid blocking the update loop.
OnTextMessage func(chatID int64, messageID int, text string, forwarded bool, userID int64) (string, error)
// OnCallbackQuery is called when a callback query is received and
// it was NOT handled by the TelegramApprover. Returns the response
// text (may be empty).
OnCallbackQuery func(chatID int64, callbackData string) (string, error)
// OnCommand is called when a bot command (e.g. /start) is received.
// userID is the Telegram user who sent the command.
// Returns the response text (may be empty).
OnCommand func(chatID int64, messageID int, command string, args string, userID int64) (string, error)
// OnVoiceMessage is called when a voice message is received.
// Returns the response text (may be empty).
// fileID is the Telegram file ID of the voice message in OGG format.
// userID is the Telegram user who sent the voice message.
// Callers should use DownloadVoice to save the file locally.
OnVoiceMessage func(chatID int64, messageID int, fileID string, userID int64) (string, error)
// OnPhotoMessage is called when a photo message is received.
// Returns the response text (may be empty).
// fileIDs contains all available sizes (last = largest).
// Callers should use DownloadPhoto with the last element.
// caption is the optional text the user attached to the photo (may be empty).
// userID is the Telegram user who sent the photo message.
OnPhotoMessage func(chatID int64, messageID int, fileIDs []string, caption string, userID int64) (string, error)
// OnDocumentMessage is called when a document/file message is received.
// Returns the response text (may be empty).
// fileID is the Telegram file ID. Callers should use DownloadDocument
// and pass the document's fileName to save the file locally.
// userID is the Telegram user who sent the document message.
OnDocumentMessage func(chatID int64, messageID int, fileID string, fileName string, userID int64) (string, error)
// OnError is called when a processing error occurs.
OnError func(chatID int64, err error)
// contains filtered or unexported fields
}
Handler routes incoming Telegram updates to the appropriate callback based on message type. It is the bridge between the raw Telegram API and the agent.
func NewHandler ¶
NewHandler creates a Handler with the given bot and default settings.
func (*Handler) DeleteApprover ¶
DeleteApprover removes the TelegramApprover for the given chat ID. Thread-safe. Used when a session is reset or ends.
func (*Handler) GetApprover ¶
func (h *Handler) GetApprover(chatID int64) *TelegramApprover
GetApprover retrieves the TelegramApprover for the given chat ID. Returns nil if no approver is registered. Thread-safe.
func (*Handler) HandleUpdate ¶
HandleUpdate routes an incoming Telegram update to the appropriate handler. Recovers from panics in handler callbacks to prevent a single bad update from crashing the entire bot loop.
SECURITY: every handler routed here must enforce isAllowed itself before acting (handleMessage and handleCallback both do). When adding a new update type, gate it the same way — callback queries once bypassed authorization because their handler skipped this check.
func (*Handler) SendResponse ¶
SendResponse sends a response text to the given chat. It handles MEDIA: prefix, chunking, MarkdownV2 formatting, and retry logic. If replyToMessageID is non-zero, the response is sent as a reply to that message.
func (*Handler) SetApprover ¶
func (h *Handler) SetApprover(chatID int64, a *TelegramApprover)
SetApprover stores a TelegramApprover for the given chat ID. Thread-safe: safe to call from any goroutine.
type HandlerConfig ¶
type HandlerConfig struct {
AllowedChats []int64 // non-empty: only these chat IDs pass; empty: no chat filter
BotUsername string // for @mention detection in groups (without @)
MaxMsgLength int // default: 4096
AllowedUsers []int64 // non-empty: only these user IDs pass; empty: no user filter
// AllowAllUsers must be true to permit access when BOTH allowlists are
// empty. Default false = fail-closed (deny everyone) so an unconfigured
// handler never silently allows all users. See ValidateConfig.
AllowAllUsers bool
}
HandlerConfig controls which messages the Handler processes.
type HealthServer ¶
type HealthServer struct {
// contains filtered or unexported fields
}
HealthServer serves a lightweight HTTP health check endpoint. It reports bot liveness and uptime for monitoring systems.
func NewHealthServer ¶
func NewHealthServer(addr string) *HealthServer
NewHealthServer creates a HealthServer listening on the given address. Use "127.0.0.1:9090" or "0.0.0.0:9090". Empty string disables the server.
func (*HealthServer) ServeHTTP ¶
func (hs *HealthServer) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler.
func (*HealthServer) SetLogger ¶
func (hs *HealthServer) SetLogger(l Logger)
SetLogger sets the logger. If nil, a NopLogger is used.
func (*HealthServer) SetReady ¶
func (hs *HealthServer) SetReady()
SetReady marks the health server as ready (polling has started). Thread-safe: safe to call from any goroutine.
type InlineKeyboardButton ¶
type InlineKeyboardButton struct {
Text string `json:"text,omitempty"`
CallbackData string `json:"callback_data,omitempty"`
URL string `json:"url,omitempty"`
}
InlineKeyboardButton represents one button of an inline keyboard.
type InlineKeyboardMarkup ¶
type InlineKeyboardMarkup struct {
InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"`
}
InlineKeyboardMarkup represents an inline keyboard that appears next to the message.
type LogLevel ¶
type LogLevel int
LogLevel represents the severity of a log message.
func ParseLogLevel ¶
ParseLogLevel converts a string to a LogLevel. Defaults to LogInfo.
type Logger ¶
type Logger interface {
Debug(msg string, fields ...any)
Info(msg string, fields ...any)
Warn(msg string, fields ...any)
Error(msg string, fields ...any)
// With returns a child logger that adds the given fields to every message.
With(fields ...any) Logger
}
Logger is the logging interface for the Telegram integration. Fields are alternating key-value pairs: log.Info("msg", "chat_id", 42, "error", err)
func NewFileLogger ¶
NewFileLogger creates a Logger that writes to the given file path. If path is empty, writes to stderr. Format:
2006-01-02T15:04:05.000Z [INFO] telegram: message key=value
The file is opened in append mode, created if missing. Thread-safe via sync.Mutex.
func NewNopLogger ¶
func NewNopLogger() Logger
NewNopLogger returns a Logger that discards all messages (for tests).
type Message ¶
type Message struct {
ID int `json:"message_id"`
From *User `json:"from,omitempty"`
Chat *Chat `json:"chat,omitempty"`
Date int `json:"date,omitempty"`
Text string `json:"text,omitempty"`
Entities []MessageEntity `json:"entities,omitempty"`
Photo []PhotoSize `json:"photo,omitempty"`
Voice *Voice `json:"voice,omitempty"`
Document *Document `json:"document,omitempty"`
Caption string `json:"caption,omitempty"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
ForwardFrom *User `json:"forward_from,omitempty"`
ForwardDate int `json:"forward_date,omitempty"`
ForwardOrigin *ForwardOrigin `json:"forward_origin,omitempty"`
}
Message represents a Telegram message.
type MessageEntity ¶
type MessageEntity struct {
Type string `json:"type,omitempty"`
Offset int `json:"offset,omitempty"`
Length int `json:"length,omitempty"`
URL string `json:"url,omitempty"`
User *User `json:"user,omitempty"`
}
MessageEntity represents a special entity in a text message.
type PhotoSize ¶
type PhotoSize struct {
FileID string `json:"file_id,omitempty"`
FileUniqueID string `json:"file_unique_id,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
FileSize int `json:"file_size,omitempty"`
}
PhotoSize represents one size of a photo or a file/sticker thumbnail.
type PlanInfo ¶
type PlanInfo struct {
Slug string // filename without .md extension
Path string // full filesystem path
ModTime time.Time // last modification time
Preview string // first line or ~80 chars of content
}
PlanInfo is a lightweight summary of a plan file.
type Poller ¶
type Poller struct {
Bot *Bot
Offset int
Interval time.Duration
Timeout int
// contains filtered or unexported fields
}
Poller implements long-polling for Telegram updates.
func NewPoller ¶
NewPoller creates a new Poller with sensible defaults. Offset starts at 0, Interval is 1s, Timeout is 30s.
func (*Poller) Poll ¶
Poll performs a single long-poll cycle. It calls GetUpdates with the current offset and timeout, advances the offset past the highest update ID received, and returns the updates (may be empty on timeout). Returns ctx.Err() if the context is cancelled.
func (*Poller) Start ¶
Start begins the infinite long-polling loop. It calls Poll() repeatedly, sending each received update to the channel. On empty result (timeout), sleeps for Interval then retries. On error, sleeps with exponential backoff (interval * 2^consecutiveErrors, capped at 60 * interval), logs to the logger, but continues. Backoff resets to zero after a successful poll. When ctx is cancelled, closes the channel and returns ctx.Err().
type SendMessageResponse ¶
SendMessageResponse is the Telegram API response for sendMessage.
type SendOpts ¶
type SendOpts struct {
ParseMode string `json:"parse_mode,omitempty"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"`
ReplyToMessageID int `json:"reply_to_message_id,omitempty"`
}
SendOpts contains optional parameters for SendMessage.
type SessionInfo ¶
type SessionInfo struct {
ID string // session ID (e.g. "tg-12345")
Task string // first user message or label
CreatedAt time.Time // when the session started
UpdatedAt time.Time // last activity
Turns int // number of user turns
}
SessionInfo is a lightweight summary of a session for listing.
type SessionManager ¶
type SessionManager struct {
Store *session.Store
Cache map[int64]*ChatSession
Mu sync.RWMutex
BaseDir string
SessionTTL time.Duration
// contains filtered or unexported fields
}
SessionManager manages per-chat Telegram sessions backed by the existing session.Store. Each Telegram chat gets its own session identified by "tg-<chatID>". An in-memory cache avoids redundant disk reads.
func NewSessionManager ¶
func NewSessionManager(store *session.Store, ttl time.Duration) *SessionManager
NewSessionManager creates a new SessionManager backed by the given store. The ttl parameter controls how long a session is considered active since its last use. If ttl is 0, a default of 24h is used. The cache map is initialized to empty.
func (*SessionManager) AppendMessage ¶
func (sm *SessionManager) AppendMessage(chatID int64, role string, content string) error
AppendMessage adds a single message (role + content) to the chat session's message list and saves the updated session. It uses GetOrCreate to ensure the session exists.
func (*SessionManager) ArchiveAndDelete ¶ added in v0.58.8
func (sm *SessionManager) ArchiveAndDelete(chatID int64) error
ArchiveAndDelete archives the current session to a timestamped file, then removes it from cache and store. This preserves the conversation history for later reference while starting fresh on the next message. The archived session is saved with an ID like "tg-<chatID>-<YYYYMMDD>-<HHMMSS>" so it can be browsed via `odek session list`.
func (*SessionManager) Delete ¶
func (sm *SessionManager) Delete(chatID int64) error
Delete removes the chat session from both the cache and the backing store. Idempotent — returns nil if the session doesn't exist.
func (*SessionManager) DeleteClarifyChannel ¶
func (sm *SessionManager) DeleteClarifyChannel(chatID int64)
DeleteClarifyChannel removes the clarify channel for a chat (called after clarify completes or times out).
func (*SessionManager) GetClarifyChannel ¶
func (sm *SessionManager) GetClarifyChannel(chatID int64) (chan string, bool)
GetClarifyChannel retrieves the clarify response channel for a chat. Returns false if no channel is set (clarify not in progress).
func (*SessionManager) GetOrCreate ¶
func (sm *SessionManager) GetOrCreate(chatID int64) (*ChatSession, error)
GetOrCreate returns the ChatSession for the given chatID. Checks the in-memory cache first, then the backing session store, and only creates a new empty session as a last resort. This ensures conversations survive bot restarts without the user needing to ask for resume explicitly.
func (*SessionManager) ListSessions ¶
func (sm *SessionManager) ListSessions(limit int) ([]SessionInfo, error)
ListSessions returns metadata for all sessions in the backing store, sorted by most-recent-first, limited to `limit` entries. If limit <= 0, all sessions are returned.
func (*SessionManager) Load ¶
func (sm *SessionManager) Load(chatID int64) (*ChatSession, error)
Load retrieves a ChatSession from the cache first, then from the backing store. If the session exists in the store but not in cache, it is loaded from disk, converted to a ChatSession, and cached. Returns nil, nil if the session is not found anywhere — callers should use GetOrCreate to create a new session in that case.
func (*SessionManager) PrunePlans ¶
func (sm *SessionManager) PrunePlans(days int) (int, error)
PrunePlans deletes plan files (~/.odek/plans/*.md) older than `days` days. Plans don't have a formal store yet — this scans the directory and checks file modification times. Returns the number of plan files removed. If the plans directory doesn't exist, returns 0, nil.
func (*SessionManager) PruneSessions ¶
func (sm *SessionManager) PruneSessions(days int) (int, error)
PruneSessions deletes sessions that haven't been updated in `days` days or more. Returns the number of sessions removed.
func (*SessionManager) ResumeSession ¶
func (sm *SessionManager) ResumeSession(chatID int64, sessionID string) (*ChatSession, error)
ResumeSession loads a session from the backing store and binds it to the given chatID. This replaces any existing session for that chat. sessionID can be a partial prefix match — the first matching session (by ID prefix or task contains) is used. Returns the new ChatSession or an error if no matching session is found.
func (*SessionManager) Save ¶
func (sm *SessionManager) Save(chatID int64, messages []llm.Message) error
Save persists the given messages for a chat session to both the cache and the backing session.Store. It updates LastActive, increments TurnCount, and writes a full session.Session to the store.
func (*SessionManager) SetClarifyChannel ¶
func (sm *SessionManager) SetClarifyChannel(chatID int64, ch chan string)
SetClarifyChannel stores a clarify response channel for the given chat.
type TelegramApprover ¶
type TelegramApprover struct {
// ChatID is the Telegram chat where approval prompts are sent.
ChatID int64
// contains filtered or unexported fields
}
TelegramApprover implements danger.Approver by sending approval requests via Telegram inline keyboards. The agent loop calls PromptCommand which:
- Sends the command details + [Approve] [Deny] [Trust] keyboard
- Blocks on a channel waiting for the user's callback response
- Returns nil on approve/trust, error on deny/timeout
The poller goroutine calls HandleCallback when a callback query arrives. The callback data encodes the action and request ID so HandleCallback can wake the correct blocked goroutine.
Thread-safe: PromptCommand and HandleCallback are safe to call concurrently.
func NewTelegramApprover ¶
func NewTelegramApprover(bot *Bot, chatID, userID int64) *TelegramApprover
NewTelegramApprover creates a TelegramApprover for the given chat and originating user. Callbacks are only accepted from userID; use 0 to allow callbacks from any user (legacy behavior, not recommended for groups).
func (*TelegramApprover) Cancel ¶
func (a *TelegramApprover) Cancel()
Cancel interrupts any pending PromptCommand by closing the cancel channel. Safe to call multiple times — subsequent calls are no-ops.
func (*TelegramApprover) HandleCallback ¶
func (a *TelegramApprover) HandleCallback(data string, userID int64) bool
HandleCallback processes a callback query from an inline keyboard approval. It parses the callback data, looks up the pending request, and unblocks the waiting goroutine. Callbacks are only accepted from the originating user (or any user if userID is unknown/0). Returns true if the callback was handled (was an approval callback), false if it should fall through to OnCallbackQuery.
func (*TelegramApprover) IsTrusted ¶
func (a *TelegramApprover) IsTrusted(cls danger.RiskClass) bool
IsTrusted reports whether the given risk class is already trusted for this session. Primarily used for testing.
func (*TelegramApprover) PromptCommand ¶
func (a *TelegramApprover) PromptCommand(cls danger.RiskClass, cmd, description string) error
PromptCommand sends an approval request with inline keyboard and waits for the user to respond. Returns nil on approve/trust, error on deny/timeout.
func (*TelegramApprover) PromptOperation ¶
func (a *TelegramApprover) PromptOperation(op danger.ToolOperation) error
PromptOperation implements danger.Approver for tool operations.
func (*TelegramApprover) ResetTrust ¶
func (a *TelegramApprover) ResetTrust()
ResetTrust clears all trusted risk classes. Used by /new command.
func (*TelegramApprover) SetLogger ¶
func (a *TelegramApprover) SetLogger(l Logger)
SetLogger sets the logger for this approver. If nil, a NopLogger is used.
func (*TelegramApprover) SetTrustAll ¶
func (a *TelegramApprover) SetTrustAll(enabled bool)
SetTrustAll enables or disables blanket trust for all risk classes. When enabled, PromptCommand returns nil for every call without prompting.
type TelegramConfig ¶
type TelegramConfig struct {
Token string `json:"bot_token"`
AllowedChats []int64 `json:"allowed_chats"`
AllowedUsers []int64 `json:"allowed_users"`
BotUsername string `json:"bot_username"`
PollInterval int `json:"poll_interval"` // seconds, default 1
PollTimeout int `json:"poll_timeout"` // seconds, default 30
MaxMsgLength int `json:"max_msg_length"` // default 4096
DailyTokenBudget int64 `json:"daily_token_budget"` // 0 = unlimited (default)
SessionTTL int `json:"session_ttl_hours"` // hours, default 24
AgentTimeout int `json:"agent_timeout_seconds"` // max agent run duration, default 900 (15m), 0 = unlimited
MaxDownloadSize int64 `json:"max_download_size,omitempty"` // 0 = default 5 MiB; <0 = unlimited; >0 = explicit cap
MediaQuotaPerChat int64 `json:"media_quota_per_chat,omitempty"` // 0 = disabled; >0 = per-chat quota in bytes
FallbackURLs []string `json:"fallback_urls"`
HealthAddr string `json:"health_addr"` // e.g. "127.0.0.1:9090" (empty = disabled)
LogLevel string `json:"log_level"` // "debug","info","warn","error" (default "info")
LogFile string `json:"log_file"` // path or empty for stderr
DefaultChatID int64 `json:"default_chat_id"` // for --deliver and cron delivery
// AllowAllUsers must be explicitly set to true to run the bot with NO
// allowlist (any Telegram user may drive the agent). Without it, an empty
// AllowedChats + AllowedUsers is a fatal misconfiguration (fail-closed) so
// an open bot can never be deployed by accident. Env: ODEK_TELEGRAM_ALLOW_ALL.
AllowAllUsers bool `json:"allow_all_users"`
}
TelegramConfig holds all configuration for the Telegram bot.
func ConfigFromEnv ¶
func ConfigFromEnv(base TelegramConfig) TelegramConfig
ConfigFromEnv reads configuration from environment variables, starting with the given base config and overriding any values that are set in the environment.
func DefaultConfig ¶
func DefaultConfig() TelegramConfig
DefaultConfig returns a TelegramConfig with sensible defaults.
func (TelegramConfig) HasAllowlist ¶ added in v1.5.0
func (c TelegramConfig) HasAllowlist() bool
HasAllowlist reports whether at least one allowlist (chats or users) is configured. With no allowlist the bot is open to every Telegram user, which requires the explicit AllowAllUsers opt-in (see ValidateConfig).
type TelegramError ¶
TelegramError represents an error returned by the Telegram Bot API. It includes the HTTP status code so callers can distinguish transient (429, 5xx) from fatal (401, 403, 409) errors without string matching.
func (*TelegramError) Error ¶
func (e *TelegramError) Error() string
type Update ¶
type Update struct {
ID int `json:"update_id"`
Message *Message `json:"message,omitempty"`
EditedMessage *Message `json:"edited_message,omitempty"`
CallbackQuery *CallbackQuery `json:"callback_query,omitempty"`
}
Update represents an incoming Telegram update.
type UpdateResponse ¶
type UpdateResponse struct {
OK bool `json:"ok"`
Result json.RawMessage `json:"result,omitempty"`
Description string `json:"description,omitempty"`
ErrorCode int `json:"error_code,omitempty"`
}
UpdateResponse is the generic Telegram API response for a single update-related request.
type User ¶
type User struct {
ID int64 `json:"id"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
Username string `json:"username,omitempty"`
IsBot bool `json:"is_bot,omitempty"`
}
User represents a Telegram user or bot.
type UserProfilePhotos ¶
type UserProfilePhotos struct {
TotalCount int `json:"total_count,omitempty"`
Photos [][]PhotoSize `json:"photos,omitempty"`
}
UserProfilePhotos contains a set of user profile photos.
type Voice ¶
type Voice struct {
FileID string `json:"file_id,omitempty"`
FileUniqueID string `json:"file_unique_id,omitempty"`
Duration int `json:"duration,omitempty"`
MimeType string `json:"mime_type,omitempty"`
FileSize int `json:"file_size,omitempty"`
}
Voice represents a voice note.
type WebhookInfo ¶
type WebhookInfo struct {
URL string `json:"url,omitempty"`
}
WebhookInfo is a placeholder for future webhook information.