-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #167 from 0x2142/initial-rest-api
Initial REST API
- Loading branch information
Showing
49 changed files
with
1,367 additions
and
266 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package api | ||
|
||
import ( | ||
"fmt" | ||
"net" | ||
"net/http" | ||
|
||
apiv1 "github.com/0x2142/frigate-notify/api/v1" | ||
"github.com/0x2142/frigate-notify/config" | ||
"github.com/danielgtaylor/huma/v2" | ||
"github.com/danielgtaylor/huma/v2/adapters/humago" | ||
"github.com/rs/zerolog/log" | ||
) | ||
|
||
func RunAPIServer() error { | ||
|
||
router := http.NewServeMux() | ||
|
||
// Configure API | ||
apiConfig := huma.DefaultConfig("Frigate-Notify", config.Internal.AppVersion) | ||
apiConfig.Info.License = &huma.License{Name: "MIT", | ||
URL: "https://github.com/0x2142/frigate-notify/blob/main/LICENSE"} | ||
apiConfig.Info.Contact = &huma.Contact{Name: "Matt Schmitz", | ||
URL: "https://github.com/0x2142/frigate-notify", | ||
} | ||
api := humago.New(router, apiConfig) | ||
|
||
apiv1.Registerv1Routes(api) | ||
|
||
log.Debug().Msg("Starting API server...") | ||
listenAddr := fmt.Sprintf("0.0.0.0:%v", config.ConfigData.App.API.Port) | ||
listener, err := net.Listen("tcp", listenAddr) | ||
if err != nil { | ||
return err | ||
} | ||
go http.Serve(listener, router) | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package apiv1 | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/0x2142/frigate-notify/config" | ||
"github.com/0x2142/frigate-notify/events" | ||
"github.com/danielgtaylor/huma/v2" | ||
"github.com/rs/zerolog/log" | ||
) | ||
|
||
type ConfigOutput struct { | ||
Body struct { | ||
Config config.Config `json:"config"` | ||
} | ||
} | ||
|
||
type PutConfigInput struct { | ||
Body struct { | ||
Config config.Config `json:"config"` | ||
SkipSave bool `json:"skipsave,omitempty" doc:"Skip writing new config to file" default:"false"` | ||
SkipBackup bool `json:"skipbackup,omitempty" doc:"Skip creating config file backup" default:"false"` | ||
SkipValidate bool `json:"skipvalidate,omitempty" doc:"Skip config validation checking"` | ||
SkipReload bool `json:"skipreload,omitempty" doc:"Skip config reload after updating settings" hidden:"true"` | ||
} | ||
} | ||
|
||
type PutConfigOutput struct { | ||
Body struct { | ||
Status string `json:"status"` | ||
Errors []string `json:"errors,omitempty"` | ||
} | ||
} | ||
|
||
// GetConfig returns the current running configuration | ||
func GetConfig(ctx context.Context, input *struct{}) (*ConfigOutput, error) { | ||
log.Trace(). | ||
Str("uri", V1_PREFIX+"/config"). | ||
Str("method", "GET"). | ||
Msg("Received API request") | ||
|
||
resp := &ConfigOutput{} | ||
resp.Body.Config = config.ConfigData | ||
|
||
log.Trace(). | ||
Str("uri", V1_PREFIX+"/config"). | ||
Interface("response_json", resp.Body). | ||
Msg("Sent API response") | ||
|
||
return resp, nil | ||
} | ||
|
||
// PutConfig replaces the current running configuration | ||
func PutConfig(ctx context.Context, input *PutConfigInput) (*PutConfigOutput, error) { | ||
log.Trace(). | ||
Str("uri", V1_PREFIX+"/config"). | ||
Str("method", "PUT"). | ||
Msg("Received API request") | ||
|
||
resp := &PutConfigOutput{} | ||
|
||
newConfig := input.Body.Config | ||
|
||
var validationErrors []string | ||
if !input.Body.SkipValidate { | ||
log.Trace().Msg("Skipping config validation checks") | ||
validationErrors = newConfig.Validate() | ||
} | ||
|
||
if len(validationErrors) == 0 { | ||
resp.Body.Status = "ok" | ||
if !input.Body.SkipReload { | ||
go reloadCfg(newConfig, input.Body.SkipSave, input.Body.SkipBackup) | ||
} | ||
|
||
log.Trace(). | ||
Str("uri", V1_PREFIX+"/config"). | ||
Interface("response_json", resp.Body). | ||
Msg("Sent API response") | ||
return resp, nil | ||
} else { | ||
resp.Body.Status = "validation error" | ||
resp.Body.Errors = validationErrors | ||
|
||
log.Trace(). | ||
Str("uri", V1_PREFIX+"/config"). | ||
Interface("response_json", resp.Body). | ||
Msg("Sent API response") | ||
|
||
return resp, huma.Error422UnprocessableEntity("config validation failed") | ||
} | ||
} | ||
|
||
func reloadCfg(newconfig config.Config, skipSave bool, skipBackup bool) { | ||
log.Info().Msg("Reloading app config...") | ||
log.Trace(). | ||
Bool("skipSave", skipSave). | ||
Bool("skipBackup", skipBackup). | ||
Msg("Config reload via API") | ||
if config.ConfigData.Frigate.MQTT.Enabled { | ||
events.DisconnectMQTT() | ||
} | ||
|
||
config.ConfigData = newconfig | ||
if !skipSave { | ||
config.Save(skipBackup) | ||
} | ||
|
||
if config.ConfigData.Frigate.MQTT.Enabled { | ||
events.SubscribeMQTT() | ||
} | ||
log.Info().Msg("Config reload completed") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package apiv1 | ||
|
||
import ( | ||
"bytes" | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/danielgtaylor/huma/v2/humatest" | ||
) | ||
|
||
func TestGetConfig(t *testing.T) { | ||
_, api := humatest.New(t) | ||
|
||
Registerv1Routes(api) | ||
|
||
resp := api.Get("/api/v1/config") | ||
|
||
if resp.Code != http.StatusOK { | ||
t.Error("Expected HTTP 200, got ", resp.Code) | ||
} | ||
} | ||
|
||
func TestPutConfig(t *testing.T) { | ||
_, api := humatest.New(t) | ||
|
||
Registerv1Routes(api) | ||
|
||
newconfig := `{ | ||
"config":{ | ||
"frigate":{ | ||
"server":"http://192.0.2.10:5000", | ||
"mqtt":{ | ||
"enabled": true | ||
} | ||
}, | ||
"alerts":{ | ||
} | ||
}, | ||
"skipvalidate": true, | ||
"skipbackup": true, | ||
"skipsave": true, | ||
"skipreload": true | ||
}` | ||
|
||
resp := api.Put("/api/v1/config", bytes.NewReader([]byte(newconfig))) | ||
|
||
if resp.Code != http.StatusAccepted { | ||
t.Error("Expected HTTP 202, got ", resp.Code) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package apiv1 | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/rs/zerolog/log" | ||
) | ||
|
||
type HealthzOutput struct { | ||
Body struct { | ||
Status string `json:"status"` | ||
} | ||
} | ||
|
||
// GetHealthz returns current app liveness state | ||
func GetHealthz(ctx context.Context, input *struct{}) (*HealthzOutput, error) { | ||
log.Trace(). | ||
Str("uri", V1_PREFIX+"/healthz"). | ||
Str("method", "GET"). | ||
Msg("Received API request") | ||
|
||
resp := &HealthzOutput{} | ||
|
||
resp.Body.Status = "ok" | ||
|
||
log.Trace(). | ||
Str("uri", V1_PREFIX+"/healthz"). | ||
Interface("response_json", resp.Body). | ||
Msg("Sent API response") | ||
return resp, nil | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package apiv1 | ||
|
||
import ( | ||
"encoding/json" | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/danielgtaylor/huma/v2/humatest" | ||
) | ||
|
||
func TestGetHealthz(t *testing.T) { | ||
_, api := humatest.New(t) | ||
|
||
Registerv1Routes(api) | ||
|
||
resp := api.Get("/api/v1/healthz") | ||
|
||
if resp.Code != http.StatusOK { | ||
t.Error("Expected HTTP 200, got ", resp.Code) | ||
} | ||
|
||
var healthzResponse map[string]interface{} | ||
json.Unmarshal([]byte(resp.Body.Bytes()), &healthzResponse) | ||
|
||
if healthzResponse["status"] != "ok" { | ||
t.Error("Response body did not match expected result") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package apiv1 | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/0x2142/frigate-notify/config" | ||
"github.com/rs/zerolog/log" | ||
) | ||
|
||
type NotifStateInput struct { | ||
Body struct { | ||
Enabled bool `json:"enabled" enum:"true,false" doc:"Set state of notifications" required:"true"` | ||
} | ||
} | ||
|
||
type NotifStateOutput struct { | ||
Body struct { | ||
Enabled bool `json:"enabled" enum:"true,false" doc:"Frigate-Notify enabled for notifications" default:"true"` | ||
} | ||
} | ||
|
||
// GetNotifState returns whether app is enabled for sending notifications or not | ||
func GetNotifState(ctx context.Context, input *struct{}) (*NotifStateOutput, error) { | ||
log.Trace(). | ||
Str("uri", V1_PREFIX+"/notif_state"). | ||
Str("method", "GET"). | ||
Msg("Received API request") | ||
|
||
resp := &NotifStateOutput{} | ||
resp.Body.Enabled = config.Internal.Status.Notifications.Enabled | ||
|
||
log.Trace(). | ||
Str("uri", V1_PREFIX+"/notif_state"). | ||
Interface("response_json", resp.Body). | ||
Msg("Sent API response") | ||
|
||
return resp, nil | ||
} | ||
|
||
// PostNotifState updates state to enable or disable app notifications | ||
func PostNotifState(ctx context.Context, input *NotifStateInput) (*NotifStateOutput, error) { | ||
log.Trace(). | ||
Str("uri", V1_PREFIX+"/notif_state"). | ||
Str("method", "POST"). | ||
Msg("Received API request") | ||
|
||
config.Internal.Status.Notifications.Enabled = input.Body.Enabled | ||
|
||
log.Debug(). | ||
Bool("state", input.Body.Enabled). | ||
Msg("App state changed via API") | ||
|
||
resp := &NotifStateOutput{} | ||
resp.Body.Enabled = config.Internal.Status.Notifications.Enabled | ||
|
||
log.Trace(). | ||
Str("uri", V1_PREFIX+"/notif_state"). | ||
//Interface("response_json", resp.Body). | ||
Msg("Sent API response") | ||
|
||
return resp, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package apiv1 | ||
|
||
import ( | ||
"bytes" | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/danielgtaylor/huma/v2/humatest" | ||
) | ||
|
||
func TestGetNotifState(t *testing.T) { | ||
_, api := humatest.New(t) | ||
|
||
Registerv1Routes(api) | ||
|
||
resp := api.Get("/api/v1/notif_state") | ||
|
||
if resp.Code != http.StatusOK { | ||
t.Error("Expected HTTP 200, got ", resp.Code) | ||
} | ||
} | ||
|
||
func TestPostNotifState(t *testing.T) { | ||
_, api := humatest.New(t) | ||
|
||
Registerv1Routes(api) | ||
|
||
resp := api.Post("/api/v1/notif_state", bytes.NewReader([]byte(`{"enabled": false}`))) | ||
|
||
if resp.Code != http.StatusAccepted { | ||
t.Error("Expected HTTP 202, got ", resp.Code) | ||
} | ||
} |
Oops, something went wrong.