Compare commits

..

8 Commits

Author SHA1 Message Date
efim
ea85460c0a fix: remove all marks when noone speaks next
if room comes to a state where last person stopped speaking,
then setting room to clean state is logical,
next person to start speaking is the only reference point
2023-12-03 14:39:40 +00:00
efim
acd9f4fc62 feat: custom gesture & room metrics 2023-12-02 11:30:56 +00:00
efim
42c73c5902 feat: prometheus nixos module & test-container
for testing grafana in container
export QEMU_NET_OPTS="hostfwd=tcp::3000-:3000"
 nixos-rebuild build-vm --flake .#test-container
and then run the container, login is test/test

and the grafana port is forwarded to host machine, yay
2023-12-02 08:19:13 +00:00
efim
0a8db09fe8 feat: reporting basic metrics to prometheus 2023-12-02 06:54:11 +00:00
efim
5cab5d88d9 feat: simplest room expiration 2023-12-01 04:14:23 +00:00
efim
a502ee72a0 fix: request context to rooms manager 2023-12-01 04:13:10 +00:00
efim
cc33c3f742 feat: expire & prolong sessions 2023-12-01 04:09:17 +00:00
efim
e0bd77fe3b fix: page names 2023-11-30 04:17:54 +00:00
16 changed files with 1436 additions and 50 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
/result
/routes/static/out.css
/nixos.qcow2
/data/

File diff suppressed because it is too large Load Diff

7
dev-conf/prometheus.yml Normal file
View File

@@ -0,0 +1,7 @@
global:
scrape_interval: 15s
scrape_configs:
- job_name: go
static_configs:
- targets: ['localhost:8081']

View File

@@ -22,17 +22,20 @@
pkgs.nodePackages.prettier
pkgs.gnumake
pkgs.redis
pkgs.prometheus
pkgs.grafana
];
shellHook = ''
export GOPATH=$PWD/.go
export PATH=$GOPATH/bin:$PATH
export GRAPHANA_PATH=${pkgs.grafana}
'';
};
packages = rec {
some-automoderation = pkgs.buildGoModule {
inherit pname version;
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
vendorHash = "sha256-zc4n5UxsmW8Nt52kS57i1W61Gy/J8T0RJPlwJnYJjHI=";
vendorHash = "sha256-ID0WG0pa9DkXTJ7aB9VywGO3R85FWkpXaaIuugnG6mg=";
preBuild = ''
${pkgs.nodePackages.tailwindcss}/bin/tailwindcss -i routes/in.css -o routes/static/out.css
@@ -54,7 +57,7 @@
'';
};
networking.firewall.enable = false;
users.groups.test = {};
users.groups.test = { };
users.mutableUsers = false;
users.users.test = {
isNormalUser = true;
@@ -67,15 +70,33 @@
host = "some-automoderation.sunshine.industries";
useNginx = false;
port = 9090;
metricsPort = 9091;
redisPort = 9999;
enablePrometheus = true;
};
services.prometheus = {
enable = true;
port = 9998;
# scrape config will be set up by the module
};
services.grafana = {
enable = true;
settings.server.http_port = 3000;
settings.server.http_addr = "0.0.0.0";
provision.datasources = {
settings.datasources = [{
name = "local-prometheus";
type = "prometheus";
url = "http://localhost:9998";
}];
};
};
})
];
};
};
nixosModules.some-automoderation-module = { lib, pkgs, config, ... }:
let
cfg = config.services.${pname};
let cfg = config.services.${pname};
in {
options.services.${pname} = {
enable =
@@ -88,8 +109,7 @@
useNginx = lib.mkOption {
type = lib.types.bool;
default = true;
description =
"Whether to set up nginx reverse proxy";
description = "Whether to set up nginx reverse proxy";
};
port = lib.mkOption {
type = lib.types.int;
@@ -102,6 +122,16 @@
default = 7777;
description = "Port on which to connect to redis database.";
};
metricsPort = lib.mkOption {
type = lib.types.int;
default = 8091;
description = "Port on which server exposes metrics.";
};
enablePrometheus = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to add scrape rules to prometheus";
};
useHostTls = lib.mkOption {
type = lib.types.bool;
default = false;
@@ -109,12 +139,10 @@
"Whether virtual host should enable NixOS ACME certs";
};
};
config =
let
config = let
username = "${pname}";
groupname = "${pname}";
in
lib.mkIf cfg.enable {
in lib.mkIf cfg.enable {
users.groups."${groupname}" = { };
users.users."${username}" = {
isNormalUser = true; # needed to allow for home dir
@@ -128,8 +156,9 @@
startLimitBurst = 10;
serviceConfig = {
ExecStart = let
serveCliArg =
"--port ${toString cfg.port} --redisPort ${toString cfg.redisPort}";
serveCliArg = "--port ${toString cfg.port} --redisPort ${
toString cfg.redisPort
} --metricsPort ${toString cfg.metricsPort}";
in "${packages.some-automoderation}/bin/${pname} ${serveCliArg}";
Restart = "on-failure";
User = "${username}";
@@ -154,10 +183,15 @@
enable = true;
user = "${username}";
port = cfg.redisPort;
settings = {
notify-keyspace-events = "KEA";
}
;
settings = { notify-keyspace-events = "KEA"; };
};
services.prometheus = lib.mkIf cfg.enablePrometheus {
scrapeConfigs = [{
job_name = "some-automoderation";
static_configs = [{
targets = [ "localhost:${toString cfg.metricsPort}" ];
}];
}];
};
};
};

