diff --git a/hswaw/site/BUILD.bazel b/hswaw/site/BUILD.bazel index 21edded9..5cb3b991 100644 --- a/hswaw/site/BUILD.bazel +++ b/hswaw/site/BUILD.bazel @@ -4,14 +4,17 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") go_library( name = "go_default_library", srcs = [ + "at.go", "feeds.go", "main.go", + "spaceapi.go", "views.go", ], importpath = "code.hackerspace.pl/hscloud/hswaw/site", visibility = ["//visibility:private"], deps = [ "//go/mirko:go_default_library", + "//hswaw/site/calendar:go_default_library", "//hswaw/site/static:static_go", "//hswaw/site/templates:templates_go", "@com_github_golang_glog//:go_default_library", @@ -25,8 +28,8 @@ go_binary( ) container_image( - name="latest", - base="@prodimage-bionic//image", + name = "latest", + base = "@prodimage-bionic//image", files = [":site"], directory = "/hswaw/site/", entrypoint = ["/hswaw/site/site"], diff --git a/hswaw/site/at.go b/hswaw/site/at.go new file mode 100644 index 00000000..70df8c7c --- /dev/null +++ b/hswaw/site/at.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +const ( + atURL = "https://at.hackerspace.pl/api" +) + +// atStatus is the result of queruing checkinator/at (Hackerspace presence +// service). +type atStatus struct { + // Users is the list of present and publicly visible users. + Users []atUser `json:"users"` + // ESPs is the number of ESP{8266,32} devices. + ESPs int `json:"esps"` + // Kektops is the number of nettop “Kektop” devices. + Kektops int `json:"kektops"` + // Unknown is the number of unknown devices in the network. + Unknown int `json:"unknown"` +} + +type atUser struct { + Login string `json:"login"` +} + +func getAt(ctx context.Context) (*atStatus, error) { + r, err := http.NewRequestWithContext(ctx, "GET", atURL, nil) + if err != nil { + return nil, fmt.Errorf("NewRequest(%q): %w", atURL, err) + } + res, err := http.DefaultClient.Do(r) + if err != nil { + return nil, fmt.Errorf("GET: %w", err) + } + defer res.Body.Close() + + var status atStatus + if err := json.NewDecoder(res.Body).Decode(&status); err != nil { + return nil, fmt.Errorf("when decoding JSON: %w", err) + } + + return &status, nil +} diff --git a/hswaw/site/calendar/BUILD.bazel b/hswaw/site/calendar/BUILD.bazel index 297fde32..fcdac53a 100644 --- a/hswaw/site/calendar/BUILD.bazel +++ b/hswaw/site/calendar/BUILD.bazel @@ -3,12 +3,12 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ - "load.go", "event.go", + "load.go", "time.go", ], importpath = "code.hackerspace.pl/hscloud/hswaw/site/calendar", - visibility = ["//visibility:private"], + visibility = ["//hswaw/site:__subpackages__"], deps = [ "@com_github_arran4_golang_ical//:go_default_library", "@com_github_golang_glog//:go_default_library", diff --git a/hswaw/site/calendar/load.go b/hswaw/site/calendar/load.go index 5ea91984..5b36b9cb 100644 --- a/hswaw/site/calendar/load.go +++ b/hswaw/site/calendar/load.go @@ -14,8 +14,8 @@ import ( ) const ( - // eventsURL is the calendar from which we load public Hackerspace events. - eventsURL = "https://owncloud.hackerspace.pl/remote.php/dav/public-calendars/g8toktZrA9fyAHNi/?export" + // EventsURL is the calendar from which we load public Hackerspace events. + EventsURL = "https://owncloud.hackerspace.pl/remote.php/dav/public-calendars/g8toktZrA9fyAHNi/?export" ) // eventsBySooner sorts upcoming events so the one that happens the soonest @@ -107,13 +107,13 @@ func parseUpcomingEvents(now time.Time, data io.Reader) ([]*UpcomingEvent, error // relative to the given time 'now' as per the Warsaw Hackerspace public // calender (from owncloud.hackerspace.pl). func GetUpcomingEvents(ctx context.Context, now time.Time) ([]*UpcomingEvent, error) { - r, err := http.NewRequestWithContext(ctx, "GET", eventsURL, nil) + r, err := http.NewRequestWithContext(ctx, "GET", EventsURL, nil) if err != nil { - return nil, fmt.Errorf("NewRequest(%q): %w", eventsURL, err) + return nil, fmt.Errorf("NewRequest(%q): %w", EventsURL, err) } res, err := http.DefaultClient.Do(r) if err != nil { - return nil, fmt.Errorf("Do(%q): %w", eventsURL, err) + return nil, fmt.Errorf("Do(%q): %w", EventsURL, err) } defer res.Body.Close() return parseUpcomingEvents(now, res.Body) diff --git a/hswaw/site/main.go b/hswaw/site/main.go index a7f3e54a..c4437bc9 100644 --- a/hswaw/site/main.go +++ b/hswaw/site/main.go @@ -102,5 +102,6 @@ func (s *service) handleHTTPStatic(w http.ResponseWriter, r *http.Request) { func (s *service) registerHTTP(mux *http.ServeMux) { mux.HandleFunc("/static/", s.handleHTTPStatic) + mux.HandleFunc("/spaceapi", s.handleSpaceAPI) mux.HandleFunc("/", s.handleIndex) } diff --git a/hswaw/site/spaceapi.go b/hswaw/site/spaceapi.go new file mode 100644 index 00000000..13c5aad5 --- /dev/null +++ b/hswaw/site/spaceapi.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + + "code.hackerspace.pl/hscloud/hswaw/site/calendar" + "github.com/golang/glog" +) + +// SpaceAPIResponse, per https://spaceapi.io/ - kinda. Mostly rewritten from +// old implementation, someone should update this to use the official schema. +type SpaceAPIResponse struct { + API string `json:"api"` + Space string `json:"space"` + Logo string `json:"logo"` + URL string `json:"url"` + Location SpaceAPILocation `json:"location"` + State SpaceAPIState `json:"state"` + Contact map[string]string `json:"contact"` + IssueReportChannels []string `json:"issue_report_channels"` + Projects []string `json:"projects"` + Feeds map[string]SpaceAPIFeed `json:"feeds"` + Sensors map[string][]SpaceAPISensor `json:"sensors"` +} + +type SpaceAPILocation struct { + Latitude float64 `json:"lat"` + Longitude float64 `json:"lon"` + Address string `json:"address"` +} + +type SpaceAPIState struct { + Open bool `json:"open"` + Message string `json:"message"` + Icon struct { + Open string `json:"open"` + Closed string `json:"closed"` + } `json:"icon"` +} + +type SpaceAPIFeed struct { + Type string `json:"type"` + URL string `json:"url"` +} + +type SpaceAPISensor struct { + Value int `json:"value"` + Names []string `json:"names"` +} + +func generateSpaceAPIResponse(ctx context.Context) SpaceAPIResponse { + state := SpaceAPIState{} + state.Icon.Open = "https://static.hackerspace.pl/img/status-open-small.png" + state.Icon.Closed = "https://static.hackerspace.pl/img/status-closed-small.png" + // TODO(q3k): post-coronavirus, make automatically open based on calendar + // events and Open Thursdays. + open := false + if open { + state.Open = true + state.Message = "open for public" + } else { + state.Open = false + state.Message = "members only" + } + + peopleNowPresent := SpaceAPISensor{} + atState, err := getAt(ctx) + if err != nil { + glog.Errorf("Failed to get checkinator status: %v", err) + } else { + peopleNowPresent.Names = make([]string, len(atState.Users)) + for i, u := range atState.Users { + peopleNowPresent.Names[i] = u.Login + } + peopleNowPresent.Value = len(peopleNowPresent.Names) + } + + res := SpaceAPIResponse{ + API: "0.13", + Space: "Warsaw Hackerspace", + Logo: "https://static.hackerspace.pl/img/syrenka-black.png", + URL: "https://hackerspace.pl", + Location: SpaceAPILocation{ + Latitude: 52.24160, + Longitude: 20.98485, + Address: "ul. Wolność 2A, 01-018 Warszawa, Poland", + }, + State: state, + Contact: map[string]string{ + "irc": "irc://irc.libera.chat/#hswaw", + "twitter": "@hackerspacepl", + "facebook": "hackerspacepl", + "ml": "waw@lists.hackerspace.pl", + }, + IssueReportChannels: []string{"irc"}, + Projects: []string{ + "https://wiki.hackerspace.pl/projects", + }, + Feeds: map[string]SpaceAPIFeed{ + "blog": SpaceAPIFeed{ + Type: "atom", + URL: feedsURLs["blog"], + }, + "calendar": SpaceAPIFeed{ + Type: "ical", + URL: calendar.EventsURL, + }, + "wiki": SpaceAPIFeed{ + Type: "rss", + URL: "https://wiki.hackerspace.pl/feed.php", + }, + }, + Sensors: map[string][]SpaceAPISensor{ + "people_now_present": []SpaceAPISensor{peopleNowPresent}, + }, + } + + return res +} diff --git a/hswaw/site/static/landing.css b/hswaw/site/static/landing.css index 431d8eab..3ddae1ac 100644 --- a/hswaw/site/static/landing.css +++ b/hswaw/site/static/landing.css @@ -141,9 +141,19 @@ li i { opacity: 60%; } -#quicklinks { - float: right; - font-family: monospace; - font-size: 14px; - margin: 2rem; +.atlist { + display: inline; + list-style: none; +} + +.atlist li { + display: inline; +} + +.atlist li:after { + content: ", "; +} + +.atlist li:last-child:after { + content: "."; } diff --git a/hswaw/site/templates/index.html b/hswaw/site/templates/index.html index 48f0acdb..058c1620 100644 --- a/hswaw/site/templates/index.html +++ b/hswaw/site/templates/index.html @@ -36,6 +36,28 @@ ul. Wolność 2A

Hackerspace nie zna barier. Jeśli masz ciekawy pomysł i szukasz ludzi chętnych do współpracy lub po prostu potrzebujesz miejsca i sprzętu - zapraszamy!

+

Kto jest teraz w spejsie?

+

+ {{ if ne .AtError nil }} + Ups, nie udało się załadować stanu checkinatora. + {{ else }} + {{ $count := len .AtStatus.Users }} + {{ if gt $count 4 }} + Według naszych instrumentów w spejsie obecnie znajduje się {{ $count }} osób: + {{ else if gt $count 1 }} + Według naszych instrumentów w spejsie obecnie znajdują się {{ $count }} osoby: + {{ else if gt $count 0 }} + Według naszych instrumentów w spejsie obecnie znajduje się jedna osoba: + {{ else }} + Według naszych instrumentów w spejsie obecnie nie ma nikogo. + {{ end }} +

+ {{ end }} +

Blog

diff --git a/hswaw/site/views.go b/hswaw/site/views.go index 59f948e9..09755b2d 100644 --- a/hswaw/site/views.go +++ b/hswaw/site/views.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "html/template" "net/http" @@ -53,7 +54,19 @@ func render(w http.ResponseWriter, t *template.Template, data interface{}) { // handleIndex handles rendering the main page at /. func (s *service) handleIndex(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + atStatus, atError := getAt(ctx) + render(w, tmplIndex, map[string]interface{}{ - "Entries": s.getFeeds(), + "Entries": s.getFeeds(), + "AtStatus": atStatus, + "AtError": atError, }) } + +func (s *service) handleSpaceAPI(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(generateSpaceAPIResponse(ctx)) +}