1
0
Fork 0

More API clients

main v0.9.0
Jordan Hotmann 2023-11-10 16:22:46 -07:00
parent 9142be3688
commit 2685228c40
No known key found for this signature in database
GPG Key ID: 01B504170C2A2EA3
11 changed files with 517 additions and 6 deletions

9
go.mod
View File

@ -12,11 +12,13 @@ require (
github.com/google/uuid v1.3.1 // indirect github.com/google/uuid v1.3.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
golang.org/x/net v0.15.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/time v0.3.0 // indirect
) )
require ( require (
github.com/gabriel-vasile/mimetype v1.4.3
github.com/go-chi/chi v1.5.5 github.com/go-chi/chi v1.5.5
github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/render v1.0.3 github.com/go-chi/render v1.0.3
@ -27,8 +29,9 @@ require (
github.com/nats-io/nats-server/v2 v2.10.2 // indirect github.com/nats-io/nats-server/v2 v2.10.2 // indirect
github.com/nats-io/nkeys v0.4.5 // indirect github.com/nats-io/nkeys v0.4.5 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.13.0 // indirect github.com/samber/lo v1.38.1
golang.org/x/sys v0.12.0 // indirect golang.org/x/crypto v0.14.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.31.0 // indirect
) )

15
go.sum
View File

@ -4,6 +4,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
@ -49,6 +51,8 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -62,8 +66,11 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -71,8 +78,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -83,8 +91,9 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=

15
internal/util/resty.go Normal file
View File

@ -0,0 +1,15 @@
package util
import (
"fmt"
"github.com/go-resty/resty/v2"
)
// CheckSuccess wraps a resty request and if the status code is not 2XX will make the error non-nil
func CheckSuccess(resp *resty.Response, err error) (*resty.Response, error) {
if err == nil && !resp.IsSuccess() {
err = fmt.Errorf("%d status received: %s", resp.StatusCode(), string(resp.Body()))
}
return resp, err
}

106
pkg/gokapi/api.go Normal file
View File

@ -0,0 +1,106 @@
package gokapi
import (
"fmt"
"os"
"path/filepath"
"code.jhot.me/jhot/hats/internal/util"
"github.com/gabriel-vasile/mimetype"
"github.com/go-resty/resty/v2"
)
type GokapiClient struct {
host string
restClient *resty.Client
}
func New(host string, token string) *GokapiClient {
return &GokapiClient{
host: host,
restClient: resty.New().
SetHeader("apikey", token).
SetBaseURL(fmt.Sprintf("%s/api", host)),
}
}
func NewFromEnv() *GokapiClient {
return New(os.Getenv("GOKAPI_HOST"), os.Getenv("GOKAPI_TOKEN"))
}
type GokapiFile struct {
ID string `json:"Id"`
Name string `json:"Name"`
Size string `json:"Size"`
HotlinkId string `json:"HotlinkId"`
ContentType string `json:"ContentType"`
ExpireAt int64 `json:"ExpireAt"`
SizeBytes int64 `json:"SizeBytes"`
ExpireAtString string `json:"ExpireAtString"`
DownloadsRemaining int `json:"DownloadsRemaining"`
DownloadCount int `json:"DownloadCount"`
UnlimitedDownloads bool `json:"UnlimitedDownloads"`
UnlimitedTime bool `json:"UnlimitedTime"`
RequiresClientSideDecryption bool `json:"RequiresClientSideDecryption"`
IsEncrypted bool `json:"IsEncrypted"`
IsPasswordProtected bool `json:"IsPasswordProtected"`
IsSavedOnLocalStorage bool `json:"IsSavedOnLocalStorage"`
}
func (c *GokapiClient) ListFiles() ([]GokapiFile, error) {
var data []GokapiFile
_, err := util.CheckSuccess(c.restClient.R().SetHeader("Accept", "application/json").SetResult(&data).Get("files/list"))
if err != nil {
return data, err
}
return data, nil
}
type UploadOptions struct {
AllowedDownloads int
ExpiryDays int
Password string
FilenameOverride string
}
func (c *GokapiClient) UploadFile(filePath string, opts *UploadOptions) (GokapiFile, error) {
var fileName string
if opts.FilenameOverride != "" {
fileName = opts.FilenameOverride
} else {
fileName = filepath.Base(filePath)
}
f, err := os.Open(filePath)
if err != nil {
return GokapiFile{}, err
}
defer f.Close()
mimeType, _ := mimetype.DetectFile(filePath)
req := c.restClient.R().
SetMultipartField("file", fileName, mimeType.String(), f).
SetMultipartFormData(map[string]string{
"allowedDownloads": fmt.Sprintf("%d", opts.AllowedDownloads),
"expiryDays": fmt.Sprintf("%d", opts.ExpiryDays),
"password": opts.Password,
})
var data struct {
Result string `json:"Result"`
FileInfo GokapiFile `json:"FileInfo"`
}
_, err = util.CheckSuccess(req.SetResult(&data).Post("files/add"))
if err != nil {
return GokapiFile{}, err
}
return data.FileInfo, nil
}
func (c *GokapiClient) GetDownloadUrl(f GokapiFile) string {
return fmt.Sprintf("%s/downloadFile?id=%s", c.host, f.ID)
}

View File

@ -58,6 +58,7 @@ var Services = struct {
SetHvacMode string SetHvacMode string
SetFanMode string SetFanMode string
SetTemperature string SetTemperature string
SetValue string
Start string Start string
Change string Change string
Cancel string Cancel string
@ -74,6 +75,7 @@ var Services = struct {
SetHvacMode: "set_hvac_mode", SetHvacMode: "set_hvac_mode",
SetFanMode: "set_fan_mode", SetFanMode: "set_fan_mode",
SetTemperature: "set_temperature", SetTemperature: "set_temperature",
SetValue: "set_value",
Start: "start", Start: "start",
Change: "change", Change: "change",
Cancel: "cancel", Cancel: "cancel",
@ -89,6 +91,7 @@ var ExtraProps = struct {
TargetTempHigh string TargetTempHigh string
TargetTempLow string TargetTempLow string
Duration string Duration string
Value string
}{ }{
Transition: "transition", Transition: "transition",
Brightness: "brightness", Brightness: "brightness",
@ -98,6 +101,7 @@ var ExtraProps = struct {
TargetTempHigh: "target_temp_high", TargetTempHigh: "target_temp_high",
TargetTempLow: "target_temp_low", TargetTempLow: "target_temp_low",
Duration: "duration", Duration: "duration",
Value: "value",
} }
type ResultContext struct { type ResultContext struct {

93
pkg/nws/api.go Normal file
View File

@ -0,0 +1,93 @@
package nws
import (
"fmt"
"strings"
"code.jhot.me/jhot/hats/internal/util"
"github.com/go-resty/resty/v2"
"github.com/samber/lo"
)
const (
NWS_BASE_URL = "https://api.weather.gov"
)
type NwsClient struct {
restClient *resty.Client
}
func New() *NwsClient {
return &NwsClient{
restClient: resty.New().
SetBaseURL(NWS_BASE_URL).
SetHeader("Accept", "application/geo+json").
SetHeader("User-Agent", util.GetEnvWithDefault("NWS_USER_AGENT", "(HATS-client, hats@jhot.me)")),
}
}
func (c *NwsClient) CoordinatesToPoint(lat float64, lon float64) (Point, error) {
var p Point
_, err := c.restClient.R().SetResult(&p).Get(fmt.Sprintf("points/%f,%f", lat, lon))
return p, err
}
func (c *NwsClient) GetHourlyForecast(p Point) (HourlyProps, error) {
var data struct {
Properties HourlyProps `json:"properties"`
}
_, err := util.CheckSuccess(c.restClient.R().SetResult(&data).
Get(removeBase(p.Properties.ForecastHourly)))
if err != nil {
return HourlyProps{}, nil
}
return data.Properties, nil
}
func (p HourlyProps) GetHighLow(hours int) (high int, low int) {
if hours > len(p.Periods) {
hours = len(p.Periods)
}
temps := lo.Map(p.Periods[0:hours-1], func(p HourlyPeriod, _ int) int {
return p.Temperature
})
return lo.Max(temps), lo.Min(temps)
}
func (c *NwsClient) GetStations(p Point) ([]string, error) {
var data struct {
ObservationStations []string `json:"observationStations"`
}
_, err := util.CheckSuccess(c.restClient.R().SetResult(&data).
Get(removeBase(p.Properties.ObservationStations)))
if err != nil {
return []string{}, err
}
return data.ObservationStations, nil
}
func (c *NwsClient) GetLatestObservations(p Point) (Observations, error) {
stations, err := c.GetStations(p)
if err != nil {
return Observations{}, fmt.Errorf("%w: error getting stations", err)
}
var data struct {
Properties Observations `json:"properties"`
}
_, err = util.CheckSuccess(c.restClient.R().SetResult(&data).
Get(removeBase(stations[0] + "/observations/latest")))
if err != nil {
return Observations{}, err
}
return data.Properties, nil
}
func removeBase(longUrl string) string {
return strings.TrimPrefix(longUrl, NWS_BASE_URL+"/")
}

51
pkg/nws/structs.go Normal file
View File

@ -0,0 +1,51 @@
package nws
import "time"
type UnitValue[T any] struct {
UnitCode string `json:"unitCode"`
Value T `json:"value"`
}
type PointProps struct {
GridId string `json:"gridId"`
GridX int `json:"gridX"`
GridY int `json:"gridY"`
Forecast string `json:"forecast"`
ForecastHourly string `json:"forecastHourly"`
ForecastGridData string `json:"forecastGridData"`
ForecastZone string `json:"forecastZone"`
TimeZone string `json:"timeZone"`
RadarStation string `json:"radarStation"`
ObservationStations string `json:"observationStations"`
}
type Point struct {
ID string `json:"id"`
Properties PointProps `json:"properties"`
}
type HourlyPeriod struct {
Number int `json:"number"`
StartTime time.Time `json:"startTime"`
Endime time.Time `json:"endTime"`
IsDayTime bool `json:"isDayTime"`
Temperature int `json:"temperature"`
ProbabilityOfPrecipitation UnitValue[int] `json:"probabilityOfPrecipitation"`
DewPoint UnitValue[float64] `json:"dewpoint"`
ShortForecast string `json:"shortForecast"`
}
type HourlyProps struct {
Updated time.Time `json:"updated"`
Units string `json:"units"`
Periods []HourlyPeriod `json:"periods"`
}
type Observations struct {
Timestamp time.Time `json:"timestamp"`
Temperature UnitValue[float64] `json:"temperature"`
DewPoint UnitValue[float64] `json:"dewpoint"`
WindDirection UnitValue[int] `json:"windDirection"`
WindSpeed UnitValue[float64] `json:"windSpeed"`
}

86
pkg/qbittorrent/api.go Normal file
View File

@ -0,0 +1,86 @@
package qbittorrent
import (
"errors"
"fmt"
"net/http"
"os"
"strings"
"code.jhot.me/jhot/hats/internal/util"
"github.com/go-resty/resty/v2"
)
type QbittorrentClient struct {
restClient *resty.Client
}
func New(host string) *QbittorrentClient {
return &QbittorrentClient{
restClient: resty.New().SetBaseURL(fmt.Sprintf("%s/api/v2", host)),
}
}
func NewFromEnv() (*QbittorrentClient, error) {
c := New(os.Getenv("QBITTORRENT_HOST"))
err := c.Login(os.Getenv("QBITTORRENT_USER"), os.Getenv("QBITTORRENT_PASS"))
return c, err
}
func (c *QbittorrentClient) Login(user string, pass string) error {
resp, err := util.CheckSuccess(c.restClient.R().SetFormData(map[string]string{
"username": user,
"password": pass,
}).Post("auth/login"))
if err != nil {
return err
}
authCookie := resp.Header().Get("set-cookie")
if authCookie == "" {
return errors.New("auth cookie not found")
}
first := strings.Split(authCookie, ";")[0]
parts := strings.Split(first, "=")
if len(parts) != 2 {
return fmt.Errorf("auth cookie not in expected format: %s", authCookie)
}
c.restClient.SetCookie(&http.Cookie{
Name: parts[0],
Value: parts[1],
})
return nil
}
func (c *QbittorrentClient) GetVersion() (string, error) {
resp, err := util.CheckSuccess(c.restClient.R().Get("app/version"))
if err != nil {
return "", err
}
return string(resp.Body()), nil
}
func (c *QbittorrentClient) GetTransferInfo() (GlobalTransferInfo, error) {
var data GlobalTransferInfo
_, err := util.CheckSuccess(c.restClient.R().SetResult(&data).Get("transfer/info"))
return data, err
}
func (c *QbittorrentClient) GetAltSpeedLimitState() (bool, error) {
resp, err := util.CheckSuccess(c.restClient.R().Get("transfer/speedLimitsMode"))
if err != nil {
return false, err
}
return string(resp.Body()) == "1", nil
}
func (c *QbittorrentClient) ToggleAltSpeedLimitState() error {
_, err := util.CheckSuccess(c.restClient.R().Post("transfer/toggleSpeedLimitsMode"))
return err
}

View File

@ -0,0 +1,10 @@
package qbittorrent
type GlobalTransferInfo struct {
DownloadSpeed int `json:"dl_info_speed"`
DownloadedData int `json:"dl_info_data"`
UploadSpeed int `json:"up_info_speed"`
UploadedData int `json:"up_info_data"`
DhtNodes int `json:"dht_nodes"`
Status string `json:"connection_status"`
}

83
pkg/syncthing/rest.go Normal file
View File

@ -0,0 +1,83 @@
package syncthing
import (
"fmt"
"os"
"code.jhot.me/jhot/hats/internal/util"
"github.com/go-resty/resty/v2"
)
type SyncthingClient struct {
restClient *resty.Client
}
func New(host string, token string) *SyncthingClient {
return &SyncthingClient{
restClient: resty.New().
SetBaseURL(fmt.Sprintf("%s/rest", host)).
SetHeader("X-API-Key", token),
}
}
func NewFromEnv() *SyncthingClient {
return New(os.Getenv("SYNCTHING_HOST"), os.Getenv("SYNCTHING_TOKEN"))
}
func (c *SyncthingClient) GetVersion() (VersionInfo, error) {
var data VersionInfo
_, err := util.CheckSuccess(c.restClient.R().SetResult(&data).Get("system/version"))
return data, err
}
func (c *SyncthingClient) GetDeviceStats() (map[string]DeviceStatistics, error) {
var data map[string]DeviceStatistics
_, err := util.CheckSuccess(c.restClient.R().SetResult(&data).Get("stats/device"))
return data, err
}
func (c *SyncthingClient) Pause(id string) error {
req := c.restClient.R()
if id != "" {
req.SetHeader("device", id)
}
_, err := util.CheckSuccess(req.Post("system/pause"))
return err
}
func (c *SyncthingClient) Resume(id string) error {
req := c.restClient.R()
if id != "" {
req.SetHeader("device", id)
}
_, err := util.CheckSuccess(req.Post("system/resume"))
return err
}
func (c *SyncthingClient) GetFolderConfig(id string) ([]FolderConfig, error) {
url := "config/folders"
if id != "" {
url = fmt.Sprintf("%s/%s", url, id)
var data FolderConfig
_, err := util.CheckSuccess(c.restClient.R().SetResult(&data).Get(url))
return []FolderConfig{data}, err
} else {
var data []FolderConfig
_, err := util.CheckSuccess(c.restClient.R().SetResult(&data).Get(url))
return data, err
}
}
func (c *SyncthingClient) SetFolderConfig(id string, param string, value any) error {
_, err := util.CheckSuccess(c.restClient.R().SetBody(map[string]any{param: value}).Patch(fmt.Sprintf("config/folders/%s", id)))
return err
}

51
pkg/syncthing/structs.go Normal file
View File

@ -0,0 +1,51 @@
package syncthing
type VersionInfo struct {
Arch string `json:"arch"`
LongVersion string `json:"longVersion"`
OS string `json:"os"`
Version string `json:"version"`
}
type DeviceStatistics struct {
LastSeen string `json:"lastSeen"`
LastConnectionDuration int `json:"lastConnectionDurationS"`
}
type FolderConfig struct {
ID string `json:"id"`
Label string `json:"label,omitempty"`
FilesystemType string `json:"filesystemType,omitempty"`
Path string `json:"path,omitempty"`
Type string `json:"type,omitempty"`
RescanIntervalS int `json:"rescanIntervalS,omitempty"`
FsWatcherEnabled bool `json:"fsWatcherEnabled,omitempty"`
FsWatcherDelayS int `json:"fsWatcherDelayS,omitempty"`
IgnorePerms bool `json:"ignorePerms,omitempty"`
AutoNormalize bool `json:"autoNormalize,omitempty"`
Copiers int `json:"copiers,omitempty"`
PullerMaxPendingKiB int `json:"pullerMaxPendingKiB,omitempty"`
Hashers int `json:"hashers,omitempty"`
Order string `json:"order,omitempty"`
IgnoreDelete bool `json:"ignoreDelete,omitempty"`
ScanProgressIntervalS int `json:"scanProgressIntervalS,omitempty"`
PullerPauseS int `json:"pullerPauseS,omitempty"`
MaxConflicts int `json:"maxConflicts,omitempty"`
DisableSparseFiles bool `json:"disableSparseFiles,omitempty"`
DisableTempIndexes bool `json:"disableTempIndexes,omitempty"`
Paused bool `json:"paused,omitempty"`
WeakHashThresholdPct int `json:"weakHashThresholdPct,omitempty"`
MarkerName string `json:"markerName,omitempty"`
CopyOwnershipFromParent bool `json:"copyOwnershipFromParent,omitempty"`
ModTimeWindowS int `json:"modTimeWindowS,omitempty"`
MaxConcurrentWrites int `json:"maxConcurrentWrites,omitempty"`
DisableFsync bool `json:"disableFsync,omitempty"`
BlockPullOrder string `json:"blockPullOrder,omitempty"`
CopyRangeMethod string `json:"copyRangeMethod,omitempty"`
CaseSensitiveFS bool `json:"caseSensitiveFS,omitempty"`
JunctionsAsDirs bool `json:"junctionsAsDirs,omitempty"`
SyncOwnership bool `json:"syncOwnership,omitempty"`
SendOwnership bool `json:"sendOwnership,omitempty"`
SyncXattrs bool `json:"syncXattrs,omitempty"`
SendXattrs bool `json:"sendXattrs,omitempty"`
}