forked from hswaw/hscloud
hswaw/oodviewer: init
This brings oodviewer into k0. oodviewer started as a py2/flask script running on q3k's personal infra, which is now being turned down. This is a rewrite of that script into similarly mediocre Go, conforming to the exact same mediocre JSON API and spartan HTML interface. This also deploys it into k0 in the oodviewer-prod namespace. It's already running, but the 'oodviewer.q3k.me' TTL has to expire before it begins handling traffic. Change-Id: Ieef1b0f8f0c60e6fa5dbe7701e0a07a4257f99ce
This commit is contained in:
parent
be3adb9e09
commit
5e695e8f9b
13 changed files with 558 additions and 0 deletions
|
@ -2,3 +2,5 @@ hscloud/hswaw
|
||||||
=============
|
=============
|
||||||
|
|
||||||
Services and systems related to the Warsaw Hackerspace (ie. the physical place, not its cloud/ISP infrastructure).
|
Services and systems related to the Warsaw Hackerspace (ie. the physical place, not its cloud/ISP infrastructure).
|
||||||
|
|
||||||
|
- [oodviewer](oodviewer/), a spartan web interface to access our IRC bots' memory
|
||||||
|
|
49
hswaw/oodviewer/BUILD.bazel
Normal file
49
hswaw/oodviewer/BUILD.bazel
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_layer", "container_push")
|
||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = [
|
||||||
|
"app.go",
|
||||||
|
"main.go",
|
||||||
|
"views.go",
|
||||||
|
],
|
||||||
|
importpath = "code.hackerspace.pl/hscloud/hswaw/oodviewer",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//hswaw/oodviewer/templates:go_default_library",
|
||||||
|
"@com_github_golang_glog//:go_default_library",
|
||||||
|
"@com_github_lib_pq//:go_default_library",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "oodviewer",
|
||||||
|
embed = [":go_default_library"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
|
container_layer(
|
||||||
|
name = "layer_bin",
|
||||||
|
files = [
|
||||||
|
":oodviewer",
|
||||||
|
],
|
||||||
|
directory = "/hswaw/",
|
||||||
|
)
|
||||||
|
|
||||||
|
container_image(
|
||||||
|
name = "runtime",
|
||||||
|
base = "@prodimage-bionic//image",
|
||||||
|
layers = [
|
||||||
|
":layer_bin",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
container_push(
|
||||||
|
name = "push",
|
||||||
|
image = ":runtime",
|
||||||
|
format = "Docker",
|
||||||
|
registry = "registry.k0.hswaw.net",
|
||||||
|
repository = "q3k/oodviewer",
|
||||||
|
tag = "{BUILD_TIMESTAMP}-{STABLE_GIT_COMMIT}",
|
||||||
|
)
|
28
hswaw/oodviewer/README.md
Normal file
28
hswaw/oodviewer/README.md
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
Oodviewer
|
||||||
|
=========
|
||||||
|
|
||||||
|
Spartan web interface for the term database of our IRC bot (ood/oof/klacz).
|
||||||
|
|
||||||
|
Go rewrite of a shitty old Python script that q3k wrote and hosted on his own infra. Now productionized!
|
||||||
|
|
||||||
|
Building and Running
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
bazel build //hswaw/oodviewer
|
||||||
|
bazel run //hswaw/oodviewer -- -postgres 'postgres://ood:password@host/ood'
|
||||||
|
|
||||||
|
Production deployment
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Runs on k0, connects to ood's database on boston. Serves from https://oodviewer.q3k.me/.
|
||||||
|
|
||||||
|
To deploy:
|
||||||
|
|
||||||
|
bazel run //hswaw/oodviewer:push
|
||||||
|
# update //hswaw/oodviewer/prod.jsonnet with new image name
|
||||||
|
kubecfg update prod.jsonnet
|
||||||
|
|
||||||
|
Development
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Beg and borrow ood admins for psql credentials. Keep in mind that you will not be able to access the production database over the Internet - either develop on Boston or run a port forward over SSH.
|
117
hswaw/oodviewer/app.go
Normal file
117
hswaw/oodviewer/app.go
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
// app is the model of the oodviewer app.
|
||||||
|
// The data modeled is a K/V map from string ('Term') to list of entries.
|
||||||
|
type app struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// term represents a key in the K/V map of the model.
|
||||||
|
type term struct {
|
||||||
|
// Name of the term, the 'K' of the K/V map.
|
||||||
|
Name string
|
||||||
|
// Count of entries (len(V) of the K/V map).
|
||||||
|
Entries uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// entry is an element contained under a term. A list of entries ([]entry) is
|
||||||
|
// the 'V' of the K/V map.
|
||||||
|
type entry struct {
|
||||||
|
Entry string `json:"entry"`
|
||||||
|
Added int64 `json:"added"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// newApp returns an instantiated app given a lib/pq postgres connection
|
||||||
|
// string.
|
||||||
|
func newApp(postgres string) (*app, error) {
|
||||||
|
db, err := sql.Open("postgres", flagPostgres)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Open: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &app{
|
||||||
|
db: db,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTerms returns all terms stored in the database.
|
||||||
|
func (a *app) getTerms(ctx context.Context) ([]term, error) {
|
||||||
|
rows, err := a.db.QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
_term._name,
|
||||||
|
count(_entry._text)
|
||||||
|
FROM
|
||||||
|
_term
|
||||||
|
LEFT JOIN _entry
|
||||||
|
ON
|
||||||
|
_entry._term_oid = _term._oid
|
||||||
|
GROUP BY _term._oid
|
||||||
|
ORDER BY _term._name
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var res []term
|
||||||
|
for rows.Next() {
|
||||||
|
var name string
|
||||||
|
var count uint64
|
||||||
|
if err := rows.Scan(&name, &count); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res = append(res, term{
|
||||||
|
Name: name,
|
||||||
|
Entries: count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEntries returns all entries of a given term stored in the database.
|
||||||
|
func (a *app) getEntries(ctx context.Context, name string) ([]entry, error) {
|
||||||
|
rows, err := a.db.QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
_entry._text,
|
||||||
|
_entry._added_at,
|
||||||
|
_entry._added_by
|
||||||
|
FROM
|
||||||
|
_term
|
||||||
|
LEFT JOIN _entry
|
||||||
|
ON _entry._term_oid = _term._oid
|
||||||
|
WHERE lower(_term._name) = lower($1)
|
||||||
|
ORDER BY _entry._added_at
|
||||||
|
`, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var res []entry
|
||||||
|
for rows.Next() {
|
||||||
|
var text string
|
||||||
|
var added time.Time
|
||||||
|
var author string
|
||||||
|
if err := rows.Scan(&text, &added, &author); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res = append(res, entry{
|
||||||
|
Entry: text,
|
||||||
|
Added: added.Unix(),
|
||||||
|
Author: author,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
54
hswaw/oodviewer/main.go
Normal file
54
hswaw/oodviewer/main.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagPostgres string
|
||||||
|
flagListen string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Set("logtostderr", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRobots(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Prevent indexing by any (honest) search engine.
|
||||||
|
fmt.Fprintf(w, "User-agent: *\nDisallow: /\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.StringVar(&flagPostgres, "postgres", "", "Postgres connection string (see lib/pq docs)")
|
||||||
|
flag.StringVar(&flagListen, "listen", "127.0.0.1:8080", "Address to listen at for public HTTP traffic")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
|
||||||
|
a, err := newApp(flagPostgres)
|
||||||
|
if err != nil {
|
||||||
|
glog.Exitf("newApp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/robots.txt", handleRobots)
|
||||||
|
|
||||||
|
http.HandleFunc("/terms.json", a.handleTermsJson)
|
||||||
|
http.HandleFunc("/term.json/", a.handleTermJson)
|
||||||
|
http.HandleFunc("/randomterm.json/", a.handleRandomTermJson)
|
||||||
|
|
||||||
|
http.HandleFunc("/terms", a.handleTerms)
|
||||||
|
http.HandleFunc("/", a.handleTerms)
|
||||||
|
|
||||||
|
http.HandleFunc("/term/", a.handleTerm)
|
||||||
|
|
||||||
|
glog.Infof("Listening at %q", flagListen)
|
||||||
|
if err := http.ListenAndServe(flagListen, nil); err != nil {
|
||||||
|
glog.Exit(err)
|
||||||
|
}
|
||||||
|
}
|
85
hswaw/oodviewer/prod.jsonnet
Normal file
85
hswaw/oodviewer/prod.jsonnet
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
// Production deployment of oodviewer.q3k.me.
|
||||||
|
//
|
||||||
|
// See README.md for more information.
|
||||||
|
|
||||||
|
local kube = import "../../kube/kube.libsonnet";
|
||||||
|
|
||||||
|
{
|
||||||
|
local top = self,
|
||||||
|
local cfg = self.cfg,
|
||||||
|
ns: kube.Namespace("oodviewer-prod"),
|
||||||
|
|
||||||
|
cfg:: {
|
||||||
|
dbUser: "ood",
|
||||||
|
dbPass: std.split(importstr "secrets/plain/postgres-pass", "\n")[0],
|
||||||
|
dbHost: "hackerspace.pl",
|
||||||
|
dbName: "ood",
|
||||||
|
postgresConnectionString: "postgres://%s:%s@%s/%s?sslmode=disable" % [cfg.dbUser, cfg.dbPass, cfg.dbHost, cfg.dbName],
|
||||||
|
|
||||||
|
image: "registry.k0.hswaw.net/q3k/oodviewer:315532800-937278cfb82e41dd2d2010cbd184834b3392116b",
|
||||||
|
domain: "oodviewer.q3k.me",
|
||||||
|
},
|
||||||
|
|
||||||
|
secret: top.ns.Contain(kube.Secret("oodviewer")) {
|
||||||
|
data_: {
|
||||||
|
"postgres": cfg.postgresConnectionString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
deploy: top.ns.Contain(kube.Deployment("oodviewer")) {
|
||||||
|
spec+: {
|
||||||
|
replicas: 3,
|
||||||
|
template+: {
|
||||||
|
spec+: {
|
||||||
|
containers_: {
|
||||||
|
default: kube.Container("default") {
|
||||||
|
image: cfg.image,
|
||||||
|
command: [
|
||||||
|
"/hswaw/oodviewer",
|
||||||
|
"-listen", "0.0.0.0:8080",
|
||||||
|
"-postgres", "$(POSTGRES)",
|
||||||
|
],
|
||||||
|
env_: {
|
||||||
|
POSTGRES: kube.SecretKeyRef(top.secret, "postgres"),
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
requests: { cpu: "0.01", memory: "64M" },
|
||||||
|
limits: { cpu: "1", memory: "256M" },
|
||||||
|
},
|
||||||
|
ports_: {
|
||||||
|
http: { containerPort: 8080 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
service: top.ns.Contain(kube.Service("oodviewer")) {
|
||||||
|
target_pod:: top.deploy.spec.template,
|
||||||
|
},
|
||||||
|
|
||||||
|
ingress: top.ns.Contain(kube.Ingress("oodviewer")) {
|
||||||
|
metadata+: {
|
||||||
|
annotations+: {
|
||||||
|
"kubernetes.io/tls-acme": "true",
|
||||||
|
"certmanager.k8s.io/cluster-issuer": "letsencrypt-prod",
|
||||||
|
"nginx.ingress.kubernetes.io/proxy-body-size": "0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spec+: {
|
||||||
|
tls: [ { hosts: [ cfg.domain ], secretName: "oodviewer-tls" } ],
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
host: cfg.domain,
|
||||||
|
http: {
|
||||||
|
paths: [
|
||||||
|
{ path: "/", backend: top.service.name_port },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
40
hswaw/oodviewer/secrets/cipher/postgres-pass
Normal file
40
hswaw/oodviewer/secrets/cipher/postgres-pass
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
-----BEGIN PGP MESSAGE-----
|
||||||
|
|
||||||
|
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
|
||||||
|
hdsApEHzDw==
|
||||||
|
=nmyr
|
||||||
|
-----END PGP MESSAGE-----
|
1
hswaw/oodviewer/secrets/plain/.gitignore
vendored
Normal file
1
hswaw/oodviewer/secrets/plain/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
*
|
20
hswaw/oodviewer/templates/BUILD.bazel
Normal file
20
hswaw/oodviewer/templates/BUILD.bazel
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||||
|
load("@io_bazel_rules_go//extras:embed_data.bzl", "go_embed_data")
|
||||||
|
|
||||||
|
go_embed_data(
|
||||||
|
name = "templates_data",
|
||||||
|
srcs = glob(["*.html"]),
|
||||||
|
package = "templates",
|
||||||
|
flatten = True,
|
||||||
|
)
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = [
|
||||||
|
":templates_data", # keep
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//hswaw/oodviewer:__pkg__",
|
||||||
|
],
|
||||||
|
importpath = "code.hackerspace.pl/hscloud/hswaw/oodviewer/templates",
|
||||||
|
)
|
8
hswaw/oodviewer/templates/base.html
Normal file
8
hswaw/oodviewer/templates/base.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>oodviewer</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{ template "body" . }}
|
||||||
|
</body>
|
||||||
|
</html>
|
8
hswaw/oodviewer/templates/term.html
Normal file
8
hswaw/oodviewer/templates/term.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{{ define "body" }}
|
||||||
|
<h1>Entries for {{ .Name }}</h1>
|
||||||
|
<ul>
|
||||||
|
{{ range .Entries }}
|
||||||
|
<li>{{ .Entry }} <i>(added by {{ .Author }} on {{ .Added }})</i></li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
8
hswaw/oodviewer/templates/terms.html
Normal file
8
hswaw/oodviewer/templates/terms.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{{ define "body" }}
|
||||||
|
<h1>Available terms:</h1>
|
||||||
|
<ul>
|
||||||
|
{{ range .Terms }}
|
||||||
|
<li><a href="{{ .URL }}">{{ .Name }}</a> ({{ .Count }} entries)</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
138
hswaw/oodviewer/views.go
Normal file
138
hswaw/oodviewer/views.go
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
|
||||||
|
"code.hackerspace.pl/hscloud/hswaw/oodviewer/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tplBase = template.Must(template.New("base").Parse(string(templates.Data["base.html"])))
|
||||||
|
tplTerm = template.Must(template.Must(tplBase.Clone()).Parse(string(templates.Data["term.html"])))
|
||||||
|
tplTerms = template.Must(template.Must(tplBase.Clone()).Parse(string(templates.Data["terms.html"])))
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleTermsJson returns a JSON list of all terms.
|
||||||
|
func (a *app) handleTermsJson(w http.ResponseWriter, r *http.Request) {
|
||||||
|
terms, err := a.getTerms(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("getTerms: %v", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprintf(w, "internal error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Target API from old oodviewer, even if it's terrible.
|
||||||
|
var res [][]interface{}
|
||||||
|
for _, term := range terms {
|
||||||
|
res = append(res, []interface{}{
|
||||||
|
term.Name, term.Entries,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTerms renders a HTML page containing all terms.
|
||||||
|
func (a *app) handleTerms(w http.ResponseWriter, r *http.Request) {
|
||||||
|
terms, err := a.getTerms(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("getTerms: %v", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprintf(w, "internal error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
termsData := make([]struct {
|
||||||
|
URL string
|
||||||
|
Name string
|
||||||
|
Count uint64
|
||||||
|
}, len(terms))
|
||||||
|
|
||||||
|
for i, term := range terms {
|
||||||
|
termsData[i].URL = url.QueryEscape(term.Name)
|
||||||
|
termsData[i].Name = term.Name
|
||||||
|
termsData[i].Count = term.Entries
|
||||||
|
}
|
||||||
|
|
||||||
|
tplTerms.Execute(w, map[string]interface{}{
|
||||||
|
"Terms": termsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTermJson returns a JSON list of all entries contained within a term.
|
||||||
|
func (a *app) handleTermJson(w http.ResponseWriter, r *http.Request) {
|
||||||
|
parts := strings.Split(r.URL.Path, "/")
|
||||||
|
name := parts[len(parts)-1]
|
||||||
|
|
||||||
|
entries, err := a.getEntries(r.Context(), name)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("getEntries: %v", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprintf(w, "internal error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRandomTermJson returns a JSON serialized randomly chosen entry from a
|
||||||
|
// given term.
|
||||||
|
func (a *app) handleRandomTermJson(w http.ResponseWriter, r *http.Request) {
|
||||||
|
parts := strings.Split(r.URL.Path, "/")
|
||||||
|
name := parts[len(parts)-1]
|
||||||
|
|
||||||
|
entries, err := a.getEntries(r.Context(), name)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("getEntries: %v", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprintf(w, "internal error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(entries) < 1 {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
fmt.Fprintf(w, "no such entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entry := entries[rand.Intn(len(entries))]
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTerm renders an HTML page of all entries contained within a term.
|
||||||
|
func (a *app) handleTerm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
parts := strings.Split(r.URL.Path, "/")
|
||||||
|
name := parts[len(parts)-1]
|
||||||
|
|
||||||
|
entries, err := a.getEntries(r.Context(), name)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("getEntries: %v", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprintf(w, "internal error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entriesData := make([]struct {
|
||||||
|
Entry string
|
||||||
|
Author string
|
||||||
|
Added string
|
||||||
|
}, len(entries))
|
||||||
|
for i, entry := range entries {
|
||||||
|
entriesData[i].Entry = entry.Entry
|
||||||
|
entriesData[i].Author = entry.Author
|
||||||
|
entriesData[i].Added = time.Unix(entry.Added, 0).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
tplTerm.Execute(w, map[string]interface{}{
|
||||||
|
"Name": name,
|
||||||
|
"Entries": entriesData,
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in a new issue