11
go.mod
View File

@@ -4,13 +4,22 @@ go 1.20
require (
github.com/kr/pretty v0.3.1
github.com/prometheus/client_golang v1.17.0
github.com/redis/go-redis/v9 v9.2.1
golang.org/x/crypto v0.15.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
golang.org/x/sys v0.14.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)

30
go.sum
View File

@@ -1,18 +1,46 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

27
main.go
View File

@@ -7,8 +7,10 @@ import (
"log"
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/redis/go-redis/v9"
"sunshine.industries/some-automoderation/metrics"
"sunshine.industries/some-automoderation/rooms"
"sunshine.industries/some-automoderation/routes"
"sunshine.industries/some-automoderation/sessions"
@@ -19,21 +21,34 @@ var ctx = context.Background()
func main() {
var port int
flag.IntVar(&port, "port", 8080, "Port on which the server should start")
var metricsPort int
flag.IntVar(&metricsPort, "metricsPort", 8081, "Port on which the server expose metrics")
var redisPort int
flag.IntVar(&redisPort, "redisPort", 7777, "Port on which server should connect to redis db")
flag.Parse()
metrics := metrics.SetupMetrics()
promhttp.Handler()
promHandler := promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{})
promMux := http.NewServeMux()
promMux.Handle("/metrics", promHandler)
go func() {
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", metricsPort), promMux))
}()
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("localhost:%d", redisPort),
Addr: fmt.Sprintf("localhost:%d", redisPort),
Password: "",
DB: 0,
DB: 0,
})
roomsM := rooms.RedisRM { Rdb: rdb, }
sessions := sessions.RedisSM{ Rdb: rdb, }
roomsM := rooms.RedisRM{Rdb: rdb}
sessions := sessions.RedisSM{Rdb: rdb}
log.Printf("Server will start on port: %d; listening to redis on: %d\n", port, redisPort)
routes.RegisterRoutes(sessions, roomsM)
log.Printf("Server will start on port: %d; /metrics on %d; listening to redis on: %d\n", port, metricsPort, redisPort)
routes.RegisterRoutes(sessions, roomsM, &metrics)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
}

43
metrics/metrics.go Normal file
View File

