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 diff --git a/README.md b/README.md index 4cace42..da23ea6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ $ curl http://localhost:8080 | jq { "api_compatibility": [ - "14" + "14", + "15" ], "space": "CCCHH", ... @@ -15,16 +16,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 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 444fd45..05727aa 100644 --- a/config/config.go +++ b/config/config.go @@ -2,7 +2,7 @@ package config import ( "bytes" - "encoding/json" + "encoding/json/v2" "flag" "fmt" "log" @@ -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(paths) + log.Println("Config files:", paths) for _, path := range paths { // Read file file, err := os.ReadFile(path) @@ -78,17 +78,15 @@ func ParseConfiguration() (conf SpaceapidConfig) { } // Parse JSON - dec := json.NewDecoder(bytes.NewReader(file)) - dec.DisallowUnknownFields() - err = dec.Decode(&conf) + err = json.UnmarshalRead(bytes.NewReader(file), &conf, json.RejectUnknownMembers(true)) if err != nil { log.Fatalln("Could not parse spaceapid config file:", err) } } - // 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/go.mod b/go.mod index 506613d..61b6d1f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module git.hamburg.ccc.de/ccchh/spaceapid -go 1.22 +go 1.27 diff --git a/handlers/root.go b/handlers/root.go index 47091d5..e79023a 100644 --- a/handlers/root.go +++ b/handlers/root.go @@ -1,14 +1,14 @@ package handlers import ( - "encoding/json" + "encoding/json/v2" "log" "net/http" "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 { @@ -19,7 +19,7 @@ func Root(resp *types.SpaceAPIResponseV14) func(http.ResponseWriter, *http.Reque } // Serialize response - response, err := json.Marshal(resp) + response, err := json.Marshal(resp, json.Deterministic(true)) if err != nil { log.Println("Failed to serialize JSON response:", err) w.WriteHeader(http.StatusInternalServerError) 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/handlers/state.go b/handlers/state.go index d1ab9d9..47d7878 100644 --- a/handlers/state.go +++ b/handlers/state.go @@ -10,9 +10,9 @@ import ( "git.hamburg.ccc.de/ccchh/spaceapid/types" ) -func StateOpen( +func StateOpenPUT( 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) @@ -22,7 +22,7 @@ func StateOpen( } // Parse request body - newState, err := strconv.ParseBool(string(body)) + doorNowOpen, err := strconv.ParseBool(string(body)) if err != nil { log.Println("Failed to parse request body from", r.RemoteAddr, "with error:", err) http.Error(w, "HTTP request body should either be true or false", http.StatusBadRequest) @@ -30,14 +30,35 @@ func StateOpen( } // Set SpaceAPI response values - resp.Open = newState + if doorNowOpen { + resp.Open = types.SpaceAPIStateOpenIsOpen + } else { + resp.Open = types.SpaceAPIStateOpenIsClosed + } + resp.LastChange = time.Now().Unix() + } +} + +func StateOpenDELETE( + authDB config.HTTPBACredentials, validCredentials []config.HTTPBACredentialID, + resp *types.SpaceAPIState, +) 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.Open = types.SpaceAPIStateOpenIsUnknown resp.LastChange = time.Now().Unix() } } 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 +75,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/handlers/util.go b/handlers/util.go index 27c2d9f..82af6fa 100644 --- a/handlers/util.go +++ b/handlers/util.go @@ -1,7 +1,6 @@ package handlers import ( - "errors" "fmt" "io" "net/http" @@ -18,16 +17,16 @@ 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)) + 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 diff --git a/main.go b/main.go index 35b564d..23bd31b 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "flag" + "fmt" "log" "net/http" "os" @@ -16,15 +17,22 @@ import ( ) var ( - flagHelp bool - version string + flagHelp bool + listenAddress string + listenPort string + version string ) func init() { + flag.StringVar(&listenAddress, "i", "", "IP address the HTTP server should bind to (all available addresses by default)") + flag.StringVar(&listenPort, "p", "8080", "Port the HTTP server should listen on") flag.BoolVar(&flagHelp, "h", false, "Print help output") } func main() { + if version == "" { + version = "unknown" + } log.Println("SpaceAPId version", version) flag.Parse() @@ -43,7 +51,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) @@ -56,7 +64,10 @@ func main() { ) // state->open http.HandleFunc("PUT /state/open", - handlers.StateOpen(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response.State), + handlers.StateOpenPUT(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response.State), + ) + http.HandleFunc("DELETE /state/open", + handlers.StateOpenDELETE(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response.State), ) // state->message http.HandleFunc("PUT /state/message", @@ -68,12 +79,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], ), ) @@ -82,5 +99,5 @@ func main() { // Start webserver log.Println("Starting HTTP server...") - log.Fatalln(http.ListenAndServe(":8080", nil)) + log.Fatalln(http.ListenAndServe(fmt.Sprintf("%s:%s", listenAddress, listenPort), nil)) } diff --git a/persistence/persistentState.go b/persistence/persistentState.go index 74fbdb2..1f72394 100644 --- a/persistence/persistentState.go +++ b/persistence/persistentState.go @@ -1,7 +1,8 @@ package persistence import ( - "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "errors" "log" "os" @@ -16,11 +17,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.PersistentState ) log.Println("Merging old state from", persistentStatePath, "...") @@ -48,7 +49,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 +73,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,27 +88,38 @@ func SaveCurrentState(response types.SpaceAPIResponseV14) { defer file.Close() // Create persistent state - persistentStateV14 := types.PersistentStateV14{ - State: struct { - Open bool `json:"open"` - LastChange int64 `json:"lastchange"` - Message string `json:"message,omitempty"` - }{Open: response.State.Open, LastChange: response.State.LastChange, Message: response.State.Message}, + var ( + isOpen bool + isUnknown bool + ) + switch response.State.Open { + case types.SpaceAPIStateOpenIsUnknown: + isUnknown = true + case types.SpaceAPIStateOpenIsClosed: + break + case types.SpaceAPIStateOpenIsOpen: + isOpen = true + } + persistentState := types.PersistentState{ + State: types.PersistentSpaceState{ + Open: isOpen, IsUnknown: isUnknown, + LastChange: response.State.LastChange, Message: response.State.Message, + }, + Sensors: make(map[string][]types.PersistentEnvironmentSensor), } // Save sensor state - persistentStateV14.Sensors = make(map[string][]types.PersistentEnvironmentSensor) for key, environmentSensors := range response.Sensors { - persistentStateV14.Sensors[key] = make([]types.PersistentEnvironmentSensor, len(environmentSensors)) + persistentState.Sensors[key] = make([]types.PersistentEnvironmentSensor, len(environmentSensors)) for i, sensor := range environmentSensors { - persistentStateV14.Sensors[key][i].Value = sensor.Value - persistentStateV14.Sensors[key][i].Location = sensor.Location - persistentStateV14.Sensors[key][i].Name = sensor.Name - persistentStateV14.Sensors[key][i].LastChange = sensor.LastChange + persistentState.Sensors[key][i].Value = sensor.Value + persistentState.Sensors[key][i].Location = sensor.Location + persistentState.Sensors[key][i].Name = sensor.Name + persistentState.Sensors[key][i].LastChange = sensor.LastChange } } // Serialize persistent state - marshal, err := json.MarshalIndent(persistentStateV14, "", "\t") + marshal, err := json.Marshal(persistentState, json.JoinOptions(jsontext.Multiline(true), jsontext.SpaceAfterComma(true))) if err != nil { log.Fatalln("Failed serializing persistent state... error:", err) } 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..b844175 --- /dev/null +++ b/types/v15.go @@ -0,0 +1,202 @@ +package types + +import ( + "encoding/json/v2" +) + +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 SpaceAPIStateOpen int + +const ( + SpaceAPIStateOpenIsUnknown = iota + SpaceAPIStateOpenIsClosed + SpaceAPIStateOpenIsOpen +) + +func (state SpaceAPIStateOpen) MarshalJSON() ([]byte, error) { + switch state { + case SpaceAPIStateOpenIsClosed: + return json.Marshal(false) + case SpaceAPIStateOpenIsOpen: + return json.Marshal(true) + default: + return []byte{}, nil + } +} + +func (state SpaceAPIStateOpen) IsZero() bool { + return state == SpaceAPIStateOpenIsUnknown +} + +type SpaceAPIState struct { + Open SpaceAPIStateOpen `json:"open,omitzero"` + 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 PersistentState) { + if ps.State.IsUnknown { + s.Open = SpaceAPIStateOpenIsUnknown + } else { + if ps.State.Open { + s.Open = SpaceAPIStateOpenIsOpen + } else { + s.Open = SpaceAPIStateOpenIsClosed + } + } + 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 PersistentState struct { + State PersistentSpaceState `json:"state"` + Sensors map[string][]PersistentEnvironmentSensor `json:"sensors,omitempty"` +} + +type PersistentSpaceState struct { + Open bool `json:"open"` + IsUnknown bool `json:"is_unknown"` + LastChange int64 `json:"lastchange"` + Message string `json:"message,omitempty"` +} + +type PersistentEnvironmentSensor struct { + Value float64 `json:"value"` + Location string `json:"location,omitempty"` + Name string `json:"name,omitempty"` + LastChange int64 `json:"lastchange"` +}