callbacks

package
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: May 22, 2026 License: MIT Imports: 6 Imported by: 0

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

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

	// NSNav is the namespace for cross-cutting navigation (back
	// 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.

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

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

func Encode(ns, action string, args ...string) string

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

func Decode(raw string) (Data, error)

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

func NewShortMap(ttl time.Duration) *ShortMap

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

func (m *ShortMap) Get(id string) (string, bool)

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

func (m *ShortMap) Put(value string) string

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

func (m *ShortMap) Size() int

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.

Jump to

Keyboard shortcuts

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