remoteactions

package
v0.13.3 Latest Latest
Warning

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

Go to latest
Published: May 9, 2026 License: GPL-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package remoteactions implements the OUTBOUND half of the Actions feature: operator-curated macros and TOTP credentials used to fire `@@<otp>#<action> [k=v ...]` invocations at remote stations from inside Messages.

This package is a sibling of pkg/actions/ (the inbound Actions runner), not a fork. The two share nothing at runtime: outbound macros never enter the inbound classifier, and inbound invocations never read remote credentials. They share only the wire grammar, which is owned by pkg/actions/parser.go and re-used here for validation parity.

Schema: see migration 16 in pkg/configstore/migrate_remote_actions.go. Models in this package's models.go are the in-memory shape; they are intentionally NOT registered with AutoMigrate.

Composition root: Service (service.go), constructed from pkg/app/wiring.go after the messages.Service is wired. Failure to construct is non-fatal — the REST handlers return 503 and the UI drawer reads as empty.

Index

Constants

View Source
const MaxActionNameLen = actions.MaxActionNameLen

MaxActionNameLen mirrors the inbound parser's hard limit.

View Source
const MaxArgsStringLen = 200

MaxArgsStringLen is a generous cap on the args portion of a macro. The wire-format budget check happens at fire time; this is a belt-and-suspenders ceiling on what the DB will store.

View Source
const MaxLabelLen = 64

MaxLabelLen is the macro tile label cap. Roughly fits a one-line drawer button on mobile.

Variables

View Source
var ErrReorderUnknownID = errors.New("remoteactions: reorder list contains unknown id")

ErrReorderUnknownID is returned when Reorder receives an id that does not name a macro for the supplied targetCall (deleted, wrong target, or never existed). Mapped to HTTP 400 by the handler.

Functions

func Generate

func Generate(cred *RemoteOTPCredential, now time.Time) (string, time.Time, error)

Generate computes the current TOTP code for cred at the given moment and returns (code, nextStepBoundary). The next-step time is the inclusive upper edge of the current TOTP window — used by the UI countdown so the picker shows "next in Ns" without a separate query.

Defaults for zero-valued fields:

Algorithm "" -> SHA1
Digits   0  -> 6
Period   0  -> 30

SecretB32 may be lowercase or include whitespace; the function normalizes via NormalizeBase32Secret. An invalid secret returns an error rather than silently substituting.

func NormalizeBase32Secret

func NormalizeBase32Secret(s string) (string, error)

NormalizeBase32Secret strips whitespace, uppercases, and validates that the result decodes as base32 (with or without padding). Returns the uppercased no-whitespace form on success — this is the form stored in the SecretB32 column.

func NormalizeTargetCall

func NormalizeTargetCall(s string) (string, error)

NormalizeTargetCall uppercases and validates a target callsign. Accepts the same shape as APRS addressees: 1..6 alphanumeric base call, optional `-` SSID of 1..2 chars. Total max 9 chars (callsign- SSID with separator).

func ValidateActionName

func ValidateActionName(s string) error

ValidateActionName accepts a name that the inbound parser would also accept. Empty string is rejected.

func ValidateBase32Secret

func ValidateBase32Secret(s string) error

ValidateBase32Secret reports whether s decodes as a base32 TOTP secret. Whitespace and padding are tolerated; case is normalized. Length must be at least 16 chars after normalization (covers all RFC 6238 secret lengths used in practice — 80 bits = 16 base32 chars for SHA1; longer for SHA256/SHA512).

Types

type CredStore

type CredStore struct {
	// contains filtered or unexported fields
}

CredStore is the persistence layer for RemoteOTPCredential rows. One instance per Service; safe for concurrent use (delegates to gorm.DB which is goroutine-safe).

func NewCredStore

func NewCredStore(db *gorm.DB) *CredStore

func (*CredStore) Create

func (s *CredStore) Create(ctx context.Context, c *RemoteOTPCredential) error

Create inserts a new credential, stamping CreatedAt to time.Now().UTC(). Returns the unique-constraint error from SQLite verbatim when Name collides; callers map that to HTTP 409.

func (*CredStore) Delete

func (s *CredStore) Delete(ctx context.Context, id uint) error

Delete removes the credential. Macros bound to it have their remote_otp_credential_id nulled by the FK ON DELETE SET NULL.

func (*CredStore) Get

func (s *CredStore) Get(ctx context.Context, id uint) (*RemoteOTPCredential, error)

Get fetches by primary key. Returns gorm.ErrRecordNotFound (use errors.Is) when missing.

func (*CredStore) List

List returns every credential ordered by Name. The list is small (one or two per remote station the operator interacts with) so a full scan is fine.

func (*CredStore) TouchLastUsed

func (s *CredStore) TouchLastUsed(ctx context.Context, id uint, when time.Time) error

TouchLastUsed records the most recent moment a credential generated a TOTP code. UTC always; the UI sorts the picker by this column so recently-used credentials float to the top.

func (*CredStore) Update

func (s *CredStore) Update(ctx context.Context, c *RemoteOTPCredential) error

Update writes the row identified by c.ID. The fields touched are Name, SecretB32, Algorithm, Digits, Period — caller is responsible for validation. CreatedAt and LastUsedAt are not modified. Returns gorm.ErrRecordNotFound when no row matches c.ID so callers can map to HTTP 404.

func (*CredStore) UsedBy

func (s *CredStore) UsedBy(ctx context.Context) (map[uint][]string, error)

UsedBy returns a map of credential id -> distinct uppercased target callsigns whose macros reference it. One scan over the macros table; the REST list endpoint joins this map onto the credential rows so the deletion gate ("Unbind from N macro(s) first") can render without a per-credential query.

type MacroStore

type MacroStore struct {
	// contains filtered or unexported fields
}