@@ -0,0 +1,43 @@
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
)
type MetricsContainer struct {
Registry *prometheus.Registry
LiveConnectionsGauge prometheus.Gauge
RaiseGestureCounter *prometheus.CounterVec
SpeakerCounter *prometheus.CounterVec
}
const GestureNameLabel string = "gestureString"
func SetupMetrics() MetricsContainer {
registry := prometheus.NewRegistry()
liveConnectionsGauge := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "live_connections_total",
Help: "Total amount of live SSE subscriptions to room state",
})
raiseGestureCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "raise_gesture_total",
Help: "Total amount of raised hands",
}, []string{GestureNameLabel})
speakerCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "speaker_total",
Help: "Total amount of speaker turns",
}, []string{GestureNameLabel})
registry.MustRegister(liveConnectionsGauge, raiseGestureCounter, speakerCounter,
collectors.NewGoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
)
return MetricsContainer{
Registry: registry,
LiveConnectionsGauge: liveConnectionsGauge,
RaiseGestureCounter: raiseGestureCounter,
SpeakerCounter: speakerCounter,
}
}

View File

@@ -53,6 +53,7 @@ func (r *Room) RaiseHand(p PersonId, gesture HandGesture) Room {
// - we should find next speaker
// - assign room.CurrentSpeaker to next speaker or to PersonId(0) to indicate that noone is speaking
// - if next speaker has gesture of higher priority - set Mark to current speaker for their gesture level
// when there is not next speaker, remove all marks
func (r *Room) ReleaseHand(p PersonId) {
// releasing a hand of a current speaker should result in selection of a new speaker
log.Printf("about to release hand of %d in %+v", p, r)
@@ -76,6 +77,7 @@ func (r *Room) ReleaseHand(p PersonId) {
if !nextSpeakerFound {
log.Printf("there is not next speaker, that's ok")
r.CurrentSpeaker = PersonId(0)
r.Marks = make(map[HandGesture]PersonId)
} else {
// searching for the next speaker
currentSpeakerGesture := handReleaseGesture

View File

@@ -20,6 +20,7 @@ var releaseHandTests = []releaseHandTest{
releasingNonSpeakerHand,
releaseToPersonWithHandAndMark,
raisingLevelSetMarksWithoutOverridingExisting,
releaseAllMarksWhenNoSpeaker,
}
func TestRoomReleaseHand(t *testing.T) {
@@ -314,3 +315,40 @@ var releaseToPersonWithHandAndMark releaseHandTest = releaseHandTest{
Marks: map[HandGesture]PersonId{},
},
}
// there is a mark for Expand on person 2, mark for Change Topic on person 3
// speaker is person 1 with Expand
// after hand release person nobody should be speaking, with all marks removed
var releaseAllMarksWhenNoSpeaker releaseHandTest = releaseHandTest{
testName: "releaseAllMarksWhenNoSpeaker",
room: Room{
Name: "test",
CurrentSpeaker: person1.Id,
Paricipants: []PersonId{
person1.Id,
person2.Id,
person3.Id,
person4.Id,
},
ParticipantHands: map[PersonId]HandGesture{
person1.Id: Expand,
},
Marks: map[HandGesture]PersonId{
Expand: person2.Id,
ChangeTopic: person3.Id,
},
},
releasingParticipantId: person1.Id,
expected: Room{
Name: "test",
CurrentSpeaker: PersonId(0),
Paricipants: []PersonId{
person1.Id,
person2.Id,
person3.Id,
person4.Id,
},
ParticipantHands: map[PersonId]HandGesture{},
Marks: map[HandGesture]PersonId{},
},
}

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"log"
"math/rand"
"time"
"github.com/redis/go-redis/v9"
)
@@ -50,13 +51,14 @@ func (r *Room) UnmarshalBinary(data []byte) error {
// let's check whether it will be possible to save nested structs
type RoomManager interface {
Get(roomName string) (Room, bool, error)
Save(room Room) error
Get(ctx context.Context, roomName string) (Room, bool, error)
Save(ctx context.Context, room Room) error
Update(ctx context.Context, roomName string, f func(fromRoom Room) (toRoom Room)) error
Subscribe(ctx context.Context, roomName string) <-chan Room
}
const roomRedisPrefix = "room"
const roomTtl = 24 * time.Hour
func roomNameToRedisId(roomName string) string {
return fmt.Sprintf("%s:%s", roomRedisPrefix, roomName)
@@ -66,9 +68,9 @@ type RedisRM struct {
Rdb *redis.Client
}
func (redisRM RedisRM) Get(roomName string) (Room, bool, error) {
func (redisRM RedisRM) Get(ctx context.Context, roomName string) (Room, bool, error) {
var readRoom Room
err := redisRM.Rdb.Get(context.TODO(), roomNameToRedisId(roomName)).Scan(&readRoom)
err := redisRM.Rdb.Get(ctx, roomNameToRedisId(roomName)).Scan(&readRoom)
if err == redis.Nil {
return Room{}, false, nil
}
@@ -155,7 +157,7 @@ func (redisRM RedisRM) Update(ctx context.Context, roomName string, f func(fromR
_, err = tx.Pipelined(ctx, func(pipe redis.Pipeliner) error {
log.Printf(">> about to Set %s to %v", roomName, room)
pipe.Set(ctx, roomKey, &room, 0)
pipe.Set(ctx, roomKey, &room, roomTtl)
return nil
})
@@ -177,7 +179,7 @@ func (redisRM RedisRM) Update(ctx context.Context, roomName string, f func(fromR
return errors.New("update reached maximum amount of retries")
}
func (redisRM RedisRM) Save(room Room) error {
err := redisRM.Rdb.Set(context.TODO(), roomNameToRedisId(room.Name), &room, 0).Err() // maybe even set expiration?
func (redisRM RedisRM) Save(ctx context.Context, room Room) error {
err := redisRM.Rdb.Set(ctx, roomNameToRedisId(room.Name), &room, roomTtl).Err()
return err
}

View File

@@ -34,6 +34,7 @@ func indexPageRoute(
) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, err := getRequestSession(r, sessionSM)
log.Printf("/ the session i got is %+v", session)
if err != nil {
log.Printf("/ session not found, means should render the login section %s", err)
// TODO return error i guess
@@ -53,7 +54,7 @@ func indexPageRoute(
data := pageData{
Base: baseData{
Title: "hello base template title",
Title: "Some Automoderation: simple automation",
},
Header: headerData{
Title: session.RoomId,

View File

@@ -71,7 +71,7 @@ func getRequestSession(r *http.Request,
if err != nil {
return sessions.SessionData{}, ErrAuthCookieValueInvalid
}
session := sessionsM.Get(sessionId)
session := sessionsM.Get(r.Context(), sessionId)
if session == (sessions.SessionData{}) {
return sessions.SessionData{}, ErrAuthSessionNotFound
}
@@ -115,7 +115,7 @@ func renderLoginPage(w http.ResponseWriter, roomName string, isRoomExisting bool
title := "Some Automoderation: Join room or create one"
if roomName != "" {
title = fmt.Sprintf("Some Automoderation: create or join '%s' room", roomName)
title = fmt.Sprintf("Some Automoderation: join room '%s'", roomName)
}
data := pageData{
Base: baseData{
@@ -146,7 +146,7 @@ func createRoomHandler(templateFs *embed.FS,
w.WriteHeader(http.StatusBadRequest)
}
roomName := r.PostFormValue("roomName")
_, exists, _ := roomsM.Get(roomName)
_, exists, _ := roomsM.Get(r.Context(), roomName)
if exists {
// TODO return anouther error notice
log.Printf("error, room name occupied %s", roomName)
@@ -171,12 +171,13 @@ func createRoomHandler(templateFs *embed.FS,
AllKnownPeople: map[rooms.PersonId]rooms.Person{
person.Id: person},
}
err = roomsM.Save(newRoom)
err = roomsM.Save(r.Context(), newRoom)
if err != nil {
log.Printf("what am i to do? error saving room %s", err)
// todo return error notice somehow
}
newSession, err := sessionSM.Save(r.Context(), newRoom.Name, person.Id)
log.Printf("saving session %v", newSession)
if err != nil {
log.Printf("what am i to do? error saving session %s", err)
// todo return error notice somehow
@@ -187,6 +188,7 @@ func createRoomHandler(templateFs *embed.FS,
Secure: true,
HttpOnly: true,
Path: "/",
Expires: time.Now().Add(newSession.ExpireIn),
})
w.Header().Add("HX-Redirect", fmt.Sprintf("/room/%s", newRoom.Name))
}
@@ -203,7 +205,7 @@ func checkRoomName(templateFs *embed.FS,
w.WriteHeader(http.StatusBadRequest)
}
roomName := r.PostFormValue("roomName")
_, isFound, err := roomsM.Get(roomName)
_, isFound, err := roomsM.Get(r.Context(), roomName)
if err != nil {
log.Printf("/login/room-name-check error finding room %s\n", err)
}
@@ -229,7 +231,7 @@ func joinRoomHandler(templateFs *embed.FS,
personPass := r.PostFormValue("personalPassword")
// a) get room data
room, _, err := roomsM.Get(roomName)
room, _, err := roomsM.Get(r.Context(), roomName)
if err != nil {
log.Printf("/login/join error getting room %s", roomName)
w.WriteHeader(http.StatusBadRequest)
@@ -308,6 +310,7 @@ func joinRoomHandler(templateFs *embed.FS,
}
newSession, err := sessionSM.Save(r.Context(), room.Name, person.Id)
log.Printf("saving session %v", newSession)
if err != nil {
log.Printf("/login/submit > error saving session %s", err)
}
@@ -317,6 +320,7 @@ func joinRoomHandler(templateFs *embed.FS,
Secure: true,
HttpOnly: true,
Path: "/",
Expires: time.Now().Add(newSession.ExpireIn),
})
log.Printf("is is %+v. room things %s & %s, personal things %s and %s. \n found room %+v",
newSession, roomName, roomPass, personName, personPass, room,

View File

@@ -10,6 +10,7 @@ import (
"strconv"
"strings"
"sunshine.industries/some-automoderation/metrics"
"sunshine.industries/some-automoderation/rooms"
"sunshine.industries/some-automoderation/sessions"
)
@@ -23,6 +24,7 @@ func registerPageRoutes(
templateFs *embed.FS,
sessionSM sessions.SessionManagement,
roomsM rooms.RoomManager,
metrics *metrics.MetricsContainer,
) {
http.Handle(roomPath, // ending in / captures all following path sections, i.e room name
http.StripPrefix(roomPath, roomPageRoute(templateFs, roomsM, sessionSM)))
@@ -30,15 +32,15 @@ func registerPageRoutes(
http.Handle(raiseHandPath, // ending in / captures all following path sections, i.e gesture num
authedPageMiddleware(
sessionSM,
http.StripPrefix(raiseHandPath, raiseGestureHandRoute(roomsM))))
http.StripPrefix(raiseHandPath, raiseGestureHandRoute(roomsM, metrics))))
http.Handle("/rooms/releaseHand",
authedPageMiddleware(sessionSM, releaseHandRoute(roomsM)))
authedPageMiddleware(sessionSM, releaseHandRoute(roomsM, metrics)))
http.Handle(subscribeRoomPath,
authedPageMiddleware(
sessionSM,
http.StripPrefix(subscribeRoomPath, streamingRoomStates(templateFs, roomsM))))
http.StripPrefix(subscribeRoomPath, streamingRoomStates(templateFs, roomsM, metrics))))
http.HandleFunc("/rooms/preview-templates", roomTemplatesPreview(templateFs))
@@ -48,8 +50,11 @@ func registerPageRoutes(
func streamingRoomStates(
templateFs *embed.FS,
roomsM rooms.RoomManager,
metrics *metrics.MetricsContainer,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
metrics.LiveConnectionsGauge.Inc()
defer metrics.LiveConnectionsGauge.Dec()
r.ParseForm()
roomName := r.FormValue("roomName")
defer log.Printf("/rooms/subscribe/%s stream ended\n", roomName)
@@ -112,6 +117,7 @@ func streamingRoomStates(
// TODO should return control for raised state
func raiseGestureHandRoute(
roomsM rooms.RoomManager,
metrics *metrics.MetricsContainer,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
gestureInd, err := strconv.Atoi(r.URL.Path)
@@ -120,6 +126,7 @@ func raiseGestureHandRoute(
log.Printf("/rooms/raiseGesture error %s gettin hand symbol index from path %s\n", err, r.URL.Path)
return
}
metrics.RaiseGestureCounter.WithLabelValues(selectedGesture.String()).Inc()
log.Printf("/rooms/raiseGesture successfully got gesture %d : %s", selectedGesture, selectedGesture.String())
session, found := getContextSession(r.Context())
if !found {
@@ -129,6 +136,10 @@ func raiseGestureHandRoute(
}
var room rooms.Room
err = roomsM.Update(r.Context(), session.RoomId, func(fromRoom rooms.Room) (toRoom rooms.Room) {
if fromRoom.CurrentSpeaker == rooms.PersonId(0) {
// room had no speaker, need count new speaker turn
metrics.SpeakerCounter.WithLabelValues(selectedGesture.String()).Inc()
}
toRoom = fromRoom.RaiseHand(session.PersonId, selectedGesture)
room = toRoom
return toRoom
@@ -167,6 +178,7 @@ func raiseGestureHandRoute(
// TODO should return lowered control for passed hand gesture, i guess optional
func releaseHandRoute(
roomsM rooms.RoomManager,
metrics *metrics.MetricsContainer,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, found := getContextSession(r.Context())
@@ -179,6 +191,10 @@ func releaseHandRoute(
err := roomsM.Update(r.Context(), session.RoomId, func(fromRoom rooms.Room) (toRoom rooms.Room) {
toRoom = fromRoom
toRoom.ReleaseHand(session.PersonId)
if fromRoom.CurrentSpeaker != toRoom.CurrentSpeaker && toRoom.CurrentSpeaker != rooms.PersonId(0) {
gesture := toRoom.ParticipantHands[toRoom.CurrentSpeaker]
metrics.SpeakerCounter.WithLabelValues(gesture.String()).Inc()
}
return toRoom
})
if err != nil {
@@ -223,7 +239,7 @@ func roomPageRoute(
// check session,
session, err := getRequestSession(r, sessionSM)
room, found, err := roomsM.Get(roomName)
room, found, err := roomsM.Get(r.Context(), roomName)
if err != nil || session.RoomId != roomName {
log.Printf("not authed with session %+v | error %s, but for wrong room, trying to access %s", session, err, roomName)
renderLoginPage(w, roomName, found)
@@ -268,7 +284,7 @@ func roomPageRoute(
}
data := pageData{
Base: baseData{
Title: "room-lala-from-base",
Title: fmt.Sprintf("Some Automoderation: discussion in '%s'", room.Name),
LoggedIn: true,
},
Content: contentData,

View File

@@ -4,6 +4,7 @@ import (
"embed"
"net/http"
"sunshine.industries/some-automoderation/metrics"
"sunshine.industries/some-automoderation/rooms"
"sunshine.industries/some-automoderation/sessions"
)
@@ -14,7 +15,7 @@ var templateFs embed.FS
//go:embed static
var staticFilesFs embed.FS
func RegisterRoutes(sessionsM sessions.SessionManagement, rooms rooms.RoomManager) {
func RegisterRoutes(sessionsM sessions.SessionManagement, rooms rooms.RoomManager, metrics *metrics.MetricsContainer) {
// login page
registerLoginRoutes(&templateFs, sessionsM, rooms)
@@ -22,7 +23,7 @@ func RegisterRoutes(sessionsM sessions.SessionManagement, rooms rooms.RoomManage
http.Handle("/", indexPageRoute(&templateFs, sessionsM, rooms))
// main conversation room page
registerPageRoutes(&templateFs, sessionsM, rooms)
registerPageRoutes(&templateFs, sessionsM, rooms, metrics)
// static resources route
http.Handle("/static/",

View File

@@ -16,15 +16,18 @@ type SessionData struct {
SessionId int `redis:"session_id"`
RoomId string `redis:"room_id"`
PersonId rooms.PersonId `redis:"person_id"`
ExpireIn time.Duration `redis:"-"`
}
type SessionManagement interface {
Get(sessionId int) SessionData
Get(ctx context.Context, sessionId int) SessionData
Save(ctx context.Context, roomName string, personId rooms.PersonId) (SessionData, error)
Remove(ctx context.Context, sessionId int) error
}
const sessionPrefix = "session"
const SessionTtl = 3 * time.Hour
const SessionProlongationWindow = time.Hour
func sessionIdToKey(sessionId int) string {
return fmt.Sprintf("%s:%d", sessionPrefix, sessionId)
@@ -34,15 +37,34 @@ type RedisSM struct {
Rdb *redis.Client
}
func (redisSM RedisSM) Get(sessionId int) SessionData {
func (redisSM RedisSM) Get(ctx context.Context, sessionId int) SessionData {
var foundSession SessionData
redisKey := sessionIdToKey(sessionId)
err := redisSM.Rdb.HGetAll(context.TODO(), redisKey).Scan(&foundSession)
err := redisSM.Rdb.HGetAll(ctx, redisKey).Scan(&foundSession)
if err != nil {
log.Printf("> error reading %s : %s", redisKey, err)
return SessionData{}
}
log.Printf("> successfully found %d %+v", sessionId, foundSession)
ttl, err := redisSM.Rdb.TTL(ctx, redisKey).Result()
if err != nil {
log.Printf("> error getting ttl for session %+v", foundSession)
return foundSession
}
if ttl == -2 {
log.Printf("> ttl indicates session doesn't exist for session %+v", foundSession)
return SessionData{}
}
if ttl < SessionProlongationWindow {
err = redisSM.Rdb.Expire(ctx, redisKey, SessionTtl).Err()
if err != nil {
log.Printf("> error updating ttl for session %+v", foundSession)
return foundSession
} else {
ttl = SessionTtl
}
}
foundSession.ExpireIn = ttl
return foundSession
}
func (redisSM RedisSM) Save(ctx context.Context, roomName string, personId rooms.PersonId) (SessionData, error) {
@@ -51,9 +73,10 @@ func (redisSM RedisSM) Save(ctx context.Context, roomName string, personId rooms
SessionId: randId,
RoomId: roomName,
PersonId: personId,
ExpireIn: SessionTtl,
}
err := redisSM.Rdb.HSet(ctx, sessionIdToKey(randId), newSession).Err()
redisSM.Rdb.Expire(ctx, sessionIdToKey(randId), 24 * time.Hour)
redisSM.Rdb.Expire(ctx, sessionIdToKey(randId), SessionTtl)
if err != nil {
log.Printf("> error! saving session %+v %s", newSession, err)
@@ -69,7 +92,7 @@ func (redisSM RedisSM) Remove(ctx context.Context, sessionId int) error {
type DummySM struct{}
func (d DummySM) Get(sessionId int) SessionData {
func (d DummySM) Get(ctx context.Context, sessionId int) SessionData {
log.Printf("get dummy session by %d", sessionId)
return SessionData{}
}