diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index bc18dc4..0000000 --- a/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -trim_trailing_whitespace = true -insert_final_newline = true - -[*.json] -indent_style = tab -tab_width = 4 diff --git a/README.md b/README.md index da23ea6..4cace42 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,7 @@ $ curl http://localhost:8080 | jq { "api_compatibility": [ - "14", - "15" + "14" ], "space": "CCCHH", ... @@ -16,16 +15,16 @@ $ curl http://localhost:8080 | jq ## Configuring -`spaceapid` has to be configured via one or multiple json files. +spaceapid has to be configured via one or multiple json files. A sample configuration is provided as [config-template.json](./config-template.json). The config consists of three parts: - `"credentials"` - List of Username/Password credentials for HTTP BasicAuth - `"dynamic"` - - The dynamic parts of the message -- `"static"` - - The static (pre-filled) parts of the message + - The configuration for the dynamic parts of the message +- `"response"` + - The static (pre-filled) parts of the response See [Running](#Running) for details. diff --git a/config-template.json b/config-template.json index aac6046..31cb500 100644 --- a/config-template.json +++ b/config-template.json @@ -1,112 +1,111 @@ { - "credentials": { - "club-assistant": { - "username": "club-assistant", - "password": "clubmiau" - }, - "dooris-hauptraum": { - "username": "dooris-hauptraum", - "password": "doorimiau" - } - }, - "dynamic": { - "sensors": { - "temperature": [ - { - "sensor_data": { - "unit": "°C", - "location": "Hauptraum", - "name": "Kueche", - "description": "Sensor im Ofen" - }, - "allowed_credentials": [ - "club-assistant" - ] - }, - { - "sensor_data": { - "unit": "°C", - "location": "Hauptraum", - "description": "Sensor im Hauptraum" - }, - "allowed_credentials": [ - "club-assistant" - ] - } - ], - "humidity": [ - { - "sensor_data": { - "unit": "%", - "location": "Hauptraum", - "name": "Kueche", - "description": "Sensor im Wasserhahn" - }, - "allowed_credentials": [ - "club-assistant" - ] - } - ], - "beverage_supply": [ - { - "sensor_data": { - "unit": "btl" - }, - "allowed_credentials": [ - "club-assistant" - ] - } - ] - }, - "state": { - "open": { - "allowed_credentials": [ - "dooris-hauptraum" - ] - } - } - }, - "static": { - "api_compatibility": [ - "14", - "15" - ], - "space": "CCCHH", - "logo": "https://hamburg.ccc.de/images/logo.svg", - "ext_ccc": "erfa", - "url": "https://hamburg.ccc.de/", - "location": { - "address": "Zeiseweg 9, 22765 Hamburg, Germany", - "lon": 9.94444, - "lat": 53.5584 - }, - "contact": { - "phone": "+49 40 23830150", - "irc": "ircs://irc.hackint.org:6697/#ccchh", - "mastodon": "@ccchh@chaos.social", - "email": "mail@hamburg.ccc.de", - "ml": "talk@hamburg.ccc.de", - "matrix": "#ccchh:hamburg.ccc.de" - }, - "feeds": { - "blog": { - "type": "application/rss+xml", - "url": "https://hamburg.ccc.de/feed.xml" - }, - "calendar": { - "type": "ical", - "url": "webcal://cloud.hamburg.ccc.de/remote.php/dav/public-calendars/QJAdExziSnNJEz5g/?export" - } - }, - "links": [ - { - "name": "Wiki", - "url": "https://wiki.hamburg.ccc.de" - }, - { - "name": "Git (Forgejo)", - "url": "https://git.hamburg.ccc.de" - } - ] - } + "credentials": { + "club-assistant": { + "username": "club-assistant", + "password": "clubmiau" + }, + "dooris-hauptraum": { + "username": "dooris-hauptraum", + "password": "doorimiau" + } + }, + "dynamic": { + "sensors": { + "temperature": [ + { + "sensor_data": { + "unit": "°C", + "location": "Hauptraum", + "name": "Kueche", + "description": "Sensor im Ofen" + }, + "allowed_credentials": [ + "club-assistant" + ] + }, + { + "sensor_data": { + "unit": "°C", + "location": "Hauptraum", + "description": "Sensor im Hauptraum" + }, + "allowed_credentials": [ + "club-assistant" + ] + } + ], + "humidity": [ + { + "sensor_data": { + "unit": "%", + "location": "Hauptraum", + "name": "Kueche", + "description": "Sensor im Wasserhahn" + }, + "allowed_credentials": [ + "club-assistant" + ] + } + ], + "beverage_supply": [ + { + "sensor_data": { + "unit": "btl" + }, + "allowed_credentials": [ + "club-assistant" + ] + } + ] + }, + "state": { + "open": { + "allowed_credentials": [ + "dooris-hauptraum" + ] + } + } + }, + "response": { + "api_compatibility": [ + "14" + ], + "space": "CCCHH", + "logo": "https://hamburg.ccc.de/images/logo.svg", + "ext_ccc": "erfa", + "url": "https://hamburg.ccc.de/", + "location": { + "address": "Zeiseweg 9, 22765 Hamburg, Germany", + "lon": 9.94444, + "lat": 53.5584 + }, + "contact": { + "phone": "+49 40 23830150", + "irc": "ircs://irc.hackint.org:6697/#ccchh", + "mastodon": "@ccchh@chaos.social", + "email": "mail@hamburg.ccc.de", + "ml": "talk@hamburg.ccc.de", + "matrix": "#ccchh:hamburg.ccc.de" + }, + "feeds": { + "blog": { + "type": "application/rss+xml", + "url": "https://hamburg.ccc.de/feed.xml" + }, + "calendar": { + "type": "ical", + "url": "webcal://cloud.hamburg.ccc.de/remote.php/dav/public-calendars/QJAdExziSnNJEz5g/?export" + } + }, + "links": [ + { + "name": "Wiki", + "url": "https://wiki.hamburg.ccc.de" + }, + { + "name": "Git (Forgejo)", + "url": "https://git.hamburg.ccc.de" + } + ] + } } diff --git a/config/config.go b/config/config.go index 7ad632c..444fd45 100644 --- a/config/config.go +++ b/config/config.go @@ -59,7 +59,7 @@ func getConfigPaths() (paths pathsT) { // If paths is still empty we are missing something if len(paths) == 0 { - log.Fatalln("Error: Neither env:", envConfigPath, "nor cli flag was specified, unable to start without config.") + log.Fatalln("Error: Neither env: ", envConfigPath, "nor cli flag was specified, unable to start without config.") } return } @@ -69,7 +69,7 @@ func ParseConfiguration() (conf SpaceapidConfig) { log.Println("Parsing configuration files") paths := getConfigPaths() - log.Println("Config files:", paths) + log.Println(paths) for _, path := range paths { // Read file file, err := os.ReadFile(path) @@ -86,9 +86,9 @@ func ParseConfiguration() (conf SpaceapidConfig) { } } - // Sanity check: Config should declare compatibility with current SpaceAPI message version - if !slices.Contains(conf.Response.APICompatibility, types.CurrentMessageVersion) { - log.Fatalln("Provided config doesn't specify compatibility with current API version", types.CurrentMessageVersion) + // Check if compatible with v14 + if !slices.Contains(conf.Response.APICompatibility, "14") { + log.Fatalln("Provided file doesn't specify compatibility with API version 14") } // Initialize fields for environment sensors diff --git a/config/types.go b/config/types.go index 315307f..4a305ce 100644 --- a/config/types.go +++ b/config/types.go @@ -34,5 +34,5 @@ type SpaceapidConfig struct { // The dynamic part of the spaceapi JSON Dynamic DynamicStateConfig `json:"dynamic"` // The static part of the spaceapi JSON - Response types.SpaceAPIResponse `json:"static"` + Response types.SpaceAPIResponseV14 `json:"response"` } diff --git a/handlers/root.go b/handlers/root.go index 88cdc55..47091d5 100644 --- a/handlers/root.go +++ b/handlers/root.go @@ -8,7 +8,7 @@ import ( "git.hamburg.ccc.de/ccchh/spaceapid/types" ) -func Root(resp *types.SpaceAPIResponse) func(http.ResponseWriter, *http.Request) { +func Root(resp *types.SpaceAPIResponseV14) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { // Check if GET method if r.Method != http.MethodGet { diff --git a/handlers/sensors.go b/handlers/sensors.go index e3aa51e..187620e 100644 --- a/handlers/sensors.go +++ b/handlers/sensors.go @@ -11,7 +11,7 @@ import ( "git.hamburg.ccc.de/ccchh/spaceapid/types" ) -func EnvironmentSensorPUT( +func EnvironmentSensor( authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, resp *types.EnvironmentSensor, ) func(http.ResponseWriter, *http.Request) { @@ -35,20 +35,3 @@ func EnvironmentSensorPUT( resp.LastChange = time.Now().Unix() } } - -func EnvironmentSensorDELETE( - authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, - resp *types.EnvironmentSensor, -) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - _, err := updateEndpointValidator(authDB, validCredentials, w, r) - if err != nil { - log.Println(err) - return - } - - // Set SpaceAPI response values - resp.Value = 0 - resp.LastChange = time.Now().Unix() - } -} diff --git a/handlers/state.go b/handlers/state.go index d4962c1..d1ab9d9 100644 --- a/handlers/state.go +++ b/handlers/state.go @@ -12,7 +12,7 @@ import ( func StateOpen( authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, - resp *types.SpaceAPIState, + resp *types.SpaceState, ) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { body, err := updateEndpointValidator(authDB, validCredentials, w, r) @@ -37,7 +37,7 @@ func StateOpen( func StateMessagePUT( authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, - resp *types.SpaceAPIState, + resp *types.SpaceState, ) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { body, err := updateEndpointValidator(authDB, validCredentials, w, r) @@ -54,7 +54,7 @@ func StateMessagePUT( func StateMessageDELETE( authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, - resp *types.SpaceAPIState, + resp *types.SpaceState, ) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { _, err := updateEndpointValidator(authDB, validCredentials, w, r) diff --git a/handlers/util.go b/handlers/util.go index 82af6fa..27c2d9f 100644 --- a/handlers/util.go +++ b/handlers/util.go @@ -1,6 +1,7 @@ package handlers import ( + "errors" "fmt" "io" "net/http" @@ -17,16 +18,16 @@ func updateEndpointValidator( // Check BasicAuth credentials username, password, ok := r.BasicAuth() if !ok || !util.CheckCredentials(authDB, validCredentials, username, password) { - w.Header().Set("WWW-Authenticate", "Basic realm=\"spaceapid\"") + w.Header()["WWW-Authenticate"] = []string{"Basic realm=\"spaceapid\""} http.Error(w, "", http.StatusUnauthorized) - return []byte{}, fmt.Errorf("Unauthorized request from %s Username: %s Password: %s", r.RemoteAddr, username, password) + return []byte{}, errors.New(fmt.Sprintf("Unauthorized request from %s Username: %s Password: %s", r.RemoteAddr, username, password)) } // Read request body body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "", http.StatusInternalServerError) - return []byte{}, fmt.Errorf("Failed to read request body from %s with error: %s", r.RemoteAddr, err) + return []byte{}, errors.New(fmt.Sprintf("Failed to read request body from %s with error: %s", r.RemoteAddr, err)) } return body, nil diff --git a/main.go b/main.go index 4b01eee..35b564d 100644 --- a/main.go +++ b/main.go @@ -25,9 +25,6 @@ func init() { } func main() { - if version == "" { - version = "unknown" - } log.Println("SpaceAPId version", version) flag.Parse() @@ -46,7 +43,7 @@ func main() { // Register signal handler sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM) - go func(resp *types.SpaceAPIResponse) { + go func(resp *types.SpaceAPIResponseV14) { <-sc log.Println("Saving state and shutting down...") persistence.SaveCurrentState(*resp) @@ -71,18 +68,12 @@ func main() { // Register handlers for environmental sensors for sensorType, envSensorConfigs := range conf.Dynamic.Sensors { for i, envSensorConfig := range envSensorConfigs { - urlPattern := util.GetSensorURLPath( + urlPattern := "PUT " + util.GetSensorURLPath( sensorType, envSensorConfig.SensorData.Location, envSensorConfig.SensorData.Name, ) http.HandleFunc( - "PUT "+urlPattern, - handlers.EnvironmentSensorPUT( - conf.Credentials, envSensorConfig.AllowedCredentials, &conf.Response.Sensors[sensorType][i], - ), - ) - http.HandleFunc( - "DELETE "+urlPattern, - handlers.EnvironmentSensorDELETE( + urlPattern, + handlers.EnvironmentSensor( conf.Credentials, envSensorConfig.AllowedCredentials, &conf.Response.Sensors[sensorType][i], ), ) diff --git a/persistence/persistentState.go b/persistence/persistentState.go index 87c3121..74fbdb2 100644 --- a/persistence/persistentState.go +++ b/persistence/persistentState.go @@ -16,11 +16,11 @@ const ( // MergeOldState merges a given SpaceAPIResponse with the state saved at the time of last program exit and then deletes // the file containing the old state. -func MergeOldState(response *types.SpaceAPIResponse) { +func MergeOldState(response *types.SpaceAPIResponseV14) { var ( err error oldState []byte - persistedState types.PersistentSpaceState + persistedState types.PersistentStateV14 ) log.Println("Merging old state from", persistentStatePath, "...") @@ -48,7 +48,7 @@ func MergeOldState(response *types.SpaceAPIResponse) { } // Merge state - response.State.FromPersistentSpaceState(persistedState) + response.State = persistedState.State // Merge sensors for key, environmentSensors := range persistedState.Sensors { @@ -72,7 +72,7 @@ removeOld: } } -func SaveCurrentState(response types.SpaceAPIResponse) { +func SaveCurrentState(response types.SpaceAPIResponseV14) { // Create state directory if not present err := os.MkdirAll(persistentStateDir, 0750) if err != nil { @@ -87,7 +87,7 @@ func SaveCurrentState(response types.SpaceAPIResponse) { defer file.Close() // Create persistent state - persistentStateV14 := types.PersistentSpaceState{ + persistentStateV14 := types.PersistentStateV14{ State: struct { Open bool `json:"open"` LastChange int64 `json:"lastchange"` diff --git a/types/v14.go b/types/v14.go new file mode 100644 index 0000000..3763fe4 --- /dev/null +++ b/types/v14.go @@ -0,0 +1,69 @@ +package types + +type SpaceAPIResponseV14 struct { + APICompatibility []string `json:"api_compatibility"` + Space string `json:"space"` + Logo string `json:"logo"` + ExtCCC string `json:"ext_ccc"` + URL string `json:"url"` + Location struct { + Address string `json:"address"` + Lat float64 `json:"lat"` + Lon float64 `json:"lon"` + } `json:"location"` + State SpaceState `json:"state"` + Contact struct { + Phone string `json:"phone"` + IRC string `json:"irc"` + Mastodon string `json:"mastodon"` + Email string `json:"email"` + ML string `json:"ml"` + Matrix string `json:"matrix"` + } `json:"contact"` + Sensors map[string][]EnvironmentSensor `json:"sensors"` + Feeds struct { + Blog struct { + Type string `json:"type"` + URL string `json:"url"` + } `json:"blog"` + Calendar struct { + Type string `json:"type"` + URL string `json:"url"` + } `json:"calendar"` + } `json:"feeds"` + Links []struct { + Name string `json:"name"` + URL string `json:"url"` + } `json:"links"` +} + +type SpaceState struct { + Open bool `json:"open"` + LastChange int64 `json:"lastchange"` + Message string `json:"message,omitempty"` +} + +type EnvironmentSensor struct { + Value float64 `json:"value"` + Unit string `json:"unit"` + Location string `json:"location,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + LastChange int64 `json:"lastchange,omitempty"` +} + +type PersistentStateV14 struct { + State struct { + Open bool `json:"open"` + LastChange int64 `json:"lastchange"` + Message string `json:"message,omitempty"` + } `json:"state"` + Sensors map[string][]PersistentEnvironmentSensor `json:"sensors,omitempty"` +} + +type PersistentEnvironmentSensor struct { + Value float64 `json:"value"` + Location string `json:"location,omitempty"` + Name string `json:"name,omitempty"` + LastChange int64 `json:"lastchange"` +} diff --git a/types/v15.go b/types/v15.go deleted file mode 100644 index 5180ba6..0000000 --- a/types/v15.go +++ /dev/null @@ -1,164 +0,0 @@ -package types - -const CurrentMessageVersion = "15" - -// SpaceAPIResponse represents the contents of the SpaceAPI message -// -// Currently implemented version: 15 -type SpaceAPIResponse struct { - APICompatibility []string `json:"api_compatibility"` // REQUIRED - Space string `json:"space"` // REQUIRED - Logo string `json:"logo"` // REQUIRED - URL string `json:"url"` // REQUIRED - - // Minimum number of properties: 1 - Location struct { - Address string `json:"address,omitzero"` - Lat float64 `json:"lat,omitzero"` - Lon float64 `json:"lon,omitzero"` - Timezone string `json:"timezone,omitzero"` - CountryCode string `json:"country_code,omitzero"` - Hint string `json:"hint,omitzero"` - // Minimum number of items: 1 - Areas []SpaceAPILocationAreas `json:"areas,omitempty"` - } `json:"location,omitzero"` - - SpaceFed struct { - SpaceNet bool `json:"spacenet,omitzero"` - SpaceSAML bool `json:"spacesaml,omitzero"` - } `json:"spacefed,omitzero"` - - Cam []string `json:"cam,omitempty"` - - State SpaceAPIState `json:"state,omitzero"` - - Events []SpaceAPIEvents `json:"events,omitempty"` - - Contact struct { - Phone string `json:"phone,omitzero"` - Sip string `json:"sip,omitzero"` - // keymasters - Keymasters []struct { - Name string `json:"name,omitzero"` - IRCNick string `json:"irc_nick,omitzero"` - Phone string `json:"phone,omitzero"` - Email string `json:"email,omitzero"` - Twitter string `json:"twitter,omitzero"` - Xmpp string `json:"xmpp,omitzero"` - Mastodon string `json:"mastodon,omitzero"` - Matrix string `json:"matrix,omitzero"` - } `json:"keymasters,omitempty"` - IRC string `json:"irc,omitzero"` - Twitter string `json:"twitter,omitzero"` - Mastodon string `json:"mastodon,omitzero"` - Facebook string `json:"facebook,omitzero"` - Identica string `json:"identica,omitzero"` - Foursquare string `json:"foursquare,omitzero"` - Email string `json:"email,omitzero"` - ML string `json:"ml,omitzero"` - XMPP string `json:"xmpp,omitzero"` - IssueMail string `json:"issue_mail,omitzero"` - Gopher string `json:"gopher,omitzero"` - Matrix string `json:"matrix,omitzero"` - Mumble string `json:"mumble,omitzero"` - } `json:"contact"` - - Sensors map[string][]EnvironmentSensor `json:"sensors,omitempty"` - - Feeds struct { - Blog struct { - Type string `json:"type,omitzero"` - URL string `json:"url"` // REQUIRED - } `json:"blog,omitzero"` - Wiki struct { - Type string `json:"type,omitzero"` - URL string `json:"url"` // REQUIRED - } `json:"wiki,omitzero"` - Calendar struct { - Type string `json:"type,omitzero"` - URL string `json:"url"` // REQUIRED - } `json:"calendar,omitzero"` - Flickr struct { - Type string `json:"type,omitzero"` - URL string `json:"url"` // REQUIRED - } `json:"flickr,omitzero"` - } `json:"feeds,omitzero"` - - Projects []string `json:"projects,omitempty"` - - Links []struct { - Name string `json:"name"` // REQUIRED - URL string `json:"url"` // REQUIRED - Description string `json:"description,omitzero"` - } `json:"links,omitempty"` - - MembershipPlans []struct { - Name string `json:"name"` // REQUIRED - Value int64 `json:"value"` // REQUIRED - Currency string `json:"currency"` // REQUIRED - BillingInterval string `json:"billing_interval"` // REQUIRED - Description string `json:"description,omitzero"` - } `json:"membership_plans,omitempty"` - - LinkedSpaces []struct { - Endpoint string `json:"endpoint,omitzero"` - Website string `json:"website,omitzero"` - } `json:"linked_spaces,omitempty"` - - ExtCCC string `json:"ext_ccc,omitzero"` -} - -type SpaceAPILocationAreas struct { - Name string `json:"name,omitzero"` - Description string `json:"description,omitzero"` - SquareMeters float64 `json:"square_meters"` // REQUIRED -} - -type SpaceAPIState struct { - Open bool `json:"open,omitempty"` - LastChange int64 `json:"lastchange,omitempty"` - TriggerPerson string `json:"trigger_person,omitempty"` - Message string `json:"message,omitempty"` - Icon struct { - Open string `json:"open,omitempty"` // REQUIRED - Closed string `json:"closed,omitempty"` // REQUIRED - } `json:"icon,omitzero"` -} - -func (s *SpaceAPIState) FromPersistentSpaceState(ps PersistentSpaceState) { - s.Open = ps.State.Open - s.LastChange = ps.State.LastChange - s.Message = ps.State.Message -} - -type SpaceAPIEvents struct { - Name string `json:"name"` // REQUIRED - Type string `json:"type"` // REQUIRED - Timestamp int64 `json:"timestamp"` // REQUIRED - Extra string `json:"extra,omitzero"` -} - -type EnvironmentSensor struct { - Value float64 `json:"value"` - Unit string `json:"unit"` - Location string `json:"location,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - LastChange int64 `json:"lastchange,omitempty"` -} - -type PersistentSpaceState struct { - State struct { - Open bool `json:"open"` - LastChange int64 `json:"lastchange"` - Message string `json:"message,omitempty"` - } `json:"state"` - Sensors map[string][]PersistentEnvironmentSensor `json:"sensors,omitempty"` -} - -type PersistentEnvironmentSensor struct { - Value float64 `json:"value"` - Location string `json:"location,omitempty"` - Name string `json:"name,omitempty"` - LastChange int64 `json:"lastchange"` -}