seperate room state from muxrpc handler

There is a new roomstate package now with a Manager which is shared between muxrpc and the http
handler(s).

Also adds a list of peers in the room to admin dashboard.
This commit is contained in:
Henry 2021-02-10 15:50:36 +01:00
parent a8ae13063f
commit beea19f93e
17 changed files with 376 additions and 159 deletions

View File

@ -204,6 +204,7 @@ func runroomsrv() error {
dashboardH, err := handlers.New(
nil,
repo.New(repoDir),
roomsrv.StateManager,
db.AuthWithSSB,
db.AuthFallback,
)

View File

@ -23,7 +23,7 @@ type connectWithOriginArg struct {
Origin refs.FeedRef `json:"origin"` // this should be clear from the shs session already
}
func (rs *roomState) connect(ctx context.Context, req *muxrpc.Request, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error {
func (h *handler) connect(ctx context.Context, req *muxrpc.Request, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error {
// unpack arguments
var args []connectArg
@ -38,27 +38,26 @@ func (rs *roomState) connect(ctx context.Context, req *muxrpc.Request, peerSrc *
arg := args[0]
// see if we have and endpoint for the target
rs.roomsMu.Lock()
edp, has := rs.rooms["lobby"][arg.Target.Ref()]
edp, has := h.state.Has(arg.Target)
if !has {
rs.roomsMu.Unlock()
return fmt.Errorf("no such endpoint")
}
// call connect on them
var argWorigin connectWithOriginArg
argWorigin.connectArg = arg
argWorigin.Origin = rs.self
argWorigin.Origin = h.self
targetSrc, targetSnk, err := edp.Duplex(ctx, muxrpc.TypeBinary, muxrpc.Method{"tunnel", "connect"}, argWorigin)
if err != nil {
delete(rs.rooms["lobby"], arg.Target.Ref())
rs.updater.Update(rs.rooms["lobby"].asList())
rs.roomsMu.Unlock()
h.state.Remove(arg.Target)
// TODO: the call could fail because of an error with the caller, too.
// if we remove the wrong one, tho others might get confused
// h.state.Remove(caller)
return fmt.Errorf("failed to init connect call with target: %w", err)
}
rs.roomsMu.Unlock()
// pipe data
var cpy muxrpcDuplexCopy

View File

@ -3,16 +3,15 @@
package server
import (
"context"
"net"
kitlog "github.com/go-kit/kit/log"
"go.cryptoscope.co/muxrpc/v2"
"go.cryptoscope.co/muxrpc/v2/typemux"
refs "go.mindeco.de/ssb-refs"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/broadcasts"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemuxrpc"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
refs "go.mindeco.de/ssb-refs"
)
const name = "tunnel"
@ -40,29 +39,23 @@ func (plugin) Authorize(net.Conn) bool { return true }
}
*/
func New(log kitlog.Logger, ctx context.Context, self refs.FeedRef) maybemuxrpc.Plugin {
func New(log kitlog.Logger, self refs.FeedRef, m *roomstate.Manager) maybemuxrpc.Plugin {
mux := typemux.New(log)
var rs = new(roomState)
rs.self = self
rs.logger = log
rs.updater, rs.broadcaster = broadcasts.NewRoomChanger()
rs.rooms = make(roomsStateMap)
var h = new(handler)
h.self = self
h.logger = log
h.state = m
go rs.stateTicker(ctx)
mux.RegisterAsync(append(method, "isRoom"), typemux.AsyncFunc(h.isRoom))
mux.RegisterAsync(append(method, "ping"), typemux.AsyncFunc(h.ping))
// so far just lobby (v1 rooms)
rs.rooms["lobby"] = make(roomStateMap)
mux.RegisterAsync(append(method, "announce"), typemux.AsyncFunc(h.announce))
mux.RegisterAsync(append(method, "leave"), typemux.AsyncFunc(h.leave))
mux.RegisterAsync(append(method, "isRoom"), typemux.AsyncFunc(rs.isRoom))
mux.RegisterAsync(append(method, "ping"), typemux.AsyncFunc(rs.ping))
mux.RegisterSource(append(method, "endpoints"), typemux.SourceFunc(h.endpoints))
mux.RegisterAsync(append(method, "announce"), typemux.AsyncFunc(rs.announce))
mux.RegisterAsync(append(method, "leave"), typemux.AsyncFunc(rs.leave))
mux.RegisterSource(append(method, "endpoints"), typemux.SourceFunc(rs.endpoints))
mux.RegisterDuplex(append(method, "connect"), typemux.DuplexFunc(rs.connect))
mux.RegisterDuplex(append(method, "connect"), typemux.DuplexFunc(h.connect))
return plugin{
h: &mux,

View File

@ -5,142 +5,76 @@ package server
import (
"context"
"encoding/json"
"sync"
"time"
refs "go.mindeco.de/ssb-refs"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
refs "go.mindeco.de/ssb-refs"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"go.cryptoscope.co/muxrpc/v2"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/broadcasts"
)
type roomState struct {
self refs.FeedRef
type handler struct {
logger kitlog.Logger
self refs.FeedRef
updater broadcasts.RoomChangeSink
broadcaster *broadcasts.RoomChangeBroadcast
roomsMu sync.Mutex
rooms roomsStateMap
state *roomstate.Manager
}
func (rs *roomState) stateTicker(ctx context.Context) {
tick := time.NewTicker(10 * time.Second)
for {
select {
case <-ctx.Done():
tick.Stop()
return
case <-tick.C:
}
rs.roomsMu.Lock()
for room, members := range rs.rooms {
level.Info(rs.logger).Log("room", room, "cnt", len(members))
for who := range members {
level.Info(rs.logger).Log("room", room, "feed", who[1:5])
}
}
rs.roomsMu.Unlock()
}
}
// layout is map[room-name]map[canonical feedref]client-handle
type roomsStateMap map[string]roomStateMap
// roomStateMap is a single room
type roomStateMap map[string]muxrpc.Endpoint
// copy map entries to list for broadcast update
func (rsm roomStateMap) asList() []string {
memberList := make([]string, 0, len(rsm))
for m := range rsm {
memberList = append(memberList, m)
}
return memberList
}
func (rs *roomState) isRoom(context.Context, *muxrpc.Request) (interface{}, error) {
level.Debug(rs.logger).Log("called", "isRoom")
func (h *handler) isRoom(context.Context, *muxrpc.Request) (interface{}, error) {
level.Debug(h.logger).Log("called", "isRoom")
return true, nil
}
func (rs *roomState) ping(context.Context, *muxrpc.Request) (interface{}, error) {
func (h *handler) ping(context.Context, *muxrpc.Request) (interface{}, error) {
now := time.Now().UnixNano() / 1000
level.Debug(rs.logger).Log("called", "ping")
level.Debug(h.logger).Log("called", "ping")
return now, nil
}
func (rs *roomState) announce(_ context.Context, req *muxrpc.Request) (interface{}, error) {
level.Debug(rs.logger).Log("called", "announce")
func (h *handler) announce(_ context.Context, req *muxrpc.Request) (interface{}, error) {
level.Debug(h.logger).Log("called", "announce")
ref, err := network.GetFeedRefFromAddr(req.RemoteAddr())
if err != nil {
return nil, err
}
rs.roomsMu.Lock()
// add ref to lobby
rs.rooms["lobby"][ref.Ref()] = req.Endpoint()
// update all the connected tunnel.endpoints calls
rs.updater.Update(rs.rooms["lobby"].asList())
rs.roomsMu.Unlock()
h.state.AddEndpoint(*ref, req.Endpoint())
return false, nil
}
func (rs *roomState) leave(_ context.Context, req *muxrpc.Request) (interface{}, error) {
func (h *handler) leave(_ context.Context, req *muxrpc.Request) (interface{}, error) {
ref, err := network.GetFeedRefFromAddr(req.RemoteAddr())
if err != nil {
return nil, err
}
rs.roomsMu.Lock()
// remove ref from lobby
delete(rs.rooms["lobby"], ref.Ref())
// update all the connected tunnel.endpoints calls
rs.updater.Update(rs.rooms["lobby"].asList())
rs.roomsMu.Unlock()
h.state.Remove(*ref)
return false, nil
}
func (rs *roomState) endpoints(_ context.Context, req *muxrpc.Request, snk *muxrpc.ByteSink) error {
level.Debug(rs.logger).Log("called", "endpoints")
func (h *handler) endpoints(_ context.Context, req *muxrpc.Request, snk *muxrpc.ByteSink) error {
level.Debug(h.logger).Log("called", "endpoints")
toPeer := newForwarder(snk)
// for future updates
rs.broadcaster.Register(toPeer)
h.state.Register(toPeer)
ref, err := network.GetFeedRefFromAddr(req.RemoteAddr())
if err != nil {
return err
}
rs.roomsMu.Lock()
lobby := rs.rooms["lobby"]
// if the peer didn't call tunnel.announce()
if _, has := lobby[ref.Ref()]; has {
has := h.state.AlreadyAdded(*ref, req.Endpoint())
if !has {
// just send the current state to the new peer
toPeer.Update(lobby.asList())
} else {
// register them as if they did
lobby[ref.Ref()] = req.Endpoint()
rs.rooms["lobby"] = lobby
// update everyone
rs.updater.Update(lobby.asList())
toPeer.Update(h.state.List())
}
// send the current state
rs.roomsMu.Unlock()
return nil
}

View File

@ -14,11 +14,11 @@ import (
"github.com/keks/nocomment"
"go.cryptoscope.co/muxrpc/v2"
refs "go.mindeco.de/ssb-refs"
"github.com/ssb-ngi-pointer/go-ssb-room/handlers/tunnel/server"
"github.com/ssb-ngi-pointer/go-ssb-room/handlers/whoami"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemuxrpc"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
refs "go.mindeco.de/ssb-refs"
)
func (s *Server) initNetwork() error {
@ -82,7 +82,11 @@ func (s *Server) initNetwork() error {
// s.master.Register(replicate.NewPlug(s.Users))
tunnelPlug := server.New(kitlog.With(s.logger, "unit", "tunnel"), s.rootCtx, s.Whoami())
tunnelPlug := server.New(
kitlog.With(s.logger, "unit", "tunnel"),
s.Whoami(),
s.StateManager,
)
s.public.Register(tunnelPlug)
// tcp+shs

View File

@ -12,16 +12,18 @@ import (
"path/filepath"
"sync"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"go.cryptoscope.co/netwrap"
refs "go.mindeco.de/ssb-refs"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/keys"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/multicloser"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemuxrpc"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
refs "go.mindeco.de/ssb-refs"
)
type Server struct {
@ -55,6 +57,8 @@ type Server struct {
master maybemuxrpc.PluginManager
authorizer listAuthorizer
StateManager *roomstate.Manager
}
func (s Server) Whoami() refs.FeedRef {
@ -119,6 +123,8 @@ func New(opts ...Option) (*Server, error) {
}
}
s.StateManager = roomstate.NewManager(s.rootCtx, s.logger)
if err := s.initNetwork(); err != nil {
return nil, err
}

134
roomstate/roomstate.go Normal file
View File

@ -0,0 +1,134 @@
package roomstate
import (
"context"
"sync"
"time"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"go.cryptoscope.co/muxrpc/v2"
refs "go.mindeco.de/ssb-refs"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/broadcasts"
)
type Manager struct {
logger kitlog.Logger
updater broadcasts.RoomChangeSink
broadcaster *broadcasts.RoomChangeBroadcast
roomMu sync.Mutex
room roomStateMap
}
func NewManager(ctx context.Context, log kitlog.Logger) *Manager {
var m Manager
m.updater, m.broadcaster = broadcasts.NewRoomChanger()
m.room = make(roomStateMap)
go m.stateTicker(ctx)
return &m
}
func (m *Manager) stateTicker(ctx context.Context) {
tick := time.NewTicker(10 * time.Second)
last := 0
for {
select {
case <-ctx.Done():
tick.Stop()
return
case <-tick.C:
}
m.roomMu.Lock()
cnt := len(m.room)
if cnt == last {
continue
}
last = cnt
level.Info(m.logger).Log("room-cnt", cnt)
for who := range m.room {
level.Info(m.logger).Log("feed", who[1:5])
}
m.roomMu.Unlock()
}
}
// roomStateMap is a single room
type roomStateMap map[string]muxrpc.Endpoint
// copy map entries to list for broadcast update
func (rsm roomStateMap) AsList() []string {
memberList := make([]string, 0, len(rsm))
for m := range rsm {
memberList = append(memberList, m)
}
return memberList
}
// Register listens to changes to the room
func (m *Manager) Register(sink broadcasts.RoomChangeSink) {
m.broadcaster.Register(sink)
}
// List just returns a list of feed references
func (m *Manager) List() []string {
m.roomMu.Lock()
defer m.roomMu.Unlock()
return m.room.AsList()
}
// AddEndpoint adds the endpoint to the room
func (m *Manager) AddEndpoint(who refs.FeedRef, edp muxrpc.Endpoint) {
m.roomMu.Lock()
// add ref to to the room map
m.room[who.Ref()] = edp
// update all the connected tunnel.endpoints calls
m.updater.Update(m.room.AsList())
m.roomMu.Unlock()
}
// Remove removes the peer from the room
func (m *Manager) Remove(who refs.FeedRef) {
m.roomMu.Lock()
// remove ref from lobby
delete(m.room, who.Ref())
// update all the connected tunnel.endpoints calls
m.updater.Update(m.room.AsList())
m.roomMu.Unlock()
}
// AlreadyAdded returns true if the peer was already added to the room
func (m *Manager) AlreadyAdded(who refs.FeedRef, edp muxrpc.Endpoint) bool {
m.roomMu.Lock()
// if the peer didn't call tunnel.announce()
_, has := m.room[who.Ref()]
if !has {
// register them as if they didnt
m.room[who.Ref()] = edp
// update everyone
m.updater.Update(m.room.AsList())
}
// send the current state
m.roomMu.Unlock()
return has
}
// Has returns true and the endpoint if the peer is in the room
func (m *Manager) Has(who refs.FeedRef) (muxrpc.Endpoint, bool) {
m.roomMu.Lock()
// add ref to to the room map
edp, has := m.room[who.Ref()]
m.roomMu.Unlock()
return edp, has
}

File diff suppressed because one or more lines are too long

View File

@ -8,6 +8,7 @@ import (
"github.com/gorilla/mux"
"go.mindeco.de/http/render"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
)
@ -15,18 +16,18 @@ var HTMLTemplates = []string{
"/admin/dashboard.tmpl",
}
func Handler(m *mux.Router, r *render.Renderer) http.Handler {
func Handler(m *mux.Router, r *render.Renderer, roomState *roomstate.Manager) http.Handler {
if m == nil {
m = router.Admin(nil)
}
m.Get(router.AdminDashboard).HandlerFunc(r.HTML("/admin/dashboard.tmpl", dashboard))
m.Get(router.AdminDashboard).HandlerFunc(r.HTML("/admin/dashboard.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
lst := roomState.List()
return struct {
Clients []string
Count int
}{lst, len(lst)}, nil
}))
return m
}
func dashboard(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
return struct {
Name string
}{"test"}, nil
}

View File

@ -14,6 +14,7 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
"github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/admin"
roomsAuth "github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/auth"
@ -26,6 +27,7 @@ import (
func New(
m *mux.Router,
repo repo.Interface,
roomState *roomstate.Manager,
as admindb.AuthWithSSBService,
fs admindb.AuthFallbackService,
) (http.Handler, error) {
@ -53,8 +55,11 @@ func New(
admin.HTMLTemplates,
)...),
render.FuncMap(web.TemplateFuncs(m)),
// TODO: add plural and template data variants
// TODO: move these to the i18n helper pkg
render.InjectTemplateFunc("i18npl", func(r *http.Request) interface{} {
loc := localizerFromRequest(locHelper, r)
return loc.LocalizePlurals
}),
render.InjectTemplateFunc("i18n", func(r *http.Request) interface{} {
loc := localizerFromRequest(locHelper, r)
return loc.LocalizeSimple
@ -143,7 +148,7 @@ func New(
adminRouter.Use(a.Authenticate)
// we dont strip path here because it somehow fucks with the middleware setup
adminRouter.PathPrefix("/").Handler(admin.Handler(adminRouter, r))
adminRouter.PathPrefix("/").Handler(admin.Handler(adminRouter, r, roomState))
m.PathPrefix("/news").Handler(http.StripPrefix("/news", news.Handler(m, r)))

View File

@ -3,12 +3,16 @@
package handlers
import (
"bytes"
"net/http"
"net/http/cookiejar"
"net/url"
"testing"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
refs "go.mindeco.de/ssb-refs"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
)
@ -100,6 +104,86 @@ func TestLoginForm(t *testing.T) {
})
}
func TestFallbackAuth(t *testing.T) {
setup(t)
t.Cleanup(teardown)
a, r := assert.New(t), require.New(t)
loginVals := url.Values{
"user": []string{"test"},
"pass": []string{"test"},
}
testAuthFallbackDB.CheckReturns(int64(23), nil)
url, err := testRouter.Get(router.AuthFallbackSignInForm).URL()
r.Nil(err)
url.Host = "localhost"
url.Scheme = "http"
resp := testClient.PostForm(url.String(), loginVals)
a.Equal(http.StatusSeeOther, resp.Code, "wrong HTTP status code")
a.Equal(1, testAuthFallbackDB.CheckCallCount())
// very cheap client session
jar, err := cookiejar.New(nil)
r.NoError(err)
c := resp.Result().Cookies()
jar.SetCookies(url, c)
var h = http.Header(map[string][]string{})
dashboardURL, err := testRouter.Get(router.AdminDashboard).URL()
r.Nil(err)
dashboardURL.Host = "localhost"
dashboardURL.Scheme = "http"
cs := jar.Cookies(dashboardURL)
r.Len(cs, 1, "expecting one cookie!")
theCookie := cs[0].String()
a.NotEqual("", theCookie, "should have a new cookie")
h.Set("Cookie", theCookie)
html, resp := testClient.GetHTML(dashboardURL.String(), &h)
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") {
t.Log(html.Find("body").Text())
}
assertLocalized(t, html, []localizedElement{
{"#welcome", "AdminDashboardWelcome"},
{"title", "AdminDashboardTitle"},
})
testRef := refs.FeedRef{Algo: "test", ID: bytes.Repeat([]byte{0}, 16)}
testRoomState.AddEndpoint(testRef, nil)
html, resp = testClient.GetHTML(dashboardURL.String(), &h)
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") {
t.Log(html.Find("body").Text())
}
assertLocalized(t, html, []localizedElement{
{"#welcome", "AdminDashboardWelcome"},
{"title", "AdminDashboardTitle"},
{"#roomCount", "AdminRoomCountSingular"},
})
testRef2 := refs.FeedRef{Algo: "test", ID: bytes.Repeat([]byte{1}, 16)}
testRoomState.AddEndpoint(testRef2, nil)
html, resp = testClient.GetHTML(dashboardURL.String(), &h)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
t.Log(html.Find("body").Text())
assertLocalized(t, html, []localizedElement{
{"#welcome", "AdminDashboardWelcome"},
{"title", "AdminDashboardTitle"},
{"#roomCount", "AdminRoomCountPlural"},
})
}
// utils
type localizedElement struct {

View File

@ -4,6 +4,7 @@ package handlers
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
@ -11,9 +12,11 @@ import (
"path/filepath"
"testing"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
"github.com/BurntSushi/toml"
"github.com/pkg/errors"
"go.mindeco.de/http/tester"
"go.mindeco.de/logging/logtest"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb/mockdb"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
@ -30,6 +33,8 @@ var (
testAuthDB *mockdb.FakeAuthWithSSBService
testAuthFallbackDB *mockdb.FakeAuthFallbackService
testRoomState *roomstate.Manager
testI18N = justTheKeys()
)
@ -48,18 +53,22 @@ func setup(t *testing.T) {
testAuthDB = new(mockdb.FakeAuthWithSSBService)
testAuthFallbackDB = new(mockdb.FakeAuthFallbackService)
log, _ := logtest.KitLogger("complete", t)
ctx := context.TODO()
testRoomState = roomstate.NewManager(ctx, log)
h, err := New(
testRouter,
testRepo,
testRoomState,
testAuthDB,
testAuthFallbackDB,
)
if err != nil {
t.Fatal(errors.Wrap(err, "setup: handler init failed"))
t.Fatal("setup: handler init failed:", err)
}
// log, _ := logtest.KitLogger("complete", t)
testMux = http.NewServeMux()
testMux.Handle("/", h)
testClient = tester.New(testMux, t)
@ -70,9 +79,13 @@ func teardown() {
testClient = nil
testAuthDB = nil
testAuthFallbackDB = nil
testRoomState = nil
}
// auto generate from defaults a list of Label = "Label"
// must keep order of input intact
// (at least all the globals before starting with nested plurals)
// also replaces 'one' and 'other' in plurals
func justTheKeys() []byte {
f, err := i18n.Defaults.Open("/active.en.toml")
if err != nil {
@ -86,8 +99,36 @@ func justTheKeys() []byte {
var buf = &bytes.Buffer{}
for _, key := range md.Keys() {
fmt.Fprintf(buf, "%s = \"%s\"\n", key, key)
// if we don't produce the same order as the input
// (in go maps are ALWAYS random access when ranged over)
// nested keys (such as plural form) will mess up the global level...
for _, k := range md.Keys() {
key := k.String()
val, has := justAMap[key]
if !has {
// fmt.Println("i18n test warning:", key, "not unmarshaled")
continue
}
switch tv := val.(type) {
case string:
fmt.Fprintf(buf, "%s = \"%s\"\n", key, key)
case map[string]interface{}:
// fmt.Println("i18n test warning: custom map for ", key)
fmt.Fprintf(buf, "\n[%s]\n", key)
// replace "one" and "other" keys
// with Label and LabelPlural
tv["one"] = key + "Singular"
tv["other"] = key + "Plural"
toml.NewEncoder(buf).Encode(tv)
fmt.Fprintln(buf)
default:
panic(fmt.Sprintf("unhandled toml structure under %s: %T\n", key, val))
}
}
return buf.Bytes()

View File

@ -1,9 +1,16 @@
LandingTitle = "ohai my room"
LandingWelcome = "Landing welcome here"
NewsWelcome = "so, what happend (recently)"
NewsTitle = "News"
NewsOverview = "News - Overview"
AuthFallbackWelcome = "You really shouldn't be here.... Let's get you through this."
AuthFallbackTitle = "The place of last resort"
AuthSignIn = "Sign in"
AuthSignOut = "Sign out"
NewsWelcome = "so, what happend (recently)"
NewsTitle = "News"
NewsOverview = "News - Overview"
AdminDashboardWelcome = "Welcome to your dashboard"
AdminDashboardTitle = "Room Dashboard"
[AdminRoomCount]
description = "The number of people in a room"
one = "Ther is one person in the Room"
other = "There are {{.Count}} people in the Room"

View File

@ -21,14 +21,14 @@ var Defaults = func() http.FileSystem {
fs := vfsgen۰FS{
"/": &vfsgen۰DirInfo{
name: "/",
modTime: time.Date(2021, 2, 10, 12, 9, 47, 401093107, time.UTC),
modTime: time.Date(2021, 2, 10, 16, 34, 42, 221227039, time.UTC),
},
"/active.en.toml": &vfsgen۰CompressedFileInfo{
name: "active.en.toml",
modTime: time.Date(2021, 2, 10, 13, 36, 16, 98174331, time.UTC),
uncompressedSize: 345,
modTime: time.Date(2021, 2, 10, 18, 20, 44, 393607246, time.UTC),
uncompressedSize: 591,
compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\x54\x8e\xc1\x4e\xc3\x30\x0c\x86\xef\x7b\x8a\x5f\xb9\x0c\x24\xc8\x1b\x70\xe0\x82\x84\x34\xb1\x03\x93\x10\xc7\xac\x35\x75\x84\x1b\x57\x89\xb3\xaa\x6f\x8f\x4a\xbb\x32\x6e\xf6\xf7\x7f\xb6\xfe\x43\x48\x6d\x4c\xdd\x29\x9a\x10\x9e\xe0\x94\x43\x44\x3f\x21\xab\xf6\x6e\xb7\xa6\x1f\x24\x8d\xf6\xbf\xf9\x4a\x30\xae\x88\x29\x93\xdb\x3d\x57\xe3\x97\x20\x72\x0e\xcd\xf7\x8d\xfc\xa9\x15\x99\x82\xc8\x84\xc2\x5a\xa5\x4d\x7b\xc3\x79\x39\xf2\xde\x7b\x1c\xc8\xf6\x05\x1d\x19\x26\xad\x30\xce\x5a\x3b\x86\x71\x2c\xfe\xff\xd3\xad\xdf\x89\x09\x83\x84\x86\xa0\x5f\x90\x50\x0c\x99\x8a\x66\x5b\xf4\xf7\xd8\xa5\xd7\x34\x7b\xf3\x84\x98\xfe\xf0\xb1\xda\xc6\xb5\x9a\xdb\xbd\xd1\x58\x6e\xba\x16\x7d\xc0\xc8\xc1\xc0\x61\x18\x28\xb5\xb8\xcb\xd4\x50\x32\x99\xee\x17\x77\xab\x30\x2f\x0b\x3a\x5e\x28\x5f\x22\x8d\x57\x8a\x47\x5c\x91\xfb\x09\x00\x00\xff\xff\x5e\xfa\x34\x19\x59\x01\x00\x00"),
compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\x5c\x91\xcd\x6a\xe3\x30\x14\x85\xf7\x7e\x8a\x83\x37\x99\x81\x19\xbf\x41\x16\xc3\x84\x42\x21\x34\x90\x06\x4a\x29\x5d\xc8\xf6\xad\x25\x2a\xeb\x1a\xe9\x2a\xc6\x84\xbc\x7b\x51\xfc\x13\xb7\x3b\xe9\xd3\x27\xdd\x73\xd0\x5e\xb9\xda\xb8\xe6\x64\xc4\x12\xb6\xc8\x59\x2b\x83\x76\x80\x67\x6e\xf3\x6c\x3a\x7d\x21\x5b\x71\x7b\x3b\x9f\x08\xfa\x09\x69\xf2\x94\x67\x4f\xd4\x87\x95\x14\xf8\x0f\x7a\xad\x04\x5a\x75\x1d\xb9\x1a\xbf\x3c\x55\xe4\xc4\x0e\xbf\x47\x77\x19\x97\x36\x23\x3a\x9c\xc9\x9f\x0d\xf5\x33\xc5\x5f\xcc\x28\xcf\xfe\x45\xd1\x0f\xca\xda\x52\x55\x9f\xab\x39\xaf\x1c\xe1\x49\x59\x3b\x20\x68\x8e\xb6\x76\x1b\x41\x39\x86\x2a\x8a\xa2\xc0\x9e\x64\x13\xd0\x90\x60\xe0\x08\xd1\x9e\x63\xa3\x21\xda\x84\xe2\xfb\xa3\x4b\xa0\x93\x26\x74\x56\x55\x04\xfe\x80\x55\x41\xe0\x29\xb0\x97\x51\x7f\x36\x8d\x7b\x74\xc9\x4b\x2b\x18\x77\xc7\x87\x28\x0b\xe7\x98\xfc\xba\x35\x6e\xa7\x82\x2e\x59\xf9\x7a\x95\x7a\x5e\x0a\xa7\x54\x1e\xf5\xec\xfc\xbc\xb3\x84\x3a\x32\xb7\xd8\xdd\xb5\xec\xed\x26\x26\xfc\x9f\xa3\x93\xf7\xac\xa6\x50\x79\xd3\x89\x61\x37\xb7\x70\xb1\x2d\xc9\xa7\x1a\x1d\x71\x67\x09\xc6\x41\x4d\xff\xca\x6e\x2e\xeb\x61\x02\xd2\xb6\x23\x1f\x38\x55\x82\x68\xc2\x71\xd4\x24\x09\x5b\x8c\x26\x41\x79\xc2\xe5\x52\xdc\x66\x5e\xaf\xab\x77\x97\x2b\x5f\x01\x00\x00\xff\xff\x18\xb8\x47\x4c\x4f\x02\x00\x00"),
},
}
fs["/"].(*vfsgen۰DirInfo).entries = []os.FileInfo{

View File

@ -119,6 +119,9 @@ func (l Localizer) LocalizePlurals(messageID string, pluralCount int) string {
msg, err := l.loc.Localize(&i18n.LocalizeConfig{
MessageID: messageID,
PluralCount: pluralCount,
TemplateData: map[string]int{
"Count": pluralCount,
},
})
if err == nil {
return msg

View File

@ -1,11 +1,16 @@
{{ define "title" }}{{i18n "AdminDashboard"}}{{ end }}
{{ define "title" }}{{i18n "AdminDashboardTitle"}}{{ end }}
{{ define "content" }}
<div class="page-header">
<h1 id="welcome">{{i18n "AdminWelcome"}}</h1>
<h1 id="welcome">{{i18n "AdminDashboardWelcome"}}</h1>
</div>
<div class="row">
<div class="col-md-12">
<p>super cool dashboard!</p>
<p id="roomCount">{{i18npl "AdminRoomCount" .Count}}</p>
<ul>
{{range .Clients}}
<li>{{.}}</li>
{{end}}
</ul>
</div>
</div> <!-- /row -->
{{end}}

File diff suppressed because one or more lines are too long