From 74fbef4ae80d50ee6ee28b262fd171ab0a9ebdef Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 25 Jul 2026 21:09:33 +0200 Subject: [PATCH 1/9] refactor(handlers): Use http.Header.Set instead of direct map access --- handlers/util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handlers/util.go b/handlers/util.go index 27c2d9f..d7e6dd5 100644 --- a/handlers/util.go +++ b/handlers/util.go @@ -18,7 +18,7 @@ func updateEndpointValidator( // Check BasicAuth credentials username, password, ok := r.BasicAuth() if !ok || !util.CheckCredentials(authDB, validCredentials, username, password) { - w.Header()["WWW-Authenticate"] = []string{"Basic realm=\"spaceapid\""} + w.Header().Set("WWW-Authenticate", "Basic realm=\"spaceapid\"") http.Error(w, "", http.StatusUnauthorized) return []byte{}, errors.New(fmt.Sprintf("Unauthorized request from %s Username: %s Password: %s", r.RemoteAddr, username, password)) } From e714cca47781db5547e052899d5bf8db24f73d84 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 25 Jul 2026 21:14:07 +0200 Subject: [PATCH 2/9] refactor(handlers): Use fmt.Errorf instead of errors.New(fmt.Sprintf()) --- handlers/util.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/handlers/util.go b/handlers/util.go index d7e6dd5..82af6fa 100644 --- a/handlers/util.go +++ b/handlers/util.go @@ -1,7 +1,6 @@ package handlers import ( - "errors" "fmt" "io" "net/http" @@ -20,14 +19,14 @@ func updateEndpointValidator( if !ok || !util.CheckCredentials(authDB, validCredentials, username, password) { w.Header().Set("WWW-Authenticate", "Basic realm=\"spaceapid\"") http.Error(w, "", http.StatusUnauthorized) - return []byte{}, errors.New(fmt.Sprintf("Unauthorized request from %s Username: %s Password: %s", r.RemoteAddr, username, password)) + return []byte{}, fmt.Errorf("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{}, errors.New(fmt.Sprintf("Failed to read request body from %s with error: %s", r.RemoteAddr, err)) + return []byte{}, fmt.Errorf("Failed to read request body from %s with error: %s", r.RemoteAddr, err) } return body, nil From 1d49948615318c317526012ee806d63b23f89f50 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 25 Jul 2026 21:13:23 +0200 Subject: [PATCH 3/9] fix(config): Remove redundant space from log message --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index 444fd45..b57d615 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 } From b54d4a39cd1a9b1cf5d23e752c09cd70ebe27006 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 25 Jul 2026 22:10:50 +0200 Subject: [PATCH 4/9] fix(config): Mention what that list of paths in the log is --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index b57d615..bf81c0a 100644 --- a/config/config.go +++ b/config/config.go @@ -69,7 +69,7 @@ func ParseConfiguration() (conf SpaceapidConfig) { log.Println("Parsing configuration files") paths := getConfigPaths() - log.Println(paths) + log.Println("Config files:", paths) for _, path := range paths { // Read file file, err := os.ReadFile(path) From b054b02be6c92deadb384be66a41d66dd6b08843 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 25 Jul 2026 22:09:15 +0200 Subject: [PATCH 5/9] fix(handlers): Handle DELETE for environment sensors It now works as advertised in the README! *ahem*... --- handlers/sensors.go | 19 ++++++++++++++++++- main.go | 12 +++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/handlers/sensors.go b/handlers/sensors.go index 187620e..e3aa51e 100644 --- a/handlers/sensors.go +++ b/handlers/sensors.go @@ -11,7 +11,7 @@ import ( "git.hamburg.ccc.de/ccchh/spaceapid/types" ) -func EnvironmentSensor( +func EnvironmentSensorPUT( authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, resp *types.EnvironmentSensor, ) func(http.ResponseWriter, *http.Request) { @@ -35,3 +35,20 @@ func EnvironmentSensor( 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/main.go b/main.go index 35b564d..3e867b8 100644 --- a/main.go +++ b/main.go @@ -68,12 +68,18 @@ func main() { // Register handlers for environmental sensors for sensorType, envSensorConfigs := range conf.Dynamic.Sensors { for i, envSensorConfig := range envSensorConfigs { - urlPattern := "PUT " + util.GetSensorURLPath( + urlPattern := util.GetSensorURLPath( sensorType, envSensorConfig.SensorData.Location, envSensorConfig.SensorData.Name, ) http.HandleFunc( - urlPattern, - handlers.EnvironmentSensor( + "PUT "+urlPattern, + handlers.EnvironmentSensorPUT( + conf.Credentials, envSensorConfig.AllowedCredentials, &conf.Response.Sensors[sensorType][i], + ), + ) + http.HandleFunc( + "DELETE "+urlPattern, + handlers.EnvironmentSensorDELETE( conf.Credentials, envSensorConfig.AllowedCredentials, &conf.Response.Sensors[sensorType][i], ), ) From 4540fc0c59b75783ed8b1f1ab29dcc5c3f5b9d19 Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 25 Jul 2026 22:49:01 +0200 Subject: [PATCH 6/9] docs: Add missing backticks in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cace42..57d8581 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ $ 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: From a163ba1deb42114bbd6c6e87fb78402e6992168d Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 25 Jul 2026 22:50:53 +0200 Subject: [PATCH 7/9] style: Add .editorconfig --- .editorconfig | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bc18dc4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +trim_trailing_whitespace = true +insert_final_newline = true + +[*.json] +indent_style = tab +tab_width = 4 From de506fc8f3a958b073b93f30876298dbb465ac7d Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 25 Jul 2026 22:06:13 +0200 Subject: [PATCH 8/9] feat: Log that version is unknown if version == "" --- main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/main.go b/main.go index 3e867b8..dc374bc 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,9 @@ func init() { } func main() { + if version == "" { + version = "unknown" + } log.Println("SpaceAPId version", version) flag.Parse() From a53a5c3517bebc89773194ae6ff1beada7639faa Mon Sep 17 00:00:00 2001 From: Bennett Wetters Date: Sat, 25 Jul 2026 22:35:17 +0200 Subject: [PATCH 9/9] feat: SpaceAPI v15 support Delete types/v14.go, replace with v15.go. Rename internal types to not include message version. I don't currently see the need to support multiple message versions that have incompatible layouts. v15 is backwards compatible with v14, so the config example now specifies both. Rename the "response" field in the config file to "static" as dynamic/static makes more sense from the user's perspective. (Internally it's still SpaceapidConfig.{Dynamic,Response} to align with type names and usage.) --- README.md | 9 +- config-template.json | 219 +++++++++++++++++---------------- config/config.go | 6 +- config/types.go | 2 +- handlers/root.go | 2 +- handlers/state.go | 6 +- main.go | 2 +- persistence/persistentState.go | 10 +- types/v14.go | 69 ----------- types/v15.go | 164 ++++++++++++++++++++++++ 10 files changed, 293 insertions(+), 196 deletions(-) delete mode 100644 types/v14.go create mode 100644 types/v15.go diff --git a/README.md b/README.md index 57d8581..da23ea6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ $ curl http://localhost:8080 | jq { "api_compatibility": [ - "14" + "14", + "15" ], "space": "CCCHH", ... @@ -22,9 +23,9 @@ The config consists of three parts: - `"credentials"` - List of Username/Password credentials for HTTP BasicAuth - `"dynamic"` - - The configuration for the dynamic parts of the message -- `"response"` - - The static (pre-filled) parts of the response + - The dynamic parts of the message +- `"static"` + - The static (pre-filled) parts of the message See [Running](#Running) for details. diff --git a/config-template.json b/config-template.json index 31cb500..aac6046 100644 --- a/config-template.json +++ b/config-template.json @@ -1,111 +1,112 @@ { - "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" - } - ] - } + "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" + } + ] + } } diff --git a/config/config.go b/config/config.go index bf81c0a..7ad632c 100644 --- a/config/config.go +++ b/config/config.go @@ -86,9 +86,9 @@ func ParseConfiguration() (conf SpaceapidConfig) { } } - // Check if compatible with v14 - if !slices.Contains(conf.Response.APICompatibility, "14") { - log.Fatalln("Provided file doesn't specify compatibility with API version 14") + // 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) } // Initialize fields for environment sensors diff --git a/config/types.go b/config/types.go index 4a305ce..315307f 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.SpaceAPIResponseV14 `json:"response"` + Response types.SpaceAPIResponse `json:"static"` } diff --git a/handlers/root.go b/handlers/root.go index 47091d5..88cdc55 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.SpaceAPIResponseV14) func(http.ResponseWriter, *http.Request) { +func Root(resp *types.SpaceAPIResponse) 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/state.go b/handlers/state.go index d1ab9d9..d4962c1 100644 --- a/handlers/state.go +++ b/handlers/state.go @@ -12,7 +12,7 @@ import ( func StateOpen( authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, - resp *types.SpaceState, + resp *types.SpaceAPIState, ) 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.SpaceState, + resp *types.SpaceAPIState, ) 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.SpaceState, + resp *types.SpaceAPIState, ) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { _, err := updateEndpointValidator(authDB, validCredentials, w, r) diff --git a/main.go b/main.go index dc374bc..4b01eee 100644 --- a/main.go +++ b/main.go @@ -46,7 +46,7 @@ func main() { // Register signal handler sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM) - go func(resp *types.SpaceAPIResponseV14) { + go func(resp *types.SpaceAPIResponse) { <-sc log.Println("Saving state and shutting down...") persistence.SaveCurrentState(*resp) diff --git a/persistence/persistentState.go b/persistence/persistentState.go index 74fbdb2..87c3121 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.SpaceAPIResponseV14) { +func MergeOldState(response *types.SpaceAPIResponse) { var ( err error oldState []byte - persistedState types.PersistentStateV14 + persistedState types.PersistentSpaceState ) log.Println("Merging old state from", persistentStatePath, "...") @@ -48,7 +48,7 @@ func MergeOldState(response *types.SpaceAPIResponseV14) { } // Merge state - response.State = persistedState.State + response.State.FromPersistentSpaceState(persistedState) // Merge sensors for key, environmentSensors := range persistedState.Sensors { @@ -72,7 +72,7 @@ removeOld: } } -func SaveCurrentState(response types.SpaceAPIResponseV14) { +func SaveCurrentState(response types.SpaceAPIResponse) { // Create state directory if not present err := os.MkdirAll(persistentStateDir, 0750) if err != nil { @@ -87,7 +87,7 @@ func SaveCurrentState(response types.SpaceAPIResponseV14) { defer file.Close() // Create persistent state - persistentStateV14 := types.PersistentStateV14{ + persistentStateV14 := types.PersistentSpaceState{ State: struct { Open bool `json:"open"` LastChange int64 `json:"lastchange"` diff --git a/types/v14.go b/types/v14.go deleted file mode 100644 index 3763fe4..0000000 --- a/types/v14.go +++ /dev/null @@ -1,69 +0,0 @@ -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 new file mode 100644 index 0000000..5180ba6 --- /dev/null +++ b/types/v15.go @@ -0,0 +1,164 @@ +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"` +}