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
- Variables
- func Generate(cred *RemoteOTPCredential, now time.Time) (string, time.Time, error)
- func NormalizeBase32Secret(s string) (string, error)
- func NormalizeTargetCall(s string) (string, error)
- func ValidateActionName(s string) error
- func ValidateBase32Secret(s string) error
- type CredStore
- func (s *CredStore) Create(ctx context.Context, c *RemoteOTPCredential) error
- func (s *CredStore) Delete(ctx context.Context, id uint) error
- func (s *CredStore) Get(ctx context.Context, id uint) (*RemoteOTPCredential, error)
- func (s *CredStore) List(ctx context.Context) ([]RemoteOTPCredential, error)
- func (s *CredStore) TouchLastUsed(ctx context.Context, id uint, when time.Time) error
- func (s *CredStore) Update(ctx context.Context, c *RemoteOTPCredential) error
- func (s *CredStore) UsedBy(ctx context.Context) (map[uint][]string, error)
- type MacroStore
- func (s *MacroStore) Create(ctx context.Context, m *RemoteActionMacro) error
- func (s *MacroStore) Delete(ctx context.Context, id uint) error
- func (s *MacroStore) Get(ctx context.Context, id uint) (*RemoteActionMacro, error)
- func (s *MacroStore) ListByTarget(ctx context.Context, targetCall string) ([]RemoteActionMacro, error)
- func (s *MacroStore) Reorder(ctx context.Context, targetCall string, ids []uint) error
- func (s *MacroStore) Update(ctx context.Context, m *RemoteActionMacro) error
- type RemoteActionMacro
- type RemoteOTPCredential
- type Service
- type ServiceConfig
Constants ¶
const MaxActionNameLen = actions.MaxActionNameLen
MaxActionNameLen mirrors the inbound parser's hard limit.
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.
const MaxLabelLen = 64
MaxLabelLen is the macro tile label cap. Roughly fits a one-line drawer button on mobile.
Variables ¶
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 ¶
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 ¶
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 ¶
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 ¶
ValidateActionName accepts a name that the inbound parser would also accept. Empty string is rejected.
func ValidateBase32Secret ¶
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 (*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 ¶
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 ¶
Get fetches by primary key. Returns gorm.ErrRecordNotFound (use errors.Is) when missing.
func (*CredStore) List ¶
func (s *CredStore) List(ctx context.Context) ([]RemoteOTPCredential, error)
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 ¶
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 ¶
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 ¶
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) Macros ¶
func (s *Service) Macros() *MacroStore