Join one FM-Funknetz talk group from a headset window.
The station reads ~/.funknetz.json, speaks protocol 2.0, and builds an unsigned funknetz.app. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
package station
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"encoding/json/jsontext"
|
||||
jsonv2 "encoding/json/v2"
|
||||
)
|
||||
|
||||
type loaded struct {
|
||||
host string
|
||||
port int
|
||||
callsign string
|
||||
authKey string
|
||||
talkGroup int
|
||||
}
|
||||
|
||||
type fileShape struct {
|
||||
Host jsontext.Value `json:"host"`
|
||||
Port jsontext.Value `json:"port"`
|
||||
Callsign jsontext.Value `json:"callsign"`
|
||||
AuthKey jsontext.Value `json:"authKey"`
|
||||
TalkGroup jsontext.Value `json:"talkGroup"`
|
||||
}
|
||||
|
||||
type templateFile struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Callsign string `json:"callsign"`
|
||||
AuthKey string `json:"authKey"`
|
||||
TalkGroup int `json:"talkGroup"`
|
||||
}
|
||||
|
||||
var callsignPattern = regexp.MustCompile(`^[A-Z0-9][A-Z]{0,2}\d[A-Z0-9]{0,3}[A-Z](?:-[A-Z0-9]{1,3})?$`)
|
||||
|
||||
func loadConfig(path string) (loaded, error) {
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err := writeTemplate(path); err != nil {
|
||||
return loaded{}, err
|
||||
}
|
||||
return loaded{}, fmt.Errorf("created %s", path)
|
||||
}
|
||||
return loaded{}, fmt.Errorf("could not be read")
|
||||
}
|
||||
return parseConfig(body)
|
||||
}
|
||||
|
||||
func writeTemplate(path string) error {
|
||||
body, err := jsonv2.Marshal(templateFile{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = append(body, '\n')
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, werr := f.Write(body)
|
||||
cerr := f.Close()
|
||||
if werr != nil {
|
||||
return werr
|
||||
}
|
||||
if cerr != nil {
|
||||
return cerr
|
||||
}
|
||||
return os.Chmod(path, 0o600)
|
||||
}
|
||||
|
||||
func parseConfig(body []byte) (loaded, error) {
|
||||
var shape fileShape
|
||||
if err := jsonv2.Unmarshal(body, &shape); err != nil {
|
||||
return loaded{}, fmt.Errorf("could not be read")
|
||||
}
|
||||
host, err := stringField("host", shape.Host)
|
||||
if err != nil {
|
||||
return loaded{}, err
|
||||
}
|
||||
port, err := intField("port", shape.Port, 1, 65535)
|
||||
if err != nil {
|
||||
return loaded{}, err
|
||||
}
|
||||
call, err := stringField("callsign", shape.Callsign)
|
||||
if err != nil {
|
||||
return loaded{}, err
|
||||
}
|
||||
key, err := authField(shape.AuthKey)
|
||||
if err != nil {
|
||||
return loaded{}, err
|
||||
}
|
||||
tg, err := intField("talkGroup", shape.TalkGroup, 1, int(math.MaxUint32))
|
||||
if err != nil {
|
||||
return loaded{}, err
|
||||
}
|
||||
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
return loaded{}, fmt.Errorf("host is empty")
|
||||
}
|
||||
call = strings.ToUpper(strings.TrimSpace(call))
|
||||
if call == "" {
|
||||
return loaded{}, fmt.Errorf("callsign is empty")
|
||||
}
|
||||
if !callsignPattern.MatchString(call) {
|
||||
return loaded{}, fmt.Errorf("callsign is invalid")
|
||||
}
|
||||
return loaded{host: host, port: port, callsign: call, authKey: key, talkGroup: tg}, nil
|
||||
}
|
||||
|
||||
func stringField(name string, v jsontext.Value) (string, error) {
|
||||
if len(v) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
if v.Kind() != jsontext.KindString {
|
||||
return "", fmt.Errorf("%s has the wrong type", name)
|
||||
}
|
||||
var s string
|
||||
if err := jsonv2.Unmarshal(v, &s); err != nil {
|
||||
return "", fmt.Errorf("%s has the wrong type", name)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func authField(v jsontext.Value) (string, error) {
|
||||
s, err := stringField("authKey", v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return "", fmt.Errorf("authKey is empty")
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func intField(name string, v jsontext.Value, min, max int) (int, error) {
|
||||
if len(v) == 0 {
|
||||
return 0, fmt.Errorf("%s is invalid", name)
|
||||
}
|
||||
if v.Kind() != jsontext.KindNumber {
|
||||
return 0, fmt.Errorf("%s has the wrong type", name)
|
||||
}
|
||||
var n int
|
||||
if err := jsonv2.Unmarshal(v, &n); err != nil {
|
||||
return 0, fmt.Errorf("%s is invalid", name)
|
||||
}
|
||||
if n < min || n > max {
|
||||
return 0, fmt.Errorf("%s is invalid", name)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
Reference in New Issue
Block a user