diff --git a/README.md b/README.md index 02bdaec..e65e554 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ A special `[roles.member]` heading may be used to configure policies for all rel - `api_key` - a key identifying this relay, assigned by the Livekit server. - `api_secret` - a secret key authenticating this relay, assigned by the Livekit server. +On your LiveKit server you should also set up a webhook that points to `https://yourrelay.com/.well-known/nip29/livekit/webhook`. This allows LiveKit to notify your relay when people join rooms so it can publish a kind 39004 event. + ### Example The below config file might be saved as `./config/my-relay.example.com` in order to route requests from `wss://my-relay.example.com` to this virtual relay. diff --git a/go.mod b/go.mod index 8caefb8..0e71d71 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/andybalholm/brotli v1.1.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/benbjohnson/clock v1.3.5 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/bep/debounce v1.2.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect @@ -39,6 +40,8 @@ require ( github.com/google/cel-go v0.25.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gosimple/unidecode v1.0.1 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jxskiss/base62 v1.1.0 // indirect @@ -53,6 +56,7 @@ require ( github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nats-io/nats.go v1.43.0 // indirect github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect @@ -72,6 +76,10 @@ require ( github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v4 v4.0.2 // indirect github.com/pion/webrtc/v4 v4.1.2 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/redis/go-redis/v9 v9.11.0 // indirect github.com/rs/cors v1.11.1 // indirect @@ -105,4 +113,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace fiatjaf.com/nostr => gitea.coracle.social/Coracle/nostrlib v0.0.0-20260226033137-d6812c040a53 +replace fiatjaf.com/nostr => gitea.coracle.social/Coracle/nostrlib v0.0.0-20260313164927-662e7d271c47 diff --git a/go.sum b/go.sum index 7336716..78136bd 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -gitea.coracle.social/Coracle/nostrlib v0.0.0-20260226033137-d6812c040a53 h1:HJ7KJ7IhEtqOe5vR3fPBIDYksTr75kbvICFiMurhgYY= -gitea.coracle.social/Coracle/nostrlib v0.0.0-20260226033137-d6812c040a53/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= +gitea.coracle.social/Coracle/nostrlib v0.0.0-20260313164927-662e7d271c47 h1:Pg/8ZXG2diV3uWbgt3mcAWF2ifL4FZXwotieokY8TBA= +gitea.coracle.social/Coracle/nostrlib v0.0.0-20260313164927-662e7d271c47/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= @@ -30,6 +30,8 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +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/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -73,6 +75,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE= github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frostbyte73/core v0.1.1 h1:ChhJOR7bAKOCPbA+lqDLE2cGKlCG5JXsDvvQr4YaJIA= github.com/frostbyte73/core v0.1.1/go.mod h1:mhfOtR+xWAvwXiwor7jnqPMnu4fxbv1F2MwZ0BEpzZo= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -101,6 +105,12 @@ github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -131,6 +141,10 @@ github.com/livekit/psrpc v0.7.1 h1:ms37az0QTD3UXIWuUC5D/SkmKOlRMVRsI261eBWu/Vw= github.com/livekit/psrpc v0.7.1/go.mod h1:bZ4iHFQptTkbPnB0LasvRNu/OBYXEu1NA6O5BMFo9kk= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -144,6 +158,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nats-io/nats.go v1.43.0 h1:uRFZ2FEoRvP64+UUhaTokyS18XBCR/xM2vQZKO4i8ug= github.com/nats-io/nats.go v1.43.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= @@ -194,6 +210,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= +github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= @@ -326,8 +350,8 @@ google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHh google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/zooid/groups.go b/zooid/groups.go index a7a3955..75406f4 100644 --- a/zooid/groups.go +++ b/zooid/groups.go @@ -11,7 +11,7 @@ import ( func GetGroupIDFromEvent(event nostr.Event) string { var tagName string - if slices.Contains(nip29.MetadataEventKinds, event.Kind) { + if slices.Contains(nip29.MetadataEventKinds, event.Kind) || event.Kind == nostr.KindSimpleGroupLiveKitParticipants { tagName = "d" } else { tagName = "h" @@ -322,14 +322,6 @@ func (g *GroupStore) CheckWrite(event nostr.Event) string { } } - if HasTag(meta.Tags, "no-text") && - !slices.Contains(nip29.ModerationEventKinds, event.Kind) && - event.Kind != nostr.KindSimpleGroupJoinRequest && - event.Kind != nostr.KindSimpleGroupLeaveRequest && - event.Kind != ROOM_PRESENCE { - return "blocked: this group does not allow text events" - } - if HasTag(meta.Tags, "closed") && !g.HasAccess(h, event.PubKey) { return "restricted: you are not a member of that group" } diff --git a/zooid/instance.go b/zooid/instance.go index 084aaf8..ab4c486 100644 --- a/zooid/instance.go +++ b/zooid/instance.go @@ -113,6 +113,9 @@ func MakeInstance(filename string) (*Instance, error) { router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + router.HandleFunc("GET /.well-known/nip29/livekit", instance.livekitSupportHandler) + router.HandleFunc("OPTIONS /.well-known/nip29/livekit", instance.livekitSupportHandler) + router.HandleFunc("POST /.well-known/nip29/livekit/webhook", instance.livekitWebhookHandler) router.HandleFunc("GET /.well-known/nip29/livekit/{groupId}", instance.livekitTokenHandler) router.HandleFunc("OPTIONS /.well-known/nip29/livekit/{groupId}", instance.livekitTokenHandler) diff --git a/zooid/livekit.go b/zooid/livekit.go index 768b1c6..71b030c 100644 --- a/zooid/livekit.go +++ b/zooid/livekit.go @@ -4,12 +4,15 @@ import ( "bytes" "encoding/json" "fmt" + "log" "net/http" "strings" "sync" "fiatjaf.com/nostr" "github.com/livekit/protocol/auth" + "github.com/livekit/protocol/webhook" + "slices" ) var ( @@ -29,7 +32,7 @@ func generateLivekitToken(apiKey, apiSecret, room string, pubkey nostr.PubKey) s RoomJoin: true, Room: room, }) - at.SetIdentity(pubkey.Hex()) + at.SetIdentity(pubkey.Hex() + ":" + RandomString(16)) jwt, _ := at.ToJWT() return jwt @@ -88,6 +91,24 @@ func ensureLivekitRoom(apiKey, apiSecret, serverURL, roomName string) error { return fmt.Errorf("failed to create room: %s", resp.Status) } +func (instance *Instance) livekitSupportHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Authorization") + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + + if r.Method == http.MethodOptions { + return + } + + cfg := instance.Config.Livekit + if cfg.APIKey == "" { + http.NotFound(w, r) + return + } + + w.WriteHeader(http.StatusNoContent) +} + func (instance *Instance) livekitTokenHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Authorization") @@ -145,3 +166,125 @@ func (instance *Instance) livekitTokenHandler(w http.ResponseWriter, r *http.Req ParticipantToken: token, }) } + +func (instance *Instance) livekitWebhookHandler(w http.ResponseWriter, r *http.Request) { + cfg := instance.Config.Livekit + if cfg.APIKey == "" || cfg.APISecret == "" { + http.NotFound(w, r) + return + } + + kp := auth.NewSimpleKeyProvider(cfg.APIKey, cfg.APISecret) + event, err := webhook.ReceiveWebhookEvent(r, kp) + if err != nil { + http.Error(w, "invalid webhook: "+err.Error(), http.StatusUnauthorized) + return + } + + room := event.GetRoom() + if room == nil { + http.Error(w, "missing room", http.StatusBadRequest) + return + } + groupId := room.GetName() + if groupId == "" { + http.Error(w, "missing room name", http.StatusBadRequest) + return + } + + meta, found := instance.Groups.GetMetadata(groupId) + if !found { + http.NotFound(w, r) + return + } + + if !HasTag(meta.Tags, "livekit") { + http.Error(w, "livekit not enabled for this group", http.StatusForbidden) + return + } + + switch event.Event { + case webhook.EventParticipantJoined, webhook.EventParticipantLeft: + participant := event.GetParticipant() + if participant == nil || len(participant.Identity) < 64 { + http.Error(w, "missing participant", http.StatusBadRequest) + return + } + + if _, err := nostr.PubKeyFromHex(participant.Identity[0:64]); err != nil { + log.Printf("[livekit webhook] invalid nostr pubkey in identity: %v", err) + w.WriteHeader(http.StatusNoContent) + return + } + + connected := event.Event == webhook.EventParticipantJoined + if err := instance.updateLiveKitPresence(groupId, participant.Identity, connected); err != nil { + http.Error(w, "failed to update livekit participants: "+err.Error(), http.StatusInternalServerError) + return + } + default: + w.WriteHeader(http.StatusNoContent) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (instance *Instance) updateLiveKitPresence(groupId string, identity string, connected bool) error { + identities := instance.getLiveKitParticipantIdentities(groupId) + + if connected { + if !slices.Contains(identities, identity) { + identities = append(identities, identity) + } + } else { + if idx := slices.Index(identities, identity); idx != -1 { + identities[idx] = identities[len(identities)-1] + identities = identities[:len(identities)-1] + } else { + log.Printf("[livekit webhook] identity %q not in list when processing leave (had %d participants)", + identity, len(identities)) + } + } + + log.Printf("[livekit webhook] presence update: room=%s connected=%v count=%d", + groupId, connected, len(identities)) + + return instance.publishLiveKitPresence(groupId, identities) +} + +func (instance *Instance) getLiveKitParticipantIdentities(groupId string) []string { + filter := nostr.Filter{ + Kinds: []nostr.Kind{nostr.KindSimpleGroupLiveKitParticipants}, + Authors: []nostr.PubKey{instance.Config.GetSelf()}, + Tags: nostr.TagMap{"d": []string{groupId}}, + } + + for event := range instance.Events.QueryEvents(filter, 1) { + var identities []string + for tag := range event.Tags.FindAll("participant") { + if len(tag) >= 2 && tag[1] != "" { + if !slices.Contains(identities, tag[1]) { + identities = append(identities, tag[1]) + } + } + } + return identities + } + return nil +} + +func (instance *Instance) publishLiveKitPresence(groupId string, identities []string) error { + tags := nostr.Tags{nostr.Tag{"d", groupId}} + for _, identity := range identities { + tags = append(tags, nostr.Tag{"participant", identity}) + } + + event := nostr.Event{ + Kind: nostr.KindSimpleGroupLiveKitParticipants, + CreatedAt: nostr.Now(), + Tags: tags, + } + + return instance.Events.SignAndStoreEvent(&event, true) +}