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 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/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..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 35b564d..4b01eee 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() @@ -43,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) @@ -68,12 +71,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], ), ) 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"` +}