Skip to content

Commit

Permalink
Support mappings from TOML files (#126)
Browse files Browse the repository at this point in the history
* Add initial support for loading mappings files

* Add support for reloading mappings via api

* Inject config mappings into existing mappings check

* Add new -read cli flag

* Actually include config mappings in mappings processing
  • Loading branch information
wizzomafizzo authored Dec 26, 2024
1 parent 4699e9e commit 6a7a7b7
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 52 deletions.
79 changes: 79 additions & 0 deletions pkg/api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,82 @@ func LocalClient(

return string(b), nil
}

func WaitNotification(
cfg *config.Instance,
id string,
) (string, error) {
u := url.URL{
Scheme: "ws",
Host: "localhost:" + strconv.Itoa(cfg.ApiPort()),
Path: "/api/v1.0",
}

c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
return "", err
}
defer func(c *websocket.Conn) {
err := c.Close()
if err != nil {
log.Warn().Err(err).Msg("error closing websocket")
}
}(c)

done := make(chan struct{})
var resp *models.RequestObject

go func() {
defer close(done)
for {
_, message, err := c.ReadMessage()
if err != nil {
log.Error().Err(err).Msg("error reading message")
return
}

var m models.RequestObject
err = json.Unmarshal(message, &m)
if err != nil {
continue
}

if m.JsonRpc != "2.0" {
log.Error().Msg("invalid jsonrpc version")
continue
}

if m.Id != nil {
continue
}

if m.Method != id {
continue
}

resp = &m

return
}
}()

timer := time.NewTimer(api.RequestTimeout)
select {
case <-done:
break
case <-timer.C:
return "", ErrRequestTimeout
}

if resp == nil {
return "", ErrRequestTimeout
}

var b []byte
b, err = json.Marshal(resp.Params)
if err != nil {
return "", err
}

return string(b), nil
}
15 changes: 15 additions & 0 deletions pkg/api/methods/mappings.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"errors"
"github.com/ZaparooProject/zaparoo-core/pkg/api/models"
"github.com/ZaparooProject/zaparoo-core/pkg/api/models/requests"
"github.com/ZaparooProject/zaparoo-core/pkg/platforms"
"path/filepath"
"regexp"
"strconv"
"time"
Expand Down Expand Up @@ -215,3 +217,16 @@ func HandleUpdateMapping(env requests.RequestEnv) (any, error) {

return nil, nil
}

