Compare commits
8 Commits
de8a661952
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea85460c0a | ||
|
|
acd9f4fc62 | ||
|
|
42c73c5902 | ||
|
|
0a8db09fe8 | ||
|
|
5cab5d88d9 | ||
|
|
a502ee72a0 | ||
|
|
cc33c3f742 | ||
|
|
e0bd77fe3b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@
|
||||
/result
|
||||
/routes/static/out.css
|
||||
/nixos.qcow2
|
||||
/data/
|
||||
|
||||
1162
dev-conf/generic-go-app-dashboard.json
Normal file
1162
dev-conf/generic-go-app-dashboard.json
Normal file
File diff suppressed because it is too large
Load Diff
7
dev-conf/prometheus.yml
Normal file
7
dev-conf/prometheus.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: go
|
||||
static_configs:
|
||||
- targets: ['localhost:8081']
|
||||
66
flake.nix
66
flake.nix
@@ -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
11
go.mod
@@ -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
30
go.sum
@@ -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
27
main.go
@@ -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
43
metrics/metrics.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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{},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user