telegram

package
v1.10.0 Latest Latest
Warning

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

Go to latest
Published: Jun 17, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package telegram provides Telegram bot integration.

Index

Constants

View Source
const (
	ParseModeMarkdownV2 = "MarkdownV2"
	ParseModeHTML       = "HTML"
)

Parse mode constants for SendOpts.

View Source
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

View Source
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

func CleanupMedia(maxAge time.Duration) (int, error)

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

func DeletePlan(slugPrefix string) (string, error)

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

func DownloadDocument(bot *Bot, chatID int64, fileID, fileName string) (string, error)

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

func DownloadPhoto(bot *Bot, chatID int64, fileIDs []string) (string, error)

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

func DownloadVoice(bot *Bot, chatID int64, fileID string) (string, error)

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

func EscapeMarkdown(text string) string

EscapeMarkdown escapes all reserved MarkdownV2 characters. Characters inside code spans (`...`) and code blocks (```...```) are NOT escaped.

func FormatResponse

func FormatResponse(text string) ([]string, error)

FormatResponse converts odek markdown output to Telegram MarkdownV2. It splits the result into chunks of at most 4096 bytes at paragraph boundaries.

func IsFatalAPIError

func IsFatalAPIError(err error) bool

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

func MediaDir() (string, error)

MediaDir returns the directory where downloaded media files are stored. Defaults to ~/.odek/media/. Creates the directory if it doesn't exist.

func MostRecentPlan

func MostRecentPlan() (string, string, error)

MostRecentPlan returns the slug and full content of the most recently modified plan file. Returns an error if no plans exist.

func ReadPlan

func ReadPlan(slugPrefix string) (string, string, error)

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

func ResolveMediaPath(path string) (string, error)

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

func RetryWithBackoff(fn func() error, maxAttempts int, baseDelay time.Duration) error

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

func Slugify(desc string) string

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

func NewBot(token string) *Bot

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

func (b *Bot) AnswerCallbackQuery(callbackID string, text string, showAlert bool) error

AnswerCallbackQuery sends an answer to a callback query.

func (*Bot) CheckDailyBudget

func (b *Bot) CheckDailyBudget(tokens int64) error

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

func (b *Bot) DailyTokenUsage() (used int64, limit int64)

DailyTokenUsage returns the current token usage and budget limit. Returns (0, 0) when the budget is not configured.

func (*Bot) DeleteMessage

func (b *Bot) DeleteMessage(chatID int64, messageID int) error

DeleteMessage deletes a message previously sent by the bot. Requires the bot to have can_delete_messages permission in groups/supergroups.

func (*Bot) DownloadFile

func (b *Bot) DownloadFile(filePath string) ([]byte, error)

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

func (b *Bot) EditMessageText(chatID int64, messageID int, text string, opts *SendOpts) error

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

func (b *Bot) GetFile(fileID string) (*File, error)

GetFile returns basic information about a file and prepares it for downloading.

func (*Bot) GetMe

func (b *Bot) GetMe() (*User, error)

GetMe returns basic information about the bot (useful as a health check).

func (*Bot) GetUpdates

func (b *Bot) GetUpdates(offset int, timeout int) ([]Update, error)

GetUpdates retrieves incoming updates using long polling (no context). Deprecated: Use GetUpdatesContext for context-aware cancellation.

func (*Bot) GetUpdatesContext

func (b *Bot) GetUpdatesContext(ctx context.Context, offset int, timeout int) ([]Update, error)

GetUpdates retrieves incoming updates using long polling with context support. When ctx is cancelled, the HTTP request is aborted immediately.

func (*Bot) SendChatAction

func (b *Bot) SendChatAction(chatID int64, action string) error

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

func (b *Bot) SendMessage(chatID int64, text string, opts *SendOpts) (*Message, error)

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

func (b *Bot) SetDailyTokenBudget(budget int64)

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

func (b *Bot) SetFallbackURLs(urls []string) error

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

func (b *Bot) SetFileBaseURL(url string)

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

func (b *Bot) SetLogger(l Logger)

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

func (ft *FallbackTransport) Do(req *http.Request) (*http.Response, error)

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

func (ft *FallbackTransport) RoundTrip(req *http.Request) (*http.Response, error)

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

type FileResponse struct {
	OK     bool  `json:"ok"`
	Result *File `json:"result,omitempty"`
}

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

type GetUpdatesResponse struct {
	OK     bool     `json:"ok"`
	Result []Update `json:"result,omitempty"`
}

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

func NewHandler(bot *Bot) *Handler

NewHandler creates a Handler with the given bot and default settings.

func (*Handler) DeleteApprover

func (h *Handler) DeleteApprover(chatID int64)

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

func (h *Handler) HandleUpdate(upd Update)

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

func (h *Handler) SendResponse(chatID int64, text string, replyToMessageID int)

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.

func (*Handler) SetLogger

func (h *Handler) SetLogger(l Logger)

SetLogger sets the logger for this handler. If nil, a NopLogger is used.

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.

func (*HealthServer) Start

func (hs *HealthServer) Start(ctx context.Context) error

Start begins listening on the configured address. Blocks until ctx is cancelled, then shuts down the HTTP server gracefully. Returns any error from starting the listener.

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.

const (
	LogDebug LogLevel = iota
	LogInfo
	LogWarn
	LogError
)

func ParseLogLevel

func ParseLogLevel(s string) LogLevel

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

func NewFileLogger(level LogLevel, path string) Logger

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.

func (*Message) IsCommand

func (m *Message) IsCommand() bool

IsCommand reports whether the message is a bot command. It checks the entities for type "bot_command".

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.

func ListPlans

func ListPlans(limit int) ([]PlanInfo, error)

ListPlans returns all .md plan files sorted by modification time (newest first). If limit > 0, only the most recent `limit` plans are returned. Returns an empty slice if the plans directory doesn't exist.

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

func NewPoller(bot *Bot) *Poller

NewPoller creates a new Poller with sensible defaults. Offset starts at 0, Interval is 1s, Timeout is 30s.

func (*Poller) Poll

func (p *Poller) Poll(ctx context.Context) ([]Update, error)

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) SetLogger

func (p *Poller) SetLogger(l Logger)

SetLogger sets the logger for this poller. If nil, a NopLogger is used.

func (*Poller) Start

func (p *Poller) Start(ctx context.Context, updates chan<- Update) error

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

type SendMessageResponse struct {
	OK     bool     `json:"ok"`
	Result *Message `json:"result,omitempty"`
}

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:

  1. Sends the command details + [Approve] [Deny] [Trust] keyboard
  2. Blocks on a channel waiting for the user's callback response
  3. 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

type TelegramError struct {
	Method      string
	Description string
	Code        int
}

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.

Jump to

Keyboard shortcuts

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