hswaw/site: render main page and blog feed

This reimplements the blog rendering functionality and the main/index
page.

www-main used to combine multiple atom feeds into one (Redmine and the
wordpress blog at blog.hackerspace.pl). We retain the functionality, but
only render the wordpress blog now (some other content might follow).

We also cowardly comment out the broken calendar iframe.

Change-Id: I9abcd8d85149968d06e1cb9c97d72eba7f0bc99f
This commit is contained in:
q3k 2021-05-30 23:15:20 +00:00
parent 56c888b443
commit 3c9092ad5f
5 changed files with 201 additions and 17 deletions

View file

@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = [
"feeds.go",
"main.go",
"views.go",
],

164
hswaw/site/feeds.go Normal file
View file

@ -0,0 +1,164 @@
package main
import (
"context"
"encoding/xml"
"fmt"
"html/template"
"net/http"
"sort"
"time"
"github.com/golang/glog"
)
// This implements 'Atom' feed parsing. Honestly, this was written without
// looking at any spec. If it ever breaks, you know why.
var (
// feedURLs is a map from an atom feed name to its URL. All the following
// feeds will be combined and rendered on the main page of the website.
feedsURLs = map[string]string{
"blog": "https://blog.hackerspace.pl/feed/atom/",
}
)
// atomFeed is a retrieved atom feed.
type atomFeed struct {
XMLName xml.Name `xml:"feed"`
Entries []*atomEntry `xml:"entry"`
}
// atomEntry is an entry (eg. blog post) from an atom feed. It contains fields
// directly from the XML, plus some additional parsed types and metadata.
type atomEntry struct {
XMLName xml.Name `xml:"entry"`
Author string `xml:"author>name"`
Title template.HTML `xml:"title"`
Summary template.HTML `xml:"summary"`
UpdatedRaw string `xml:"updated"`
PublishedRaw string `xml:"published"`
Link struct {
Href string `xml:"href,attr"`
} `xml:"link"`
// Updated is the updated time parsed from UpdatedRaw.
Updated time.Time
// UpdatedHuman is a human-friendly representation of Updated for web rendering.
UpdatedHuman string
// Published is the published time parsed from PublishedRaw.
Published time.Time
// Source is the name of the feed that this entry was retrieved from. Only
// set after combining multiple feeds together (ie. when returned from
// getFeeds).
Source string
}
// getAtomFeed retrieves a single Atom feed from the given URL.
func getAtomFeed(ctx context.Context, url string) (*atomFeed, error) {
r, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("NewRequest(%q): %w", url, err)
}
res, err := http.DefaultClient.Do(r)
if err != nil {
return nil, fmt.Errorf("Do(%q): %w", url, err)
}
defer res.Body.Close()
var feed atomFeed
d := xml.NewDecoder(res.Body)
if err := d.Decode(&feed); err != nil {
return nil, fmt.Errorf("Decode: %w", err)
}
for i, e := range feed.Entries {
updated, err := time.Parse(time.RFC3339, e.UpdatedRaw)
if err != nil {
return nil, fmt.Errorf("entry %d: cannot parse updated date %q: %v", i, e.UpdatedRaw, err)
}
published, err := time.Parse(time.RFC3339, e.PublishedRaw)
if err != nil {
return nil, fmt.Errorf("entry %d: cannot parse published date %q: %v", i, e.PublishedRaw, err)
}
e.Updated = updated
e.Published = published
e.UpdatedHuman = e.Updated.Format("02-01-2006")
if e.Author == "" {
e.Author = "Anonymous"
}
}
return &feed, nil
}
// feedWorker runs a worker which retrieves all atom feeds every minute and
// updates the services' feeds map with the retrieved data. On error, the feeds
// are not updated (whatever is already cached in the map will continue to be
// available) and the error is logged.
func (s *service) feedWorker(ctx context.Context) {
okay := false
get := func() {
feeds := make(map[string]*atomFeed)
prev := okay
okay = true
for name, url := range feedsURLs {
feed, err := getAtomFeed(ctx, url)
if err != nil {
glog.Errorf("Getting feed %v failed: %v", feed, err)
okay = false
continue
}
feeds[name] = feed
}
// Log whenever the first fetch succeeds, or whenever the fetch
// succeeds again (avoiding polluting logs with success messages).
if !prev && okay {
glog.Infof("Feeds okay.")
}
// Update cached feeds.
s.feedsMu.Lock()
s.feeds = feeds
s.feedsMu.Unlock()
}
// Perform initial fetch.
get()
// ... and update every minute.
t := time.NewTicker(time.Minute)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
get()
}
}
}
// getFeeds retrieves the currently cached feeds and combines them into a
// single reverse-chronological timeline, annotating each entries' Source field
// with the name of the feed from where it was retrieved.
func (s *service) getFeeds() []*atomEntry {
s.feedsMu.RLock()
feeds := s.feeds
s.feedsMu.RUnlock()
var res []*atomEntry
for n, feed := range feeds {
for _, entry := range feed.Entries {
e := *entry
e.Source = n
res = append(res, &e)
}
}
sort.Slice(res, func(i, j int) bool {
return res[j].Published.Before(res[i].Published)
})
return res
}

