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 05727aa..444fd45 100644 --- a/config/config.go +++ b/config/config.go @@ -2,7 +2,7 @@ package config import ( "bytes" - "encoding/json/v2" + "encoding/json" "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("Config files:", paths) + log.Println(paths) for _, path := range paths { // Read file file, err := os.ReadFile(path) @@ -78,15 +78,17 @@ func ParseConfiguration() (conf SpaceapidConfig) { } // Parse JSON - err = json.UnmarshalRead(bytes.NewReader(file), &conf, json.RejectUnknownMembers(true)) + dec := json.NewDecoder(bytes.NewReader(file)) + dec.DisallowUnknownFields() + err = dec.Decode(&conf) if err != nil { log.Fatalln("Could not parse spaceapid config file:", err) } } - // 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/go.mod b/go.mod index 61b6d1f..506613d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module git.hamburg.ccc.de/ccchh/spaceapid -go 1.27 +go 1.22 diff --git a/handlers/root.go b/handlers/root.go index e79023a..47091d5 100644 --- a/handlers/root.go +++ b/handlers/root.go @@ -1,14 +1,14 @@ package handlers import ( - "encoding/json/v2" + "encoding/json" "log" "net/http" "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 { @@ -19,7 +19,7 @@ func Root(resp *types.SpaceAPIResponse) func(http.ResponseWriter, *http.Request) } // Serialize response - response, err := json.Marshal(resp, json.Deterministic(true)) + response, err := json.Marshal(resp) 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 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 47d7878..d1ab9d9 100644 --- a/handlers/state.go +++ b/handlers/state.go @@ -10,9 +10,9 @@ import ( "git.hamburg.ccc.de/ccchh/spaceapid/types" ) -func StateOpenPUT( +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) @@ -22,7 +22,7 @@ func StateOpenPUT( } // Parse request body - doorNowOpen, err := strconv.ParseBool(string(body)) + newState, 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,35 +30,14 @@ func StateOpenPUT( } // Set SpaceAPI response values - 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.Open = newState resp.LastChange = time.Now().Unix() } } 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) @@ -75,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 23bd31b..35b564d 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "flag" - "fmt" "log" "net/http" "os" @@ -17,22 +16,15 @@ import ( ) var ( - flagHelp bool - listenAddress string - listenPort string - version string + flagHelp bool + 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() @@ -51,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) @@ -64,10 +56,7 @@ func main() { ) // state->open http.HandleFunc("PUT /state/open", - 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), + handlers.StateOpen(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response.State), ) // state->message http.HandleFunc("PUT /state/message", @@ -79,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], ), ) @@ -99,5 +82,5 @@ func main() { // Start webserver log.Println("Starting HTTP server...") - log.Fatalln(http.ListenAndServe(fmt.Sprintf("%s:%s", listenAddress, listenPort), nil)) + log.Fatalln(http.ListenAndServe(":8080", nil)) } diff --git a/persistence/persistentState.go b/persistence/persistentState.go index 1f72394..74fbdb2 100644 --- a/persistence/persistentState.go +++ b/persistence/persistentState.go @@ -1,8 +1,7 @@ package persistence import ( - "encoding/json/jsontext" - "encoding/json/v2" + "encoding/json" "errors" "log" "os" @@ -17,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.PersistentState + persistedState types.PersistentStateV14 ) log.Println("Merging old state from", persistentStatePath, "...") @@ -49,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 { @@ -73,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 { @@ -88,38 +87,27 @@ func SaveCurrentState(response types.SpaceAPIResponse) { defer file.Close() // Create persistent state - 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), + 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}, } // Save sensor state + persistentStateV14.Sensors = make(map[string][]types.PersistentEnvironmentSensor) for key, environmentSensors := range response.Sensors { - persistentState.Sensors[key] = make([]types.PersistentEnvironmentSensor, len(environmentSensors)) + persistentStateV14.Sensors[key] = make([]types.PersistentEnvironmentSensor, len(environmentSensors)) for i, sensor := range environmentSensors { - 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 + 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 } } // Serialize persistent state - marshal, err := json.Marshal(persistentState, json.JoinOptions(jsontext.Multiline(true), jsontext.SpaceAfterComma(true))) + marshal, err := json.MarshalIndent(persistentStateV14, "", "\t") if err != nil { log.Fatalln("Failed serializing persistent state... error:", err) } 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 b844175..0000000 --- a/types/v15.go +++ /dev/null @@ -1,202 +0,0 @@ -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"` -}