Documentation
¶
Index ¶
- Constants
- Variables
- func DecActiveSessions()
- func DetectAssistantInjection(text string) bool
- func IncActiveSessions()
- func IncMessageTelegramRichDraftOverflow()
- func IncMessageTelegramRichDraftTerminalCatchup(outcome richDraftTerminalCatchupOutcome)
- func RecordContextTokens(tokens int)
- func RecordContextTokensBySource(userID storage.ScopeID, source string, tokens int)
- func RecordMessageLLM(userID storage.ScopeID, totalDuration float64, callCount int)
- func RecordMessageLLMFirstToken(userID storage.ScopeID, durationSeconds float64)
- func RecordMessageProcessing(userID storage.ScopeID, durationSeconds float64, success bool)
- func RecordMessageReaction(userID storage.ScopeID, duration float64)
- func RecordMessageTelegram(userID storage.ScopeID, totalDuration float64, callCount int)
- func RecordMessageTelegramDraftCount(userID storage.ScopeID, count int)
- func RecordMessageTelegramEditCount(userID storage.ScopeID, count int)
- func RecordMessageTelegramRichDraftContentSnapshotCount(count int)
- func RecordMessageTools(userID storage.ScopeID, totalDuration float64, callCount int)
- func RecordResponseFlag(userID storage.ScopeID, emoji string)
- func SetActiveSessions(count int)
- type Bot
- func (b *Bot) API() telegram.BotAPI
- func (b *Bot) ForceCloseSession(ctx context.Context, userID storage.ScopeID) (int, error)
- func (b *Bot) ForceCloseSessionWithProgress(ctx context.Context, userID storage.ScopeID, onProgress rag.ProgressCallback) (*rag.ProcessingStats, error)
- func (b *Bot) GetActiveSessions() ([]rag.ActiveSessionInfo, error)
- func (b *Bot) HandleIncoming(im IncomingMessage)
- func (b *Bot) HandleReaction(ir IncomingReaction)
- func (b *Bot) HandleUpdate(ctx context.Context, rawUpdate json.RawMessage, remoteAddr string)
- func (b *Bot) HandleUpdateAsync(ctx context.Context, rawUpdate json.RawMessage, remoteAddr string)
- func (b *Bot) IsAllowedScope(scopeID storage.ScopeID) bool
- func (b *Bot) ProcessUpdate(ctx context.Context, update *telegram.Update, source string)
- func (b *Bot) ProcessUpdateAsync(ctx context.Context, update *telegram.Update, source string)
- func (b *Bot) SendTestMessage(ctx context.Context, userID storage.ScopeID, text string, saveToHistory bool) (*rag.TestMessageResult, error)
- func (b *Bot) SetAgentLogger(logger *agentlog.Logger)
- func (b *Bot) SetArtifactRepo(repo storage.ArtifactRepository)
- func (b *Bot) SetFetcher(f fetch.Fetcher)
- func (b *Bot) SetFileHandler(handler files.FileSaver)
- func (b *Bot) SetFileProcessor(processor *files.Processor)
- func (b *Bot) SetFileStorage(fs files.Storage)
- func (b *Bot) SetFlagRepo(repo storage.FlagRepository)
- func (b *Bot) SetImageGenerator(gen tools.ImageGenerator)
- func (b *Bot) SetLaplaceAgent(agent *laplace.Laplace)
- func (b *Bot) SetPrincipalResolver(r PrincipalResolver)
- func (b *Bot) SetReactorAgent(a agent.Agent)
- func (b *Bot) SetRenderer(r Renderer)
- func (b *Bot) SetScopeRepository(repo identityStore)
- func (b *Bot) SetTransport(t Transport)
- func (b *Bot) SetWebhook(webhookURL, secretToken string) error
- func (b *Bot) StartMattermostIngestion(client *mattermost.Client)
- func (b *Bot) Stop()
- type Capabilities
- type Chat
- type Document
- type FileHandler
- type ForwardInfo
- type IncomingMessage
- type IncomingReaction
- type IngressMetadata
- type MMRenderer
- type MMTransport
- func (t *MMTransport) AllowlistConfigured() bool
- func (t *MMTransport) Capabilities() Capabilities
- func (t *MMTransport) IsAllowed(nativeSenderID string) bool
- func (t *MMTransport) Kind() string
- func (t *MMTransport) SendMedia(ctx context.Context, m OutgoingMedia) (string, error)
- func (t *MMTransport) SendText(ctx context.Context, r OutgoingResponse) (string, error)
- func (t *MMTransport) SendTyping(ctx context.Context, conversationID string) error
- func (t *MMTransport) SetReaction(ctx context.Context, _, messageID, emoji string) error
- type MattermostPrincipalResolver
- func (r *MattermostPrincipalResolver) ClassifyBot(ctx context.Context, nativeID string) (bool, bool, error)
- func (r *MattermostPrincipalResolver) Invalidate(nativeID string)
- func (r *MattermostPrincipalResolver) IsTrusted(ctx context.Context, nativeID string) (bool, error)
- func (r *MattermostPrincipalResolver) Resolve(ctx context.Context, nativeID string) (*storage.PrincipalInput, error)
- type Message
- type MessageGroup
- type MessageGrouper
- type MessageOrigin
- type OutgoingMedia
- type OutgoingMediaItem
- type OutgoingMediaWireKind
- type OutgoingResponse
- type OutgoingRichMedia
- type PhotoSize
- type PrincipalResolver
- type Renderer
- type ResponseFormat
- type RichMediaTransport
- type SessionInfo
- type TelegramRenderer
- type TelegramTransport
- func (t *TelegramTransport) AllowlistConfigured() bool
- func (t *TelegramTransport) Capabilities() Capabilities
- func (t *TelegramTransport) IsAllowed(nativeSenderID string) bool
- func (t *TelegramTransport) Kind() string
- func (t *TelegramTransport) SendMedia(ctx context.Context, m OutgoingMedia) (string, error)
- func (t *TelegramTransport) SendMediaPersistent(ctx context.Context, m OutgoingMedia) (persistentSendResult, error)
- func (t *TelegramTransport) SendRichMedia(ctx context.Context, m OutgoingRichMedia) (string, error)
- func (t *TelegramTransport) SendText(ctx context.Context, r OutgoingResponse) (string, error)
- func (t *TelegramTransport) SendTextPersistent(ctx context.Context, r OutgoingResponse) (string, error)
- func (t *TelegramTransport) SendTyping(ctx context.Context, conversationID string) error
- func (t *TelegramTransport) SetReaction(ctx context.Context, conversationID, messageID, emoji string) error
- type Transport
- type Update
- type User
- type Voice
Constants ¶
const ( ContextSourceSystem = "system" ContextSourceProfile = "profile" ContextSourceTopics = "topics" ContextSourceSession = "session" )
Context source types
Variables ¶
var ErrRichMessageRejected = errors.New("rich message rejected")
ErrRichMessageRejected marks a confirmed transport-side rejection where it is safe to resend the same source through the legacy renderer. Ambiguous network failures must never be wrapped with this sentinel: retrying those as legacy could duplicate a message that the server actually accepted.
Functions ¶
func DecActiveSessions ¶ added in v0.3.0
func DecActiveSessions()
DecActiveSessions decrements the active sessions counter by 1.
func DetectAssistantInjection ¶ added in v0.11.0
DetectAssistantInjection reports whether text looks like a block of instructions addressed to an AI assistant (a forwarded "profile" or "system data" block with a persist/apply command) rather than a human message. Both the assistant-target and the command signal must be present.
func IncActiveSessions ¶ added in v0.3.0
func IncActiveSessions()
IncActiveSessions increments the active sessions counter by 1.
func IncMessageTelegramRichDraftOverflow ¶ added in v0.11.0
func IncMessageTelegramRichDraftOverflow()
IncMessageTelegramRichDraftOverflow records one preview-only source cap hit.
func IncMessageTelegramRichDraftTerminalCatchup ¶ added in v0.11.0
func IncMessageTelegramRichDraftTerminalCatchup(outcome richDraftTerminalCatchupOutcome)
IncMessageTelegramRichDraftTerminalCatchup records one terminal preview outcome. Callers pass only the package's closed outcome constants.
func RecordContextTokens ¶ added in v0.3.0
func RecordContextTokens(tokens int)
RecordContextTokens records context size in tokens.
func RecordContextTokensBySource ¶ added in v0.3.3
RecordContextTokensBySource records tokens by context source.
func RecordMessageLLM ¶ added in v0.3.8
RecordMessageLLM records total LLM duration and call count for a message.
func RecordMessageLLMFirstToken ¶ added in v0.9.0
RecordMessageLLMFirstToken records time-to-first-content in stream mode. Only call when streaming actually produced at least one content delta; reasoning-only or empty responses should not be observed here.
func RecordMessageProcessing ¶ added in v0.3.0
func RecordMessageReaction ¶ added in v0.3.8
RecordMessageReaction records SetMessageReaction duration.
func RecordMessageTelegram ¶ added in v0.3.8
RecordMessageTelegram records total Telegram send duration and call count for a message.
func RecordMessageTelegramDraftCount ¶ added in v0.11.0
RecordMessageTelegramDraftCount records ephemeral Rich Message preview calls. A draft count never implies persistent delivery.
func RecordMessageTelegramEditCount ¶ added in v0.9.0
RecordMessageTelegramEditCount records the number of editMessageText calls the streaming sink issued for a single message turn.
func RecordMessageTelegramRichDraftContentSnapshotCount ¶ added in v0.11.0
func RecordMessageTelegramRichDraftContentSnapshotCount(count int)
RecordMessageTelegramRichDraftContentSnapshotCount records successful draft snapshots that advanced the accepted content prefix for one turn.
func RecordMessageTools ¶ added in v0.3.8
RecordMessageTools records total tool execution duration and call count for a message.
func RecordResponseFlag ¶ added in v0.10.2
RecordMessageProcessing records message processing metrics. RecordResponseFlag records one user-flagged bad reply.
func SetActiveSessions ¶ added in v0.3.0
func SetActiveSessions(count int)
SetActiveSessions sets the current number of active sessions.
Types ¶
type Bot ¶
type Bot struct {
// contains filtered or unexported fields
}
func NewBot ¶
func NewBot(logger *slog.Logger, api telegram.BotAPI, cfg *config.Config, userRepo storage.UserRepository, msgRepo storage.MessageRepository, statsRepo storage.StatsRepository, factRepo storage.FactRepository, factHistoryRepo storage.FactHistoryRepository, peopleRepo storage.PeopleRepository, orClient llm.Client, ragService *rag.Service, contextService *agent.ContextService, translator *i18n.Translator) (*Bot, error)
func (*Bot) ForceCloseSession ¶ added in v0.3.0
ForceCloseSession immediately processes unprocessed messages for a user into topics.
func (*Bot) ForceCloseSessionWithProgress ¶ added in v0.3.0
func (b *Bot) ForceCloseSessionWithProgress(ctx context.Context, userID storage.ScopeID, onProgress rag.ProgressCallback) (*rag.ProcessingStats, error)
ForceCloseSessionWithProgress immediately processes unprocessed messages with progress reporting.
func (*Bot) GetActiveSessions ¶ added in v0.3.0
func (b *Bot) GetActiveSessions() ([]rag.ActiveSessionInfo, error)
GetActiveSessions returns information about active sessions (unprocessed messages) for all users.
func (*Bot) HandleIncoming ¶ added in v0.10.0
func (b *Bot) HandleIncoming(im IncomingMessage)
HandleIncoming is the transport-neutral ingestion entry: it resolves the internal scope id for the message and feeds it to the message grouper. Both the Telegram path (ProcessUpdate) and future transports (Mattermost WS loop) converge here.
func (*Bot) HandleReaction ¶ added in v0.10.2
func (b *Bot) HandleReaction(ir IncomingReaction)
HandleReaction records a flag when an authorized user reacts to one of the bot's replies. Telegram delivers message_reaction for any message in a DM (verified), so we filter to bot replies by resolving the reacted message id to a stored assistant reply; a miss (the user's own / a pre-feature message) is silently ignored. Only newly-added reactions flag — removals are ignored, matching the "any reaction = flag, no un-flag" product decision.
func (*Bot) HandleUpdate ¶
func (*Bot) HandleUpdateAsync ¶ added in v0.2.0
HandleUpdateAsync starts processing a raw update in a goroutine. It properly handles WaitGroup to ensure graceful shutdown. Used by webhook handler.
func (*Bot) IsAllowedScope ¶ added in v0.10.0
IsAllowedScope reports whether a scope id (the partition key used in the web UI and storage) belongs to an allowlisted user on ANY transport. The download handler uses it to authorize artifact access transport-agnostically: scope ids are derived as PassthroughScopeID(transport, nativeID), so we re-derive the allowed set from each transport's own allowlist rather than assuming Telegram.
NOTE: valid while scopes are pure passthrough(transport, native). Once a principal resolver remaps scopes (Variant C), this must consult the scope/ principal store instead of the config-derived set.
func (*Bot) ProcessUpdate ¶
func (*Bot) ProcessUpdateAsync ¶ added in v0.2.0
ProcessUpdateAsync starts processing an update in a goroutine. It properly handles WaitGroup to ensure graceful shutdown.
func (*Bot) SendTestMessage ¶ added in v0.3.0
func (b *Bot) SendTestMessage(ctx context.Context, userID storage.ScopeID, text string, saveToHistory bool) (*rag.TestMessageResult, error)
SendTestMessage sends a test message through the bot pipeline without Telegram. It returns detailed metrics for debugging purposes.
func (*Bot) SetAgentLogger ¶ added in v0.4.8
SetAgentLogger sets the agent logger for debug logging
func (*Bot) SetArtifactRepo ¶ added in v0.6.0
func (b *Bot) SetArtifactRepo(repo storage.ArtifactRepository)
SetArtifactRepo sets the artifact repository for linking artifacts to messages
func (*Bot) SetFetcher ¶ added in v0.10.3
SetFetcher wires the web-page fetcher so the read_url tool becomes operational. Unwired, the tool tells the LLM page reading is unavailable.
func (*Bot) SetFileHandler ¶ added in v0.6.0
SetFileHandler sets the optional file handler for artifact saving
func (*Bot) SetFileProcessor ¶ added in v0.6.0
SetFileProcessor replaces the file processor (for testing).
func (*Bot) SetFileStorage ¶ added in v0.8.0
SetFileStorage wires the artifact blob store used by the generate_image tool to persist output PNGs and by the media-reply path to read them.
func (*Bot) SetFlagRepo ¶ added in v0.10.2
func (b *Bot) SetFlagRepo(repo storage.FlagRepository)
SetFlagRepo wires the repository for user-flagged bad replies. When unset, inbound reactions are still received but no flag is recorded.
func (*Bot) SetImageGenerator ¶ added in v0.8.0
func (b *Bot) SetImageGenerator(gen tools.ImageGenerator)
SetImageGenerator wires the image-generation agent so the generate_image tool becomes operational. No-op if the agent is nil — the tool simply returns a "not configured" error to the LLM.
func (*Bot) SetLaplaceAgent ¶ added in v0.5.0
SetLaplaceAgent sets the Laplace chat agent
func (*Bot) SetPrincipalResolver ¶ added in v0.10.0
func (b *Bot) SetPrincipalResolver(r PrincipalResolver)
SetPrincipalResolver wires principal identity resolution for the active transport's DMs. Leaving it nil keeps the passthrough behavior (Telegram, and any transport that hasn't opted into principal resolution).
func (*Bot) SetReactorAgent ¶ added in v0.10.1
SetReactorAgent sets the reactor agent that decides emoji reactions.
func (*Bot) SetRenderer ¶ added in v0.10.0
SetRenderer swaps the wire-format renderer to match the active transport.
func (*Bot) SetScopeRepository ¶ added in v0.10.0
func (b *Bot) SetScopeRepository(repo identityStore)
SetScopeRepository wires the identity store (scope/identity map + principal & channel get-or-create), required for non-Telegram transports.
func (*Bot) SetTransport ¶ added in v0.10.0
SetTransport swaps the output/identity transport (used to install the Mattermost/Time transport for the work instance).
func (*Bot) SetWebhook ¶
func (*Bot) StartMattermostIngestion ¶ added in v0.10.0
func (b *Bot) StartMattermostIngestion(client *mattermost.Client)
StartMattermostIngestion consumes "posted" events from the client and feeds them into the neutral pipeline. It returns when the client closes its events channel (on ctx cancellation), making shutdown deterministic.
mmShouldProcess drops the bot's own/system posts. Authorization (the SSO/ allowlist access gate) and channel reply-gating are owned by HandleIncoming, so both DMs and channel posts pass through here.
type Capabilities ¶ added in v0.10.0
type Capabilities struct {
MaxMessageLen int // Telegram 4096 UTF-16; Mattermost MaxPostSize (runtime)
ParseMode string // "HTML" (Telegram) | "" native markdown (Mattermost)
SupportsLatex bool // true when the selected output path renders LaTeX natively
SupportsStreaming bool // Telegram: legacy edits or ephemeral rich drafts | Mattermost false
SupportsRichMessages bool // Telegram Bot API Rich Messages (opt-in) | Mattermost false
SupportsReactions bool // Telegram true | Mattermost true (by emoji name)
SupportsMedia bool // can SendMedia deliver files
MaxMediaItemsPerGroup int // Telegram 10; Mattermost 5 (files per post)
EmojiStyle string // "unicode" (Telegram) | "shortcode" (Mattermost)
MaxFileSize int64 // 0 = unset/unlimited (files are Phase 4)
// AvailableReactions are the reaction tokens the bot may use on this
// transport: unicode emoji for Telegram (the fixed Bot API set), emoji
// shortcode names for Mattermost. Empty disables reactions.
AvailableReactions []string
}
Capabilities describes per-transport rendering and feature support. The core branches on these, never on Kind(), so transport-specific behavior stays declarative.
type Chat ¶
type Chat struct {
ID int64 `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
Username string `json:"username"`
}
Chat represents a chat.
type Document ¶
type Document struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
Thumbnail *PhotoSize `json:"thumbnail,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 and audio files).
type FileHandler ¶ added in v0.6.0
type FileHandler struct {
// contains filtered or unexported fields
}
FileHandler coordinates file saving with deduplication.
func NewFileHandler ¶ added in v0.6.0
func NewFileHandler( storage files.Storage, artifactRepo storage.ArtifactRepository, logger *slog.Logger, ) *FileHandler
NewFileHandler creates a new file handler.
func (*FileHandler) SaveFile ¶ added in v0.6.0
func (fh *FileHandler) SaveFile( ctx context.Context, userID storage.ScopeID, messageID int64, fileType string, originalName string, mimeType string, reader io.Reader, messageText string, skipExtraction bool, ) (*int64, error)
SaveFile saves a Telegram file as an artifact. Returns artifact ID (existing on deduplication, new on creation) and any error. messageText is the text content of the message (msg.Text or msg.Caption) for context (v0.6.0). skipExtraction retains the raw file (bytes + content_hash + row) for reproducibility/replay but creates the row in the 'retained' state instead of 'pending', so the Extractor never processes it and it never gets a summary or embedding — keeping trivial files (e.g. short voice notes) out of RAG while still leaving them replayable. See saveVoiceArtifact.
type ForwardInfo ¶ added in v0.10.0
type ForwardInfo struct {
SenderID string // forwarded sender's native id (Telegram int64 as string)
FirstName string
LastName string
Username string
IsBot bool
IsUser bool // true only when forwarded from a user (not channel/hidden)
}
ForwardInfo carries the structured sender of a forwarded message for the People social graph. Telegram-only in v0.10 (Mattermost has no forward origin with user identity); nil for non-forwarded messages.
type IncomingMessage ¶ added in v0.10.0
type IncomingMessage struct {
ConversationID string // Telegram chat.ID (stringified) | Mattermost channel_id
SenderID string // Telegram From.ID (stringified) | Mattermost 26-char user id
MessageID string // transport message/post id
Text string // user text (Telegram Text or Caption, merged)
// DetectionText is the transport's raw visible text before typed metadata
// projection. It is used only by deterministic safety gates; it is never
// sent to the model or persisted instead of Text. Empty means Text is the
// appropriate detection view (for example Mattermost or rich-only input).
DetectionText string
SenderDisplay string // human-readable sender ("Name (@handle)") for logs
ConversationDisplay string // human-readable channel name (channel scopes); "" for DMs/Telegram
Prefix string // pre-built display prefix ("[Name (time)]" or forwarded-from)
ThreadRoot string // Telegram MessageThreadID (forum) | Mattermost root_id; "" = top level
IsDirect bool // DM (Telegram private chat | Mattermost channel_type==D)
Mention bool // bot mentioned (for channels); always acted on in DMs
ReplyToBot bool // message replies to / quotes a bot message (channel reply-gating)
SentAt time.Time
Files []files.IncomingFile
Forward *ForwardInfo // Telegram-only forwarded-sender info; nil otherwise
Ingress *IngressMetadata
// RichEgressEligible is set only by the Telegram adapter after it has
// verified a plain private-chat context. Business/direct-message topics and
// groups stay legacy until their routing identifiers are modeled end to end.
RichEgressEligible bool
}
IncomingMessage is the transport-neutral envelope that flows through the message grouper and processing pipeline. Telegram and Mattermost/Time each map their native update into this shape at the ingestion boundary; the core never sees a transport-specific message type.
type IncomingReaction ¶ added in v0.10.2
type IncomingReaction struct {
ConversationID string // transport-native chat/channel id
SenderID string // the reacting user's transport-native id
MessageID string // transport-native id of the reacted message
OldEmojis []string // emoji set before the change
NewEmojis []string // emoji set after the change
IsDirect bool // DM scope
}
IncomingReaction is the transport-neutral envelope for a reaction a user added to (or removed from) a message. Only Telegram populates it today (ProcessUpdate → incomingReactionFromTelegram); a future Mattermost WS loop can converge on HandleReaction. OldEmojis/NewEmojis are the reaction sets before/after the change, so a handler can tell additions from removals.
type IngressMetadata ¶ added in v0.11.0
type IngressMetadata struct {
Kind string // fixed metric value, currently "rich"
Disposition string // processable | partial | unsupported | invalid
HasVisibleText bool // excludes generated media/failure markers
BlockCount int
MediaCount int
Unknown bool
}
IngressMetadata carries bounded, content-free classification from a native transport decoder into the grouped-turn pipeline. It is currently populated for Telegram Rich Messages; nil keeps every legacy transport byte-compatible.
type MMRenderer ¶ added in v0.10.0
type MMRenderer struct {
// contains filtered or unexported fields
}
MMRenderer renders canonical markdown for Mattermost: split on the ###SPLIT### delimiter, fix list numbering, and hard-split oversized parts at MaxPostSize. Markdown (incl. LaTeX math) is passed through unchanged — Time renders it natively via KaTeX; the system prompt steers the model toward clean $…$.
func NewMattermostRenderer ¶ added in v0.10.0
func NewMattermostRenderer(maxPostSize int, logger *slog.Logger) *MMRenderer
NewMattermostRenderer builds the markdown pass-through renderer. maxPostSize is the server's limit; a safety margin is subtracted internally.
func (*MMRenderer) RenderCaption ¶ added in v0.10.1
RenderCaption fits the start of the response into the post budget; markdown passes through unchanged (Mattermost renders it natively), so a rune split is exact — no expansion to account for.
type MMTransport ¶ added in v0.10.0
type MMTransport struct {
// contains filtered or unexported fields
}
MMTransport adapts the Mattermost/Time client to the neutral Transport interface. Output-only: ingestion runs via startMattermostIngestion.
func NewMattermostTransport ¶ added in v0.10.0
func NewMattermostTransport(client *mattermost.Client, cfg *config.Config, logger *slog.Logger) *MMTransport
NewMattermostTransport builds the Mattermost output adapter.
func (*MMTransport) AllowlistConfigured ¶ added in v0.10.0
func (t *MMTransport) AllowlistConfigured() bool
func (*MMTransport) Capabilities ¶ added in v0.10.0
func (t *MMTransport) Capabilities() Capabilities
func (*MMTransport) IsAllowed ¶ added in v0.10.0
func (t *MMTransport) IsAllowed(nativeSenderID string) bool
IsAllowed checks the native Mattermost sender id (26-char string) against the configured allowlist. Empty allowlist rejects everyone (fail closed) — in SSO mode the caller treats an unconfigured allowlist as "all trusted senders" instead, see Bot.authorizeSender.
func (*MMTransport) Kind ¶ added in v0.10.0
func (t *MMTransport) Kind() string
func (*MMTransport) SendMedia ¶ added in v0.10.0
func (t *MMTransport) SendMedia(ctx context.Context, m OutgoingMedia) (string, error)
SendMedia uploads each item to the channel and posts them with the caption. Mattermost caps attachments at mmMaxFilesPerPost per post, so larger batches are split across posts; the caption rides the first post only. The post threads under ThreadRoot (or ReplyTo when ThreadRoot is empty), mirroring SendText. Markdown is passed through (MM renders it natively).
func (*MMTransport) SendText ¶ added in v0.10.0
func (t *MMTransport) SendText(ctx context.Context, r OutgoingResponse) (string, error)
SendText posts one rendered markdown chunk. It threads under ThreadRoot (or anchors to ReplyTo when ThreadRoot is empty) and sends a deterministic idempotency key so a retried send is collapsed server-side.
func (*MMTransport) SendTyping ¶ added in v0.10.0
func (t *MMTransport) SendTyping(ctx context.Context, conversationID string) error
func (*MMTransport) SetReaction ¶ added in v0.10.0
func (t *MMTransport) SetReaction(ctx context.Context, _, messageID, emoji string) error
SetReaction adds the given shortcode reaction to the post. The emoji must be one of mmReactions.
type MattermostPrincipalResolver ¶ added in v0.10.0
type MattermostPrincipalResolver struct {
// contains filtered or unexported fields
}
MattermostPrincipalResolver implements federated-passive resolution for the Mattermost/Time transport. It reads the sender's GetUser record and links only externally-authenticated accounts.
Trust gates (the highest-stakes lines — one slip merges two people's memory):
- auth_service == "" → local account → nil (isolation). NEVER linked.
- trustedServices set and auth_service not in it → nil (isolation).
- link key is the lowercased auth_data (the AD login), NEVER the email. A local user can self-claim an email in a trusted-looking domain, so email is stored as a principal attribute only.
objectGUID stays empty for now and is backfilled (additively) later via a Keycloak/LDAP lookup; principal dedup falls back to ad_login until then.
func NewMattermostPrincipalResolver ¶ added in v0.10.0
func NewMattermostPrincipalResolver(client mmUserLookup, trustedServices, trustedBots []string, logger *slog.Logger) *MattermostPrincipalResolver
NewMattermostPrincipalResolver builds the resolver. trustedServices restricts which auth_service values are trusted (empty = any non-empty). trustedBots lists bot-account usernames admitted despite failing the SSO gate (empty = no bots trusted — fail-closed). Both lists are lowercased/trimmed so the comparison against Mattermost's auth_service / username is case-insensitive (config "SAML" matches the wire value "saml"; "@AlertBot" matches "alertbot").
func (*MattermostPrincipalResolver) ClassifyBot ¶ added in v0.10.2
func (r *MattermostPrincipalResolver) ClassifyBot(ctx context.Context, nativeID string) (bool, bool, error)
ClassifyBot reports whether nativeID is a bot account and, if so, whether its username is on the trusted-bots allowlist. It is consulted only after the SSO trust gate (IsTrusted) has already rejected the sender, so the GetUser here is a cache hit. An empty allowlist trusts no bots (fail-closed): slices.Contains over an empty slice is false, by design — there is deliberately no "empty = trust all bots" branch.
func (*MattermostPrincipalResolver) Invalidate ¶ added in v0.10.0
func (r *MattermostPrincipalResolver) Invalidate(nativeID string)
Invalidate drops the resolver's cached view of nativeID so the next trust check re-reads the profile from the server. The bot calls this on a denial: the denied account may have just migrated to SSO, and re-reading on its next message gives instant recovery instead of waiting out the profile-cache TTL.
func (*MattermostPrincipalResolver) IsTrusted ¶ added in v0.10.0
IsTrusted reports whether the sender is an externally-authenticated account this resolver trusts (the access gate). It checks only auth_service — a non-empty value that, when a trustedServices allowlist is configured, is a member of it. It deliberately skips the ad_login/email checks Resolve applies: access is broader than linkability (an SSO user with an unusable auth_data still gets in, just on an isolated scope).
func (*MattermostPrincipalResolver) Resolve ¶ added in v0.10.0
func (r *MattermostPrincipalResolver) Resolve(ctx context.Context, nativeID string) (*storage.PrincipalInput, error)
Resolve returns the principal attributes for nativeID, or nil if the sender is not trusted-linkable (local account / untrusted service / no AD login).
type Message ¶
type Message struct {
MessageID int `json:"message_id"`
From *User `json:"from"`
Chat *Chat `json:"chat"`
Date int `json:"date"`
Text string `json:"text"`
Caption string `json:"caption"`
Photo []PhotoSize `json:"photo"`
Document *Document `json:"document"`
Voice *Voice `json:"voice"`
ForwardOrigin *MessageOrigin `json:"forward_origin"`
IsCommand bool `json:"-"` // This will be determined manually
Command string `json:"-"` // This will be determined manually
CommandArguments string `json:"-"` // This will be determined manually
}
Message represents a message.
type MessageGroup ¶
type MessageGroup struct {
Messages []IncomingMessage
Timer *time.Timer
CancelFunc context.CancelFunc
UserID storage.ScopeID
StartedAt time.Time // When the first message in this group was received
}
MessageGroup represents a collection of messages processed together as one turn. UserID is the resolved internal scope id (the storage partition key) — for Telegram it equals the sender id (passthrough); for a channel it is the channel's scope id, shared by all participants.
type MessageGrouper ¶
type MessageGrouper struct {
// contains filtered or unexported fields
}
MessageGrouper handles the grouping of incoming messages.
func NewMessageGrouper ¶
func NewMessageGrouper(b *Bot, logger *slog.Logger, turnWait time.Duration, onGroupReady func(ctx context.Context, group *MessageGroup)) *MessageGrouper
NewMessageGrouper creates a new MessageGrouper.
func (*MessageGrouper) AddMessage ¶
func (mg *MessageGrouper) AddMessage(scopeID storage.ScopeID, im IncomingMessage)
AddMessage adds a new message to its group. scopeID is the resolved internal scope id (storage partition key); the grouping key is derived from it and the message (per-sender in channels — see groupKeyFor).
func (*MessageGrouper) ForceCloseSession ¶ added in v0.3.0
func (mg *MessageGrouper) ForceCloseSession(scopeID storage.ScopeID) bool
ForceCloseSession immediately processes and closes any active groups for the given scope id. Returns true if at least one group was found and closed. In a channel a scope may have several per-sender groups; all are closed.
func (*MessageGrouper) GetActiveSessions ¶ added in v0.3.0
func (mg *MessageGrouper) GetActiveSessions() []SessionInfo
GetActiveSessions returns information about all active message grouping sessions.
func (*MessageGrouper) Stop ¶ added in v0.2.0
func (mg *MessageGrouper) Stop()
Stop processes all pending message groups and waits for completion. Should be called during graceful shutdown.
type MessageOrigin ¶
type MessageOrigin struct {
Type string `json:"type"`
Date int `json:"date"`
SenderUser *User `json:"sender_user,omitempty"`
SenderUserName string `json:"sender_user_name,omitempty"`
SenderChat *Chat `json:"sender_chat,omitempty"`
AuthorSignature string `json:"author_signature,omitempty"`
MessageID int `json:"message_id,omitempty"`
}
MessageOrigin is a union type that can be one of MessageOriginUser, MessageOriginHiddenUser, MessageOriginChat, or MessageOriginChannel.
func (*MessageOrigin) UnmarshalJSON ¶
func (mo *MessageOrigin) UnmarshalJSON(data []byte) error
UnmarshalJSON is a custom unmarshaler for MessageOrigin to handle the union type.
type OutgoingMedia ¶ added in v0.10.0
type OutgoingMedia struct {
ConversationID string
ThreadRoot string // keeps the media threaded
ReplyTo string // message id to reply to; first item only
Caption string // wire-format caption (may be empty)
Items []OutgoingMediaItem
}
OutgoingMedia is a transport-neutral media reply: one or more files sharing a single caption, threaded/replied like a text response. Caption is in the transport's wire format (HTML for Telegram, markdown for Mattermost) as produced by the transport's Renderer.RenderCaption, already fitted to the transport's caption budget; any overflow is delivered by the caller as a follow-up text message.
type OutgoingMediaItem ¶ added in v0.10.0
type OutgoingMediaItem struct {
Data []byte
Filename string
MIME string
WireKind OutgoingMediaWireKind // exact photo/document presentation; zero keeps legacy behavior
AsDocument bool // legacy document override, consulted only when WireKind is zero
SourceOrdinal int // application-only 1-based generated-artifact slot; transports ignore it (zero tells planners to use item index+1)
}
OutgoingMediaItem is one file in an OutgoingMedia batch.
type OutgoingMediaWireKind ¶ added in v0.11.0
type OutgoingMediaWireKind string
OutgoingMediaWireKind selects the exact Telegram upload method for a media item. The zero value preserves the legacy size-threshold/AsDocument policy; durable delivery plans should always select Photo or Document explicitly. Transports without a photo/document distinction may ignore it.
const ( OutgoingMediaWireKindLegacy OutgoingMediaWireKind = "" OutgoingMediaWireKindPhoto OutgoingMediaWireKind = "photo" OutgoingMediaWireKindDocument OutgoingMediaWireKind = "document" )
type OutgoingResponse ¶ added in v0.10.0
type OutgoingResponse struct {
ConversationID string
Text string
ThreadRoot string // set on every chunk to keep replies threaded
ReplyTo string // message id to reply to; set on the first chunk only
Format ResponseFormat
}
OutgoingResponse is one rendered, ready-to-send message chunk. Text is in the transport's wire format (HTML for Telegram, markdown for Mattermost) as produced by the transport's Renderer, unless Format selects another explicit representation.
type OutgoingRichMedia ¶ added in v0.11.0
type OutgoingRichMedia struct {
ConversationID string
ThreadRoot string
ReplyTo string
HTMLParts []string
MediaGroupSizes []int
Items []OutgoingMediaItem
}
OutgoingRichMedia is an optional, transport-native composition of trusted media bytes and fully rendered Rich HTML parts. HTMLParts and MediaGroupSizes describe an alternating sequence:
HTMLParts[0], media group 0, HTMLParts[1], ... media group N, HTMLParts[N+1]
Items is the flattened media order and each positive MediaGroupSizes entry consumes that many consecutive items. Empty HTML parts are valid, including a media-only message. The model never supplies bytes or media references: callers resolve generated artifact IDs through user-isolated storage, and the transport injects the corresponding references.
RichMediaTransport is intentionally separate from Transport. Backends that can't embed media in a structured message keep the established SendMedia path without adding a meaningless implementation.
type PhotoSize ¶
type PhotoSize struct {
FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
Width int `json:"width"`
Height int `json:"height"`
FileSize int `json:"file_size,omitempty"`
}
PhotoSize represents one size of a photo or a file / sticker thumbnail.
type PrincipalResolver ¶ added in v0.10.0
type PrincipalResolver interface {
Resolve(ctx context.Context, nativeID string) (*storage.PrincipalInput, error)
// IsTrusted reports whether the sender is an externally-authenticated (SSO)
// account this resolver trusts — the access gate. It is intentionally looser
// than Resolve: it does NOT require a linkable ad_login, so an SSO user with
// an email-shaped or empty auth_data is granted access but still resolves to
// an isolated scope (Resolve returns nil). Access and linkability are
// distinct concerns.
IsTrusted(ctx context.Context, nativeID string) (bool, error)
}
PrincipalResolver maps a transport-native DM sender to the principal (AD-backed person) behind it, for unified cross-transport memory. It returns nil — NOT an error — when the sender cannot be trusted-linked (a local account, no usable external subject); the caller then falls back to an isolated passthrough scope. A transport with no resolver wired never links at all (passthrough behavior).
Resolution is per-transport because the trust signal is transport-specific (Mattermost auth_service, later Talk/MAX equivalents).
type Renderer ¶ added in v0.10.0
type Renderer interface {
Render(ctx context.Context, canonicalMarkdown string) (chunks []string, err error)
// RenderCaption fits the start of a canonical-markdown response into the
// transport's media caption budget (measured on the rendered wire format)
// and returns the wire-format caption plus the remaining markdown to send
// as follow-up text via Render.
RenderCaption(ctx context.Context, canonicalMarkdown string) (wireCaption, overflowMarkdown string)
}
Renderer converts a canonical-markdown response into one or more wire-format chunks for a transport (HTML for Telegram, markdown for Mattermost), respecting the transport's message-length limit. The context carries the root processing span so renderers can record anomaly attributes (e.g. re-splits after HTML expansion).
type ResponseFormat ¶ added in v0.11.0
type ResponseFormat string
ResponseFormat identifies the wire format already present in OutgoingResponse.Text. The zero value keeps the established per-transport format; RichHTML is an explicit opt-in used only by Telegram Rich Messages.
const ( ResponseFormatDefault ResponseFormat = "" ResponseFormatRichHTML ResponseFormat = "telegram_rich_html" )
type RichMediaTransport ¶ added in v0.11.0
type RichMediaTransport interface {
SendRichMedia(ctx context.Context, m OutgoingRichMedia) (msgID string, err error)
}
RichMediaTransport is implemented by transports that can atomically persist trusted media and structured text as one message.
type SessionInfo ¶ added in v0.3.0
type SessionInfo struct {
UserID storage.ScopeID
MessageCount int
StartedAt time.Time
LastMessage time.Time
}
SessionInfo represents information about an active message grouping session.
type TelegramRenderer ¶ added in v0.10.0
type TelegramRenderer struct {
// contains filtered or unexported fields
}
TelegramRenderer converts canonical markdown into wire-format HTML chunks: split on the ###SPLIT### delimiter, fix list numbering across parts, split oversized parts, then convert each chunk markdown -> HTML. Chunks whose rendered HTML exceeds the 4096 UTF-16 wire limit (tags and entity escaping expand the text unpredictably) are re-split with a smaller source budget; the terminal fallback is escaped plain text hard-split by UTF-16, so a reply is never lost to "message is too long".
func NewTelegramRenderer ¶ added in v0.10.0
func NewTelegramRenderer(logger *slog.Logger) *TelegramRenderer
NewTelegramRenderer builds the Telegram HTML renderer.
func (*TelegramRenderer) RenderCaption ¶ added in v0.10.1
RenderCaption fits the start of the markdown response into Telegram's caption budget measured on the RENDERED HTML (markdown length is a poor proxy: tags and entity escaping expand the text). The split point shrinks by the observed expansion ratio until the HTML fits; overflow markdown is returned for the caller to send as follow-up text. The caption is always valid HTML — never raw markdown.
func (*TelegramRenderer) RenderSafeRichCaption ¶ added in v0.11.0
RenderSafeRichCaption keeps generated-media captions on the legacy media envelope while applying the same active-content policy as Rich Message output. Model-authored images, unsafe links and direct/bare mentions remain visible text but cannot become Telegram entities.
type TelegramTransport ¶ added in v0.10.0
type TelegramTransport struct {
// contains filtered or unexported fields
}
TelegramTransport adapts the Telegram Bot API to the neutral Transport interface. It wraps the same telegram.BotAPI the bot already holds, so the Telegram send path is byte-identical to the pre-seam code.
func NewTelegramTransport ¶ added in v0.10.0
func NewTelegramTransport(api telegram.BotAPI, cfg *config.Config, translator *i18n.Translator, logger *slog.Logger) *TelegramTransport
NewTelegramTransport builds the Telegram output adapter.
func (*TelegramTransport) AllowlistConfigured ¶ added in v0.10.0
func (t *TelegramTransport) AllowlistConfigured() bool
func (*TelegramTransport) Capabilities ¶ added in v0.10.0
func (t *TelegramTransport) Capabilities() Capabilities
func (*TelegramTransport) IsAllowed ¶ added in v0.10.0
func (t *TelegramTransport) IsAllowed(nativeSenderID string) bool
func (*TelegramTransport) Kind ¶ added in v0.10.0
func (t *TelegramTransport) Kind() string
func (*TelegramTransport) SendMedia ¶ added in v0.10.0
func (t *TelegramTransport) SendMedia(ctx context.Context, m OutgoingMedia) (string, error)
SendMedia delivers a batch of files as Telegram photos and/or documents. It preserves the legacy send_media_response.go classification and caption policy for zero-kind items: payloads over the configured document threshold (or forced via AsDocument) go as documents preserving resolution. An explicit WireKind always wins over those heuristics. Photo and document kinds can't share a media group, so they're sent as separate batches. Each homogeneous batch is additionally capped at Telegram's ten-item limit. The caption (rendered to HTML) rides the first batch; the reply-to anchors that caption-bearing batch only.
func (*TelegramTransport) SendMediaPersistent ¶ added in v0.11.0
func (t *TelegramTransport) SendMediaPersistent(ctx context.Context, m OutgoingMedia) (persistentSendResult, error)
SendMediaPersistent executes exactly one Bot API media call and returns all stable IDs from that call. V2 planners must pre-split mixed photo/document sets and batches above Telegram's group limit before entering the ledger's non-idempotent sending state.
func (*TelegramTransport) SendRichMedia ¶ added in v0.11.0
func (t *TelegramTransport) SendRichMedia(ctx context.Context, m OutgoingRichMedia) (string, error)
SendRichMedia atomically uploads trusted generated-photo groups and persists them together with the complete Rich HTML response. One photo is a bare image block, 2-4 photos form a collage and 5-10 form a slideshow.
The photo block is injected after model Markdown has passed the allowlisted Rich HTML renderer. Consequently model-authored URLs can never become media side effects; only the bytes resolved from GeneratedArtifactIDs arrive here.
func (*TelegramTransport) SendText ¶ added in v0.10.0
func (t *TelegramTransport) SendText(ctx context.Context, r OutgoingResponse) (string, error)
SendText sends one rendered HTML chunk. Two last-resort recoveries keep a reply from being lost (both should be unreachable with a correct renderer, and are surfaced as bot.anomaly.* span attributes when they fire):
- "can't parse entities": retried once as plain text (ParseMode cleared);
- "message is too long": resent as plain text hard-split by UTF-16.
Both retries use a fresh, non-cancellable context.
func (*TelegramTransport) SendTextPersistent ¶ added in v0.11.0
func (t *TelegramTransport) SendTextPersistent(ctx context.Context, r OutgoingResponse) (string, error)
SendTextPersistent performs exactly one Bot API request and returns its stable ID. Delivery plans use it instead of SendText's compatibility retries, because a ledger operation must never conceal a second non-idempotent request.
func (*TelegramTransport) SendTyping ¶ added in v0.10.0
func (t *TelegramTransport) SendTyping(ctx context.Context, conversationID string) error
func (*TelegramTransport) SetReaction ¶ added in v0.10.0
func (t *TelegramTransport) SetReaction(ctx context.Context, conversationID, messageID, emoji string) error
SetReaction adds the given emoji reaction to the message. The emoji must be one of telegramReactionEmoji (API-exact form).
type Transport ¶ added in v0.10.0
type Transport interface {
// SendText transmits one rendered chunk, returning the new message id.
SendText(ctx context.Context, r OutgoingResponse) (msgID string, err error)
// SendMedia transmits a batch of files with a shared caption, returning the
// primary message id. Callers gate on Capabilities().SupportsMedia.
SendMedia(ctx context.Context, m OutgoingMedia) (msgID string, err error)
// SendTyping shows a best-effort typing indicator in the conversation.
SendTyping(ctx context.Context, conversationID string) error
// SetReaction adds an emoji reaction to a message (best-effort; no-op if
// unsupported). The emoji must come from Capabilities().AvailableReactions —
// transport-native form (unicode for Telegram, shortcode for Mattermost).
SetReaction(ctx context.Context, conversationID, messageID, emoji string) error
Kind() string
Capabilities() Capabilities
// IsAllowed reports whether the native sender id is in the static allowlist.
IsAllowed(nativeSenderID string) bool
// AllowlistConfigured reports whether a static allowlist is configured at all.
// It distinguishes "empty allowlist" (IsAllowed always false, fail-closed in
// simple mode) from "allowlist used as an optional subset filter" in SSO mode,
// where an empty list means "all trusted senders" rather than "no one".
AllowlistConfigured() bool
}
Transport is the output + identity surface a chat backend must implement. Ingestion is handled per-transport (each maps its native updates into IncomingMessage and feeds Bot.HandleIncoming); only sending, typing, reactions, capabilities, and the per-transport allowlist live here.
Source Files
¶
- artifact_delivery_plan.go
- artifact_delivery_response.go
- artifact_inventory.go
- bot.go
- content.go
- delivery_plan.go
- file_handler.go
- generated_delivery_layout.go
- generated_delivery_plan.go
- generated_media_layout.go
- identity.go
- ingress_metrics.go
- injection.go
- message_grouper.go
- metrics.go
- model_output_sanitizer.go
- principal_resolver.go
- privacy.go
- process_group.go
- react.go
- response_path.go
- rich_delivery.go
- rich_file_pipeline.go
- rich_ingress.go
- rich_metrics.go
- rich_streaming.go
- send_media_response.go
- streaming.go
- transport.go
- transport_mattermost.go
- transport_telegram.go
- types.go