MacroStore is the persistence layer for RemoteActionMacro rows. One instance per Service; safe for concurrent use.

func NewMacroStore

func NewMacroStore(db *gorm.DB) *MacroStore

func (*MacroStore) Create

func (s *MacroStore) Create(ctx context.Context, m *RemoteActionMacro) error

Create inserts a new macro, stamping CreatedAt and UpdatedAt. TargetCall must already be uppercased — validation lives in the caller (validate.go).

func (*MacroStore) Delete

func (s *MacroStore) Delete(ctx context.Context, id uint) error

Delete removes one macro by id.

func (*MacroStore) Get

func (s *MacroStore) Get(ctx context.Context, id uint) (*RemoteActionMacro, error)

Get fetches by primary key. Returns gorm.ErrRecordNotFound when missing.

func (*MacroStore) ListByTarget

func (s *MacroStore) ListByTarget(ctx context.Context, targetCall string) ([]RemoteActionMacro, error)

ListByTarget returns macros for one peer, ordered by Position ascending then ID ascending (tie-break for ties; defensive against reorder races).

func (*MacroStore) Reorder

func (s *MacroStore) Reorder(ctx context.Context, targetCall string, ids []uint) error

Reorder rewrites the Position column of every macro for one targetCall to match the supplied id order (index 0 -> position 0, index 1 -> position 1, ...). Runs in a single transaction so any failure rolls the change back atomically. Every id in ids must resolve to a row for targetCall: an unknown id returns ErrReorderUnknownID and rolls back. Macros for the target that are NOT in ids keep their prior position — caller is responsible for passing the full live id set if it wants total reordering.

func (*MacroStore) Update

func (s *MacroStore) Update(ctx context.Context, m *RemoteActionMacro) error

Update writes Label, ActionName, ArgsString, RemoteOTPCredentialID, Position, and bumps UpdatedAt. TargetCall is intentionally NOT updatable — the drawer is per-thread; moving a macro between targets is a delete+create. Returns gorm.ErrRecordNotFound when no row matches m.ID so callers can map to HTTP 404.

type RemoteActionMacro

type RemoteActionMacro struct {
	ID                    uint   `gorm:"primaryKey"`
	TargetCall            string `gorm:"size:9;not null;index:idx_remote_action_macros_target_call"`
	Label                 string `gorm:"size:64;not null"`
	ActionName            string `gorm:"size:32;not null"`
	ArgsString            string `gorm:"type:text;not null;default:''"`
	RemoteOTPCredentialID *uint  `gorm:"column:remote_otp_credential_id"`
	Position              int    `gorm:"not null;default:0"`
	CreatedAt             time.Time
	UpdatedAt             time.Time
}

RemoteActionMacro is one saved (target, action, args, optional credential) tuple shown as a tile in the Messages drawer.

TargetCall is uppercased on write (validate.go); the column is plain TEXT with an index so per-thread lookup is a single seek.

RemoteOTPCredentialID is nullable: when nil the macro fires in manual-OTP mode (operator types six digits before SEND). When set, the FK has ON DELETE SET NULL — deleting a credential demotes its macros instead of cascading.

Position is the drag-reorder index, low first. Conflicts (two macros for the same target with the same position) are resolved by the store's reorder helper, not at the SQL layer.

func (*RemoteActionMacro) BeforeSave

func (m *RemoteActionMacro) BeforeSave(_ *gorm.DB) error

BeforeSave normalizes ActionName to uppercase so outbound fires send the canonical wire form. The receiver treats names case-insensitively (see pkg/configstore Action.BeforeSave) but storing the canonical uppercase form here keeps the audit/inspection surface consistent.

This hook fires for Create. The Update path uses gorm's map-based Updates which bypasses model hooks; the webapi handler uppercases in.ActionName before calling MacroStore.Update.

func (*RemoteActionMacro) TableName

func (*RemoteActionMacro) TableName() string

type RemoteOTPCredential

type RemoteOTPCredential struct {
	ID         uint   `gorm:"primaryKey"`
	Name       string `gorm:"uniqueIndex;size:64;not null"`
	SecretB32  string `gorm:"size:128;not null"`
	Algorithm  string `gorm:"size:16;not null;default:'sha1'"`
	Digits     int    `gorm:"not null;default:6"`
	Period     int    `gorm:"not null;default:30"`
	CreatedAt  time.Time
	LastUsedAt *time.Time
}

RemoteOTPCredential is one named TOTP secret used to fire macros at a remote station. Stored plaintext per the single-user-station design; the REST list endpoints scrub SecretB32 from the wire shape.

Name follows the convention "<CALLSIGN> OTP" (e.g. "NW5W OTP") but the column is just UNIQUE TEXT — operators can use whatever string they want.

Algorithm/Digits/Period default at the SQL layer (sha1 / 6 / 30) so a row inserted with zero values still works.

func (*RemoteOTPCredential) TableName

func (*RemoteOTPCredential) TableName() string

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is the composition root for the outbound-Actions package. It owns the two stores and exposes them to REST handlers via Creds() and Macros(). One instance per graywolf process.

Failure to construct (currently only "DB == nil") is non-fatal at the wiring layer; the REST handlers fall back to 503 when the service is missing.

func NewService

func NewService(cfg ServiceConfig) (*Service, error)

NewService constructs the service. Returns an error when DB is nil so the wiring layer can log and skip rather than panic.

func (*Service) Creds

func (s *Service) Creds() *CredStore

func (*Service) Macros

func (s *Service) Macros() *MacroStore

type ServiceConfig

type ServiceConfig struct {
	DB     *gorm.DB     // required
	Logger *slog.Logger // optional; nil -> slog.Default()
}

ServiceConfig wires the service to its dependencies.

Jump to

Keyboard shortcuts

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