diff --git a/.gitignore b/.gitignore index 75485c05..94c7d44c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -dayplan +/dayplan diff --git a/.scripts/build.sh b/.scripts/build.sh index efed9795..12828887 100755 --- a/.scripts/build.sh +++ b/.scripts/build.sh @@ -25,12 +25,12 @@ fi case ${1} in install) printf "installing version '%s' at '%s'...\n" "${version}" "${hash}" - go install -ldflags="-X '${source_root}/control/cli.version=${version}' -X '${source_root}/control/cli.hash=${hash}'" cmd/dayplan.go + go install -o dayplan -ldflags="-X '${source_root}/control/cli.version=${version}' -X '${source_root}/control/cli.hash=${hash}'" cmd/dayplan/main.go printf "done\n" ;; "") printf "building version '%s' at '%s'...\n" "${version}" "${hash}" - go build -v -ldflags="-X '${source_root}/control/cli.version=${version}' -X '${source_root}/control/cli.hash=${hash}'" cmd/dayplan.go + go build -o dayplan -v -ldflags="-X '${source_root}/control/cli.version=${version}' -X '${source_root}/control/cli.hash=${hash}'" cmd/dayplan/main.go printf "done\n" ;; *) diff --git a/cmd/dayplan.go b/cmd/dayplan/main.go similarity index 89% rename from cmd/dayplan.go rename to cmd/dayplan/main.go index 645e73e5..e501176e 100644 --- a/cmd/dayplan.go +++ b/cmd/dayplan/main.go @@ -18,7 +18,7 @@ func main() { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) // parse the flags - parser := flags.NewParser(&cli.Opts, flags.Default) + parser := flags.NewParser(&cli.DayplanOpts, flags.Default) parser.SubcommandsOptional = false _, err := parser.Parse() @@ -29,7 +29,7 @@ func main() { os.Exit(1) } - if cli.Opts.Version { + if cli.DayplanOpts.Version { cmd := cli.VersionCommand{} err := cmd.Execute([]string{}) if err != nil { diff --git a/cmd/dayplan_test.go b/cmd/dayplan/main_test.go similarity index 100% rename from cmd/dayplan_test.go rename to cmd/dayplan/main_test.go diff --git a/cmd/dayplan_intel_server/main.go b/cmd/dayplan_intel_server/main.go new file mode 100644 index 00000000..51f437c7 --- /dev/null +++ b/cmd/dayplan_intel_server/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "os" + + "github.com/jessevdk/go-flags" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "github.com/ja-he/dayplan/internal/control/cli" +) + +func main() { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + + var opts cli.DayplanIntelServerCommandLineOpts + parser := flags.NewParser(&opts, flags.Default) + parser.SubcommandsOptional = false + + _, err := parser.Parse() + if flags.WroteHelp(err) { + os.Exit(0) + } else if err != nil { + fmt.Fprintf(os.Stderr, "fatal error (e.g. flag parsing):\n > %s\n", err.Error()) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod index b33443b9..89f1ed5c 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,35 @@ module github.com/ja-he/dayplan -go 1.24 +go 1.24.0 require ( github.com/gdamore/tcell/v2 v2.7.0 - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/jessevdk/go-flags v1.5.0 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/nathan-osman/go-sunrise v1.1.0 github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.42.2 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.4 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/sys v0.36.0 // indirect golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect + modernc.org/libc v1.66.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 36c9518a..aa5f8b51 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,17 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.7.0 h1:I5LiGTQuwrysAt1KS9wg1yFfOI3arI3ucFrxtd/xqaA= github.com/gdamore/tcell/v2 v2.7.0/go.mod h1:hl/KtAANGBecfIPxk+FzKvThTqI84oplgbPEmVX60b8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -22,9 +26,13 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/nathan-osman/go-sunrise v1.1.0 h1:ZqZmtmtzs8Os/DGQYi0YMHpuUqR/iRoJK+wDO0wTCw8= github.com/nathan-osman/go-sunrise v1.1.0/go.mod h1:RcWqhT+5ShCZDev79GuWLayetpJp78RSjSWxiDowmlM= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= @@ -37,8 +45,12 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -46,6 +58,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -57,8 +71,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -75,8 +89,36 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74= +modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/config/config.go b/internal/config/config.go index 3fb9dae8..59205a87 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,7 +1,10 @@ package config import ( + "errors" "fmt" + "net/url" + "regexp" "gopkg.in/yaml.v3" ) @@ -11,6 +14,8 @@ import ( type Config struct { Stylesheet Stylesheet `yaml:"stylesheet"` Categories []Category `yaml:"categories"` + + IntelSources []IntelSource `yaml:"intel-sources,omitempty"` } // A Stylesheet is the stylesheet contents defined in a config file. @@ -96,6 +101,24 @@ type RangedGoal struct { Time string `yaml:"time"` } +type IntelSource struct { + Name string `yaml:"name"` + + SourceType IntelSourceType `yaml:"type"` + + HTTPDetails *HTTPIntelSourceTypeDetails `yaml:",inline"` +} + +type IntelSourceType string + +const ( + IntelSourceTypeHTTP IntelSourceType = "http" +) + +type HTTPIntelSourceTypeDetails struct { + URL string `yaml:"url"` +} + // ParseConfigAugmentDefaults parses the configuration specified in // YAML-formatted data and uses it to augment a given default configuration. func ParseConfigAugmentDefaults(defaultTheme ColorschemeType, yamlData []byte) (Config, error) { @@ -112,6 +135,9 @@ func ParseConfigAugmentDefaults(defaultTheme ColorschemeType, yamlData []byte) ( if err != nil { return defaultConfig, fmt.Errorf("error unmarshaling yaml (%s)", err) } + if err := parsedConfig.validateIntelSources(); err != nil { + return defaultConfig, fmt.Errorf("There is an issue with the intel sources format: %w", err) + } result := defaultConfig.augmentWith(parsedConfig) @@ -127,9 +153,49 @@ func (base Config) augmentWith(augment Config) Config { result.Categories = augment.Categories } + result.IntelSources = append(base.IntelSources, augment.IntelSources...) + return result } +func (base Config) validateIntelSources() error { + var errs []error + for _, src := range base.IntelSources { + if ok, _ := regexp.MatchString(`[a-zA-Z0-9_-]+`, src.Name); !ok { + errs = append(errs, fmt.Errorf("Source name '%s' is not valid.", src.Name)) + continue + } + + switch src.SourceType { + + case IntelSourceTypeHTTP: + if src.HTTPDetails == nil { + errs = append(errs, fmt.Errorf("No HTTP intel source details provided in HTTP intel source '%s'", src.Name)) + continue + } + if u, err := url.Parse(src.HTTPDetails.URL); err != nil { + errs = append(errs, fmt.Errorf("HTTP intel source '%s' details provide an invalid URL: %w", src.Name, err)) + continue + } else if u.Scheme == "" { + errs = append(errs, fmt.Errorf("HTTP intel source '%s' details provide an invalid URL: missing scheme", src.Name)) + continue + } else if u.Scheme != "http" && u.Scheme != "https" { + errs = append(errs, fmt.Errorf("HTTP intel source '%s' details provide an invalid URL: non-HTTP scheme", src.Name)) + continue + } else if u.Host == "" { + errs = append(errs, fmt.Errorf("HTTP intel source '%s' details provide an invalid URL: missing host", src.Name)) + continue + } + + default: + errs = append(errs, fmt.Errorf("Unknown intel source type '%s' (for source named '%s')", src.SourceType, src.Name)) + + } + + } + return errors.Join(errs...) +} + func (base Stylesheet) augmentWith(augment Stylesheet) Stylesheet { result := base diff --git a/internal/control/cli/add.go b/internal/control/cli/add.go index 1741fb86..1382982c 100644 --- a/internal/control/cli/add.go +++ b/internal/control/cli/add.go @@ -15,9 +15,9 @@ import ( "github.com/ja-he/dayplan/internal/provider/backend" ) -// AddCommand contains flags for the `summarize` command line command, for +// DayplanAddCommand contains flags for the `summarize` command line command, for // `go-flags` to parse command line args into. -type AddCommand struct { +type DayplanAddCommand struct { Category string `short:"c" long:"category" description:"the category of the added event(s)" value-name:"" required:"true"` Name string `short:"n" long:"name" description:"the name of the added event(s)" value-name:"" required:"true"` @@ -26,11 +26,13 @@ type AddCommand struct { RepeatInterval string `short:"r" long:"repeat-interval" description:"the repeat interval; if omitted, no repetition is assumed; requires end (til) date to be specified" choice:"daily" choice:"weekly" choice:"monthly"` RepeatTil string `short:"t" long:"repeat-til" description:"the date until which to repeat the event; requires repeat inteval to be specified" value-name:""` + + Timezone string `long:"timezone" default:"local" description:"may be 'local' (default), 'utc', otherwise assumed to be a 'Europe/Berlin' style string and treated as such."` } // Execute executes the add command. // (This gets called by `go-flags` when `add` is provided on the command line) -func (command *AddCommand) Execute(args []string) error { +func (command *DayplanAddCommand) Execute(args []string) error { var envData control.EnvData // set up dir per option @@ -80,12 +82,25 @@ func (command *AddCommand) Execute(args []string) error { } // verify times + var loc *time.Location + switch command.Timezone { + case "local": + loc = time.Local + case "utc": + loc = time.UTC + default: + var err error + loc, err = time.LoadLocation("Europe/Berlin") + if err != nil { + return fmt.Errorf("Invalid timezone '%s' (%w)", command.Timezone, err) + } + } layout := "2006-01-02 15:04:05" - start, err := time.Parse(layout, command.Start) + start, err := time.ParseInLocation(layout, command.Start, loc) if err != nil { return fmt.Errorf("could not parse start time '%s' (%w)", command.Start, err) } - end, err := time.Parse(layout, command.End) + end, err := time.ParseInLocation(layout, command.End, loc) if err != nil { return fmt.Errorf("could not parse end time '%s' (%w)", command.End, err) } @@ -97,7 +112,7 @@ func (command *AddCommand) Execute(args []string) error { if command.RepeatInterval != "" && command.RepeatTil == "" || command.RepeatInterval == "" && command.RepeatTil != "" { panic("ERROR: either both repeat interval and 'til' date need to be specified, or neither") } else if command.RepeatTil != "" { - repeatTilTime, err = time.Parse(layout, command.RepeatTil) + repeatTilTime, err = time.ParseInLocation(layout, command.RepeatTil, loc) if err != nil { return fmt.Errorf("could not parse repeat-til time '%s' (%w)", command.RepeatTil, err) } diff --git a/internal/control/cli/cli.go b/internal/control/cli/cli.go index 4a2e1137..4d655f05 100644 --- a/internal/control/cli/cli.go +++ b/internal/control/cli/cli.go @@ -1,14 +1,2 @@ -// Package cli provides the command-line interface for dayplan. +// Package cli provides the command-line interface for dayplan and other binaries. package cli - -type CommandLineOpts struct { - Version bool `short:"v" long:"version" description:"Show the program version"` - - TuiCommand TUICommand `command:"tui" subcommands-optional:"true"` - SummarizeCommand SummarizeCommand `command:"summarize" subcommands-optional:"true"` - TimesheetCommand TimesheetCommand `command:"timesheet" subcommands-optional:"true"` - AddCommand AddCommand `command:"add" subcommands-optional:"true"` - VersionCommand VersionCommand `command:"version" subcommands-optional:"true"` -} - -var Opts CommandLineOpts diff --git a/internal/control/cli/cli_dayplan.go b/internal/control/cli/cli_dayplan.go new file mode 100644 index 00000000..bc0b7a18 --- /dev/null +++ b/internal/control/cli/cli_dayplan.go @@ -0,0 +1,15 @@ +package cli + +type DayplanCommandLineOpts struct { + Version bool `short:"v" long:"version" description:"Show the program version"` + + TuiCommand DayplanTUICommand `command:"tui" subcommands-optional:"true"` + SummarizeCommand DayplanSummarizeCommand `command:"summarize" subcommands-optional:"true"` + TimesheetCommand DayplanTimesheetCommand `command:"timesheet" subcommands-optional:"true"` + AddCommand DayplanAddCommand `command:"add" subcommands-optional:"true"` + GatherIntel DayplanGatherIntelCommand `command:"gather-intel" subcommands-optional:"true"` + + VersionCommand VersionCommand `command:"version" subcommands-optional:"true"` +} + +var DayplanOpts DayplanCommandLineOpts diff --git a/internal/control/cli/cli_dayplan_intel_server.go b/internal/control/cli/cli_dayplan_intel_server.go new file mode 100644 index 00000000..8621d0cc --- /dev/null +++ b/internal/control/cli/cli_dayplan_intel_server.go @@ -0,0 +1,6 @@ +package cli + +type DayplanIntelServerCommandLineOpts struct { + RunCommand DayplanIntelServerRunCommand `command:"run" subcommands-optional:"true"` + VersionCommand VersionCommand `command:"version" subcommands-optional:"true"` +} diff --git a/internal/control/cli/gather_intel.go b/internal/control/cli/gather_intel.go new file mode 100644 index 00000000..0dd2df82 --- /dev/null +++ b/internal/control/cli/gather_intel.go @@ -0,0 +1,156 @@ +package cli + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "strings" + + "github.com/ja-he/dayplan/internal/config" + "github.com/ja-he/dayplan/internal/control" + "github.com/ja-he/dayplan/internal/intel" + "github.com/ja-he/dayplan/internal/model" + "github.com/ja-he/dayplan/internal/provider" + "github.com/ja-he/dayplan/internal/provider/backend" + "github.com/rs/zerolog/log" +) + +type DayplanGatherIntelCommand struct { +} + +// Executes the summarize command. +// (This gets called by `go-flags` when `summarize` is provided on the command +// line) +func (command *DayplanGatherIntelCommand) Execute(args []string) error { + log.Info().Msg("Gathering intel...") + log.Debug().Msg("Gathering intel...") + + var envData control.EnvData + + // set up dir per option + dayplanHome := os.Getenv("DAYPLAN_HOME") + if dayplanHome == "" { + envData.BaseDirPath = os.Getenv("HOME") + "/.config/dayplan" + } else { + envData.BaseDirPath = strings.TrimRight(dayplanHome, "/") + } + + assumedConfigFilePath := envData.BaseDirPath + "/" + "config.yaml" + log.Debug().Msgf("Reading config from %v.", assumedConfigFilePath) + + // read config from file (for the category priorities) + yamlData, err := os.ReadFile(assumedConfigFilePath) + if err != nil { + panic(fmt.Sprintf("can't read config file: '%s'", err)) + } + configData, err := config.ParseConfigAugmentDefaults(config.Light, yamlData) + if err != nil { + panic(fmt.Sprintf("can't parse config data: '%s'", err)) + } + categories := map[model.CategoryName]*model.Category{} + for _, category := range configData.Categories { + var goal model.Goal + var err error + switch { + case category.Goal.Ranged != nil: + goal, err = model.NewRangedGoalFromConfig(*category.Goal.Ranged) + case category.Goal.Workweek != nil: + goal, err = model.NewWorkweekGoalFromConfig(*category.Goal.Workweek) + } + if err != nil { + return err + } + + cat := model.Category{ + Name: model.CategoryName(category.Name), + Priority: category.Priority, + Goal: goal, + } + categories[cat.Name] = &cat + } + + filterCategories := len(DayplanOpts.SummarizeCommand.CategoryFilterString) > 0 + includeCategoriesByName := make(map[model.CategoryName]struct{}) + if filterCategories { + for _, name := range strings.Split(DayplanOpts.SummarizeCommand.CategoryFilterString, ",") { + includeCategoriesByName[model.CategoryName(name)] = struct{}{} + } + } + + var dataProvider provider.EventProvider + dataProvider, err = backend.NewFilesDataProvider( + path.Join(envData.BaseDirPath, "days"), + &backend.MemoryCategoryProvider{M: categories}, + ) + if err != nil { + return fmt.Errorf("can't create file data provider (%w)", err) + } + + log.Debug().Msgf("Will gather from %d intel sources.", len(configData.IntelSources)) + + var errs []error + var events []model.Event + for _, source := range configData.IntelSources { + switch source.SourceType { + case config.IntelSourceTypeHTTP: + newEvents, err := gatherHTTPIntel(source.Name, *source.HTTPDetails) + if err != nil { + errs = append(errs, fmt.Errorf("Unable to gather HTTP intel from source %v", source.Name)) + continue + } + log.Debug().Msgf("Found %d events from source %v.", len(newEvents), source.Name) + events = append(events, newEvents...) + + default: + errs = append(errs, fmt.Errorf("Unknown source type %v encountered.", source.SourceType)) + } + } + + for i := range events { + id, err := dataProvider.AddEvent(events[i]) + if err != nil { + errs = append(errs, fmt.Errorf("Unable to add event %d (%w).", i, err)) + continue + } + log.Debug().Msgf("Added even %d as %v.", i, id) + } + + if len(events) > 0 { + log.Debug().Msgf("Committing data provider state.") + if err := dataProvider.CommitState(); err != nil { + errs = append(errs, fmt.Errorf("Unable to commit data provider state (%w)", err)) + } + } + + return errors.Join(errs...) +} + +func gatherHTTPIntel(name string, details config.HTTPIntelSourceTypeDetails) ([]model.Event, error) { + retrieveURL, err := url.JoinPath(details.URL, "/events/retrieve") + resp, err := http.Post(retrieveURL, "application/json", nil) + if err != nil { + return nil, fmt.Errorf("Unable to GET '%s' (%w)", name, err) + } + respBodyData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("Unable to read response body data (%w)", err) + } + + parsedResponse := intel.RetrieveResponse{} + if err := json.Unmarshal(respBodyData, &parsedResponse); err != nil { + return nil, fmt.Errorf("Unable to parse response (may be an error response) (%w)", err) + } + + for i := range parsedResponse.Events { + if parsedResponse.Events[i].Category == "" { + parsedResponse.Events[i].Category = model.CategoryName(name) + } + } + + return parsedResponse.Events, nil +} diff --git a/internal/control/cli/intel_server_run_command.go b/internal/control/cli/intel_server_run_command.go new file mode 100644 index 00000000..31a064a1 --- /dev/null +++ b/internal/control/cli/intel_server_run_command.go @@ -0,0 +1,29 @@ +package cli + +import ( + "fmt" + + "github.com/ja-he/dayplan/internal/intel" + "github.com/rs/zerolog/log" +) + +type DayplanIntelServerRunCommand struct { + ListenAddr string `short:"l" long:"listen-addr" default:"localhost:8080" description:"Address to listen on"` + DBPath string `short:"d" long:"db" default:":memory:" description:"SQLite database path (use ':memory:' for in-memory)"` +} + +func (c *DayplanIntelServerRunCommand) Execute(_ []string) error { + + server, err := intel.NewServer(c.ListenAddr, c.DBPath) + if err != nil { + return fmt.Errorf("Unable to create server (%w)", err) + } + log.Debug().Msgf("Initialized server to listen on %v with DB at %v.", c.ListenAddr, c.DBPath) + + err = server.Run() + if err != nil { + return fmt.Errorf("Unable to run server (%w)", err) + } + + return nil +} diff --git a/internal/control/cli/summarize.go b/internal/control/cli/summarize.go index 391e124d..bc47888f 100644 --- a/internal/control/cli/summarize.go +++ b/internal/control/cli/summarize.go @@ -16,7 +16,7 @@ import ( // Flags for the `summarize` command line command, for `go-flags` to parse // command line args into. -type SummarizeCommand struct { +type DayplanSummarizeCommand struct { From time.Time `short:"f" long:"from" description:"the timestamp from which to start summarizing" value-name:"