func HandleReloadMappings(env requests.RequestEnv) (any, error) {
log.Info().Msg("received reload mappings request")

mapDir := filepath.Join(env.Platform.DataDir(), platforms.MappingsDir)
err := env.Config.LoadMappings(mapDir)
if err != nil {
log.Error().Err(err).Msg("error loading mappings")
return nil, errors.New("error loading mappings")
}

return nil, nil
}
1 change: 1 addition & 0 deletions pkg/api/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
MethodMappingsNew = "mappings.new"
MethodMappingsDelete = "mappings.delete"
MethodMappingsUpdate = "mappings.update"
MethodMappingsReload = "mappings.reload"
MethodReadersWrite = "readers.write"
MethodStatus = "status"
MethodVersion = "version"
Expand Down
1 change: 1 addition & 0 deletions pkg/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ var methodMap = map[string]func(requests.RequestEnv) (any, error){
models.MethodMappingsNew: methods.HandleAddMapping,
models.MethodMappingsDelete: methods.HandleDeleteMapping,
models.MethodMappingsUpdate: methods.HandleUpdateMapping,
models.MethodMappingsReload: methods.HandleReloadMappings,
// readers
models.MethodReadersWrite: methods.HandleReaderWrite,
// utils
Expand Down
108 changes: 84 additions & 24 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ import (
"github.com/rs/zerolog/log"
"io"
"os"
"os/signal"
"strings"
"syscall"
)

type Flags struct {
Write *string
Read *bool
Run *string
Launch *string
Api *string
Clients *bool
Expand All @@ -35,38 +39,48 @@ func SetupFlags() *Flags {
Write: flag.String(
"write",
"",
"write text to tag using connected reader",
"write value to next scanned token",
),
Read: flag.Bool(
"read",
false,
"print next scanned token without running",
),
Run: flag.String(
"run",
"",
"run value directly as ZapScript",
),
Launch: flag.String(
"launch",
"",
"launch text as if it were a scanned token",
"alias of run (DEPRECATED)",
),
Api: flag.String(
"api",
"",
"send method and params to API and print response",
),
Clients: flag.Bool(
"clients",
false,
"list all registered API clients and secrets",
),
NewClient: flag.String(
"new-client",
"",
"register new API client with given display name",
),
DeleteClient: flag.String(
"delete-client",
"",
"revoke access to API for given client ID",
),
Qr: flag.Bool(
"qr",
false,
"output a connection QR code along with client details",
),
//Clients: flag.Bool(
// "clients",
// false,
// "list all registered API clients and secrets",
//),
//NewClient: flag.String(
// "new-client",
// "",
// "register new API client with given display name",
//),
//DeleteClient: flag.String(
// "delete-client",
// "",
// "revoke access to API for given client ID",
//),
//Qr: flag.Bool(
// "qr",
// false,
// "output a connection QR code along with client details",
//),
Version: flag.Bool(
"version",
false,
Expand Down Expand Up @@ -112,9 +126,55 @@ func (f *Flags) Post(cfg *config.Instance) {
} else {
os.Exit(0)
}
} else if *f.Launch != "" {
} else if *f.Read {
enableRun := func() {
_, err := client.LocalClient(
cfg,
models.MethodSettingsUpdate,
"{\"launchingActive\":true}",
)
if err != nil {
log.Error().Err(err).Msg("error re-enabling run")
_, _ = fmt.Fprintf(os.Stderr, "Error re-enabling run: %v\n", err)
os.Exit(1)
}
}

_, err := client.LocalClient(
cfg,
models.MethodSettingsUpdate,
"{\"launchingActive\":false}",
)
if err != nil {
log.Error().Err(err).Msg("error disabling run")
_, _ = fmt.Fprintf(os.Stderr, "Error disabling run: %v\n", err)
os.Exit(1)
}

// cleanup after ctrl-c
sigs := make(chan os.Signal, 1)
defer close(sigs)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigs
enableRun()
os.Exit(0)
}()

resp, err := client.WaitNotification(cfg, models.TokensActive)
if err != nil {
log.Error().Err(err).Msg("error waiting for notification")
_, _ = fmt.Fprintf(os.Stderr, "Error waiting for notification: %v\n", err)
enableRun()
os.Exit(1)
}

enableRun()
fmt.Println(resp)
os.Exit(0)
} else if *f.Run != "" || *f.Launch != "" {
data, err := json.Marshal(&models.LaunchParams{
Text: f.Launch,
Text: f.Run,
})
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Error encoding params: %v\n", err)
Expand Down
68 changes: 68 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Values struct {
Launchers Launchers `toml:"launchers,omitempty"`
ZapScript ZapScript `toml:"zapscript,omitempty"`
Service Service `toml:"service,omitempty"`
Mappings Mappings `toml:"mappings,omitempty"`
}

type Audio struct {
Expand Down Expand Up @@ -77,6 +78,16 @@ type Service struct {
AllowLaunch []string `toml:"allow_launch,omitempty,multiline"`
}

type MappingsEntry struct {
TokenKey string `toml:"token_key,omitempty"`
MatchPattern string `toml:"match_pattern"`
ZapScript string `toml:"zapscript"`
}

type Mappings struct {
Entry []MappingsEntry `toml:"entry,omitempty"`
}

var BaseDefaults = Values{
ConfigSchema: SchemaVersion,
Audio: Audio{
Expand Down Expand Up @@ -346,3 +357,60 @@ func (c *Instance) IsShellCmdAllowed(cmd string) bool {
}
return false
}

func (c *Instance) LoadMappings(mappingsDir string) error {
c.mu.Lock()
defer c.mu.Unlock()

_, err := os.Stat(mappingsDir)
if err != nil {
return err
}

mapFiles, err := os.ReadDir(mappingsDir)
if err != nil {
return err
}

filesCounts := 0
mappingsCount := 0

for _, mapFile := range mapFiles {
if mapFile.IsDir() {
continue
}

if filepath.Ext(mapFile.Name()) != ".toml" {
continue
}

mapPath := filepath.Join(mappingsDir, mapFile.Name())
log.Debug().Msgf("loading mapping file: %s", mapPath)

data, err := os.ReadFile(mapPath)
if err != nil {
return err
}

var newVals Values
err = toml.Unmarshal(data, &newVals)
if err != nil {
return err
}

c.vals.Mappings.Entry = append(c.vals.Mappings.Entry, newVals.Mappings.Entry...)

filesCounts++
mappingsCount += len(newVals.Mappings.Entry)
}

log.Info().Msgf("loaded %d mapping files, %d mappings", filesCounts, mappingsCount)

return nil
}

func (c *Instance) Mappings() []MappingsEntry {
c.mu.RLock()
defer c.mu.RUnlock()
return c.vals.Mappings.Entry
}
Loading

0 comments on commit 6a7a7b7

Please sign in to comment.