View file

@ -6,6 +6,7 @@ import (
"mime"
"net/http"
"strings"
"sync"
"code.hackerspace.pl/hscloud/go/mirko"
"github.com/golang/glog"
@ -18,6 +19,11 @@ var (
)
type service struct {
// feeds is a map from atom feed name to atom feed. This is updated by a
// background worker.
feeds map[string]*atomFeed
// feedsMu locks the feeds field.
feedsMu sync.RWMutex
}
func main() {
@ -30,6 +36,7 @@ func main() {
}
s := &service{}
go s.feedWorker(mi.Context())
mux := http.NewServeMux()
s.registerHTTP(mux)
@ -65,6 +72,7 @@ func (s *service) handleHTTPStatic(w http.ResponseWriter, r *http.Request) {
func (s *service) registerHTTP(mux *http.ServeMux) {
mux.HandleFunc("/static/", s.handleHTTPStatic)
mux.HandleFunc("/", s.handleMain)
mux.HandleFunc("/about", s.handleAbout)
mux.HandleFunc("/about_en", s.handleAboutEn)
}

View file

@ -1,12 +1,14 @@
{% extends 'basic.html' %}
{% block page_scripts %}
{{ define "page_scripts" }}
<script type="text/javascript" src="https://widgets.twimg.com/j/2/widget.js"></script>
{% endblock %}
{% block page_style %}
{{ end }}
{{ define "page_style" }}
<link rel="stylesheet" href="static/main.css"/>
{% endblock %}
{% block title %}Hackerspace Warszawa - strona główna{% endblock %}
{% block content %}
{{ end }}
{{ define "title" }}Hackerspace Warszawa - strona główna{{ end }}
{{ define "content" }}
<div id="left">
<div id="about">
<h1>Czym jest Hackerspace?</h1>
@ -21,25 +23,26 @@
</div>
<h1>Nowości</h1>
<ul class="news">
{% for e in entries %}
<li class="{{e.tag}}">
<a class="news-title" href="{{e.link}}">
<h3><span class="news-rectangle">blog</span>{{e.title|safe}}</h3>
{{ range .Entries }}
<li class="{{ .Source }}">
<a class="news-title" href="{{ .Link.Href }}">
<h3><span class="news-rectangle">blog</span>{{ .Title }}</h3>
</a>
<p class="news">
{{e.summary|safe}}
{{ .Summary }}
</p>
<p class="news-footer">
Ostatnio aktualizowane {{e.updated_display}} przez {{e.author_detail.name or 'Anonymous'}}
Ostatnio aktualizowane {{ .UpdatedHuman }} przez {{ .Author }}
</p>
</li>
{% endfor %}
{{ end }}
</ul>
<h1>Kalendarz</h1>
<iframe style="max-width: 750px;width:100%;border: none;" height="315" src="https://owncloud.hackerspace.pl/index.php/apps/calendar/embed/g8toktZrA9fyAHNi"></iframe>
<i>borked ,-,</i>
<!-- TODO(q3k): fix this: <iframe style="max-width: 750px;width:100%;border: none;" height="315" src="https://owncloud.hackerspace.pl/index.php/apps/calendar/embed/g8toktZrA9fyAHNi"></iframe> -->
</div>
<div id="right">
{% include "subscribe.html" %}
<!-- TODO(q3k): add this {% include "subscribe.html" %} -->
<h1 class="twitter">Twitter</h1>
<script type="text/javascript">
new TWTR.Widget({
@ -71,4 +74,4 @@
</script>
</div>
<span class="clear"><a href="#top">↑ Powrót na górę ↑</a></span>
{% endblock %}
{{ end }}

View file

@ -42,6 +42,7 @@ func parseTemplates(names ...string) (*template.Template, error) {
var (
tmplAbout = template.Must(parseTemplates("basic", "about"))
tmplAboutEn = template.Must(parseTemplates("basic", "about_en"))
tmplMain = template.Must(parseTemplates("basic", "main"))
)
// render attempts to render a given Go template with data into the HTTP
@ -61,3 +62,10 @@ func (s *service) handleAbout(w http.ResponseWriter, r *http.Request) {
func (s *service) handleAboutEn(w http.ResponseWriter, r *http.Request) {
render(w, tmplAboutEn, nil)
}
// handleMain handles rendering the main page at /.
func (s *service) handleMain(w http.ResponseWriter, r *http.Request) {
render(w, tmplMain, map[string]interface{}{
"Entries": s.getFeeds(),
})
}