Documentation
¶
Overview ¶
Package callbacks owns the inline-keyboard callback protocol that glues every Telegram tap to the matching domain action.
The protocol is a colon-delimited string the keyboard layer stamps into [models.InlineKeyboardButton.CallbackData] and the dispatcher parses on every incoming tap:
<namespace>:<action>[:<arg>[:<arg>…]]
Telegram caps callback_data at 64 bytes (MaxCallbackDataBytes), so keyboards that need to round-trip long values (full file paths, IANA timezone names, Shortcut titles) park the value in a ShortMap and embed only the short id.
The package exposes:
- Encode / Decode — the wire format helpers.
- Data — the parsed form handlers receive.
- The NS… constants and AllNamespaces — the namespace vocabulary every keyboard and handler agrees on.
- ShortMap (shortmap.go) — the side table for overflow values.
Index ¶
Constants ¶
const ( // NSSound is the namespace for volume / mute / say callbacks. // Handler: internal/telegram/handlers/snd.go. NSSound = "snd" // NSDisplay is the namespace for brightness and screensaver // callbacks. Handler: internal/telegram/handlers/dsp.go. NSDisplay = "dsp" // NSPower is the namespace for sleep / lock / restart / // shutdown / caffeinate callbacks. Handler: // internal/telegram/handlers/pwr.go. NSPower = "pwr" // NSWifi is the namespace for Wi-Fi state, join, info, DNS // presets, and speed test callbacks. Handler: // internal/telegram/handlers/wif.go. NSWifi = "wif" // NSBT is the namespace for Bluetooth toggle and paired-device // callbacks. Handler: internal/telegram/handlers/bt.go. NSBT = "bt" // NSBattery is the namespace for battery state and health // callbacks. Handler: internal/telegram/handlers/bat.go. NSBattery = "bat" // NSSystem is the namespace for system info, memory, CPU, // process-list, and process-kill callbacks. Handler: // internal/telegram/handlers/sys.go. NSSystem = "sys" // NSMedia is the namespace for screenshot, screen-record, and // webcam callbacks. Handler: // internal/telegram/handlers/med.go. NSMedia = "med" // NSNotify is the namespace for desktop-notification and // text-to-speech callbacks. Handler: // internal/telegram/handlers/ntf.go. NSNotify = "ntf" // NSTools is the namespace for clipboard, timezone picker, // disks, time sync, and Shortcuts callbacks. Handler: // internal/telegram/handlers/tls.go. NSTools = "tls" // NSMusic is the namespace for now-playing metadata, // playback control, seek, and the embedded volume nudges // inside the 🎵 Music dashboard. Backed by the // nowplaying-cli brew formula. Handler: // internal/telegram/handlers/mus.go. NSMusic = "mus" // NSApps is the namespace for the running-app list, per-app // Quit / Force Quit / Hide actions, and the "Quit all // except…" multi-select inside the 🪟 Apps dashboard. // Handler: internal/telegram/handlers/apps.go. NSApps = "app" // to home, refresh). Handler: // internal/telegram/handlers/nav.go. NSNav = "nav" )
Namespace constants are the vocabulary every keyboard stamps and every dispatcher consults. Adding a new dashboard category means adding a constant here, listing it in AllNamespaces, and registering a handler. Values are short on purpose — they burn against the 64-byte callback_data budget.
const MaxCallbackDataBytes = 64
MaxCallbackDataBytes is Telegram's hard cap for [models.InlineKeyboardButton.CallbackData]. Both Encode and Decode enforce it: encoding panics on overflow because it signals a bug in the keyboard layer, while decoding rejects overflow as a malformed input.
Variables ¶
var AllNamespaces = []string{ NSSound, NSDisplay, NSPower, NSWifi, NSBT, NSBattery, NSSystem, NSMedia, NSNotify, NSTools, NSMusic, NSApps, NSNav, }
AllNamespaces is the canonical ordered list of every NS… constant. Used by the dispatcher's startup self-check (every namespace must have a registered handler) and by the keyboard layer's assertion tests.
Functions ¶
func Encode ¶
Encode renders ns, action, and args back to a callback_data string by joining them with single colons. The keyboard layer uses this when stamping every [models.InlineKeyboardButton].
Behavior:
- Joins parts as "ns:action[:arg[:arg…]]".
- Panics if the rendered string exceeds MaxCallbackDataBytes; that condition signals a bug in the keyboard caller (the args slice should have been moved through a ShortMap instead).
Returns the rendered string. Never returns an error — overflow is treated as programmer error, not runtime input.
Types ¶
type Data ¶
type Data struct {
// Namespace is the namespace prefix that selects the handler,
// e.g. "snd" for the sound dashboard. Always one of the NS…
// constants; the dispatcher rejects unknown values.
Namespace string
// Action is the verb inside the namespace, e.g. "up", "set",
// "dns-menu". Free-form per handler — no enum.
Action string
// Args carries zero or more colon-separated positional args
// pulled from the remainder of the callback data. Empty when
// the encoded string was just "ns:action".
Args []string
}
Data is the parsed form of a callback_data string. Handlers receive this after the dispatcher has matched the namespace and stripped it from the raw string.
Lifecycle:
- Constructed by Decode on every incoming update.callback_query and routed to the per-namespace handler keyed by Data.Namespace.
- Read-only from the handler's perspective; never mutated after parse.
Field roles:
- Namespace selects which handler runs (one per dashboard category, plus "nav" for cross-cutting navigation).
- Action selects the verb inside that handler — usually one of "open", "refresh", or a feature-specific token like "up", "down", "set", "dns-menu".
- Args carries any positional payload the keyboard stamped into the callback. Examples: a brightness step ("5"), a ShortMap id resolving to a long timezone name, a paginator page index.
func Decode ¶
Decode parses a callback_data string into Data. Called by the dispatcher on every incoming callback_query.
Behavior:
- Splits raw on ':' and assigns parts[0] → Namespace, parts[1] → Action, parts[2:] → Args.
- Returns an error (and a zero Data) when raw is empty, longer than MaxCallbackDataBytes, or contains fewer than two colon-separated parts.
Returns the parsed Data on success, otherwise zero Data and a non-nil error describing the malformed input.
type ShortMap ¶
type ShortMap struct {
// contains filtered or unexported fields
}
ShortMap is a TTL-bounded side table for callback-arg values that are too long to embed in the 64-byte MaxCallbackDataBytes budget directly.
Five users in the codebase right now: Bluetooth MACs (paired device list), Wi-Fi SSIDs (join + DNS reset), disk mount paths (Disks panel), Shortcuts names (Run Shortcut picker), IANA timezone names (Timezone picker). Each callback embeds only a short [Put]-returned id; the handler resolves it back via [Get] before acting.
Lifecycle:
- Constructed once at daemon startup via NewShortMap, stored on bot.Deps.ShortMap, and shared by every keyboard + handler for the process lifetime.
- Background eviction is opt-in via [StartJanitor]; without it, entries are pruned lazily on [Get] miss-after-expiry only.
- Tests may pin time by overriding [ShortMap.now] (see shortmap_test.go).
Concurrency:
- All access is serialised through mu. Safe to call any method from any goroutine, including from the janitor goroutine started by [StartJanitor].
Field roles:
- ttl is the per-entry expiry budget, applied at Put time (each entry expires at now+ttl). Falls back to 15 minutes when NewShortMap receives <= 0.
- items is the dispatch table keyed by the random id [newID] produces.
- now is the time source — pinned to time.Now in production, overridden in tests for deterministic expiry.
func NewShortMap ¶
NewShortMap returns a ShortMap with the given TTL applied to every entry it stores. A non-positive ttl is replaced with the safe default of 15 minutes.
Behavior:
- Initialises items as an empty map and now as time.Now.
- Does NOT spawn the eviction goroutine; callers that want proactive pruning must call ShortMap.StartJanitor separately. Without it, expired entries linger in memory until something attempts to [Get] them.
Returns a *ShortMap ready for concurrent use. Never returns an error.
func (*ShortMap) Get ¶
Get resolves an id back to the value originally stored by ShortMap.Put. Handlers call this on every callback that carries a short-id arg.
Behavior:
- Returns ("", false) when the id is unknown.
- Returns ("", false) AND deletes the entry when the id is known but its expiry has passed (lazy eviction).
- Returns (value, true) otherwise.
The handler is expected to render a "session expired — refresh the list" message on the (id known, expired) and (id unknown) cases — both look the same from this method's return.
func (*ShortMap) Put ¶
Put stores value under a freshly-generated short id and returns the id. The id is 10 base32 chars (~50 bits of entropy) — short enough to comfortably fit inside the 64-byte MaxCallbackDataBytes budget alongside the namespace, action, and any other args.
Behavior:
- Generates a fresh id via [newID]; collisions are astronomically unlikely but if one occurs the existing entry is overwritten silently.
- Marks the entry to expire at now()+ttl using the receiver's time source.
Returns the id string. Never returns an error.
func (*ShortMap) Size ¶
Size returns the current entry count, including not-yet-pruned expired entries. Provided for tests and debug instrumentation; production code shouldn't rely on it for correctness.
func (*ShortMap) StartJanitor ¶
func (m *ShortMap) StartJanitor(stop <-chan struct{})
StartJanitor spawns a background goroutine that calls ShortMap.Sweep every ttl/2 until stop is closed. Returns immediately.
Lifecycle:
- Daemon startup wires this to a process-lifetime stop channel so the janitor lives as long as the process.
- Tests pass a short-lived stop channel to bound the goroutine's lifetime to the test.
Behavior:
- Ticks every ttl/2 (so an entry is evicted within at most 1.5 × ttl after creation in the worst case).
- Stops cleanly when stop is closed; ticker is released via defer so no resource leaks.
- Each tick calls ShortMap.Sweep under the mutex; concurrent Get/Put callers serialise behind it.
func (*ShortMap) Sweep ¶
func (m *ShortMap) Sweep()
Sweep removes every expired entry in one pass. Safe to call periodically (the [StartJanitor] goroutine does exactly that).
Behavior:
- Snapshots time once via the receiver's now source, then deletes any entry whose expires field is before that snapshot.
- Holds the mutex for the full pass; the cost is O(n) over the current entry count.