diff --git a/api_signaling.go b/api_signaling.go index d9ca99a..1c48c78 100644 --- a/api_signaling.go +++ b/api_signaling.go @@ -24,6 +24,7 @@ package signaling import ( "encoding/json" "fmt" + "log" "net/url" "sort" "strings" @@ -220,9 +221,9 @@ func (r *ServerMessage) String() string { } type Error struct { - Code string `json:"code"` - Message string `json:"message"` - Details interface{} `json:"details,omitempty"` + Code string `json:"code"` + Message string `json:"message"` + Details json.RawMessage `json:"details,omitempty"` } func NewError(code string, message string) *Error { @@ -230,10 +231,19 @@ func NewError(code string, message string) *Error { } func NewErrorDetail(code string, message string, details interface{}) *Error { + var rawDetails json.RawMessage + if details != nil { + var err error + if rawDetails, err = json.Marshal(details); err != nil { + log.Printf("Could not marshal details %+v for error %s with %s: %s", details, code, message, err) + return NewError("internal_error", "Could not marshal error details") + } + } + return &Error{ Code: code, Message: message, - Details: details, + Details: rawDetails, } } @@ -511,6 +521,10 @@ type RoomServerMessage struct { Properties *json.RawMessage `json:"properties,omitempty"` } +type RoomErrorDetails struct { + Room *RoomServerMessage `json:"room"` +} + // Type "message" const ( diff --git a/clientsession.go b/clientsession.go index 9bb816e..e0243b0 100644 --- a/clientsession.go +++ b/clientsession.go @@ -417,6 +417,36 @@ func (s *ClientSession) SubscribeEvents() error { return s.events.RegisterSessionListener(s.publicId, s.backend, s) } +func (s *ClientSession) UpdateRoomSessionId(roomSessionId string) error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.roomSessionId == roomSessionId { + return nil + } + + if err := s.hub.roomSessions.SetRoomSession(s, roomSessionId); err != nil { + return err + } + + if roomSessionId != "" { + if room := s.GetRoom(); room != nil { + log.Printf("Session %s updated room session id to %s in room %s", s.PublicId(), roomSessionId, room.Id()) + } else { + log.Printf("Session %s updated room session id to %s in unknown room", s.PublicId(), roomSessionId) + } + } else { + if room := s.GetRoom(); room != nil { + log.Printf("Session %s cleared room session id in room %s", s.PublicId(), room.Id()) + } else { + log.Printf("Session %s cleared room session id in unknown room", s.PublicId()) + } + } + + s.roomSessionId = roomSessionId + return nil +} + func (s *ClientSession) SubscribeRoomEvents(roomid string, roomSessionId string) error { s.mu.Lock() defer s.mu.Unlock() diff --git a/docs/standalone-signaling-api-v1.md b/docs/standalone-signaling-api-v1.md index 69ea27e..a53dd26 100644 --- a/docs/standalone-signaling-api-v1.md +++ b/docs/standalone-signaling-api-v1.md @@ -460,6 +460,26 @@ Message format (Server -> Client): the current room or the properties of a room change. +Message format (Server -> Client if already joined before): + + { + "id": "unique-request-id-from-request", + "type": "error", + "error": { + "code": "already_joined", + "message": "Human readable error message", + "details": { + "roomid": "the-room-id", + "properties": { + ...additional room properties... + } + } + } + } + +- Sent if a client tried to join a room it is already in. + + ### Backend validation Rooms are managed by the Nextcloud backend, so the signaling server has to diff --git a/hub.go b/hub.go index 7a4e51a..ee6b1eb 100644 --- a/hub.go +++ b/hub.go @@ -1256,6 +1256,24 @@ func (h *Hub) processRoom(client *Client, message *ClientMessage) { if session != nil { if room := h.getRoomForBackend(roomId, session.Backend()); room != nil && room.HasSession(session) { // Session already is in that room, no action needed. + roomSessionId := message.Room.SessionId + if roomSessionId == "" { + // TODO(jojo): Better make the session id required in the request. + log.Printf("User did not send a room session id, assuming session %s", session.PublicId()) + roomSessionId = session.PublicId() + } + + if err := session.UpdateRoomSessionId(roomSessionId); err != nil { + log.Printf("Error updating room session id for session %s: %s", session.PublicId(), err) + } + session.SendMessage(message.NewErrorServerMessage( + NewErrorDetail("already_joined", "Already joined this room.", &RoomErrorDetails{ + Room: &RoomServerMessage{ + RoomId: room.id, + Properties: room.properties, + }, + }), + )) return } } diff --git a/hub_test.go b/hub_test.go index 6f05d30..9bfa242 100644 --- a/hub_test.go +++ b/hub_test.go @@ -22,6 +22,7 @@ package signaling import ( + "bytes" "context" "crypto/ecdsa" "crypto/ed25519" @@ -58,6 +59,10 @@ const ( testTimeout = 10 * time.Second ) +var ( + testRoomProperties = []byte("{\"prop1\":\"value1\"}") +) + var ( clusteredTests = []string{ "local", @@ -403,8 +408,9 @@ func processRoomRequest(t *testing.T, w http.ResponseWriter, r *http.Request, re response := &BackendClientResponse{ Type: "room", Room: &BackendClientRoomResponse{ - Version: BackendVersion, - RoomId: request.Room.RoomId, + Version: BackendVersion, + RoomId: request.Room.RoomId, + Properties: (*json.RawMessage)(&testRoomProperties), }, } switch request.Room.RoomId { @@ -2628,6 +2634,83 @@ func TestJoinRoom(t *testing.T) { } } +func TestJoinRoomTwice(t *testing.T) { + hub, _, _, server := CreateHubForTest(t) + + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + if err := client.SendHello(testDefaultUserId); err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello, err := client.RunUntilHello(ctx) + if err != nil { + t.Fatal(err) + } + + // Join room by id. + roomId := "test-room" + if room, err := client.JoinRoom(ctx, roomId); err != nil { + t.Fatal(err) + } else if room.Room.RoomId != roomId { + t.Fatalf("Expected room %s, got %s", roomId, room.Room.RoomId) + } else if !bytes.Equal(testRoomProperties, *room.Room.Properties) { + t.Fatalf("Expected room properties %s, got %s", string(testRoomProperties), string(*room.Room.Properties)) + } + + // We will receive a "joined" event. + if err := client.RunUntilJoined(ctx, hello.Hello); err != nil { + t.Error(err) + } + + msg := &ClientMessage{ + Id: "ABCD", + Type: "room", + Room: &RoomClientMessage{ + RoomId: roomId, + SessionId: roomId + "-" + client.publicId + "-2", + }, + } + if err := client.WriteJSON(msg); err != nil { + t.Fatal(err) + } + + message, err := client.RunUntilMessage(ctx) + if err != nil { + t.Fatal(err) + } + if err := checkUnexpectedClose(err); err != nil { + t.Fatal(err) + } + + if msg.Id != message.Id { + t.Errorf("expected message id %s, got %s", msg.Id, message.Id) + } else if err := checkMessageType(message, "error"); err != nil { + t.Fatal(err) + } else if expected := "already_joined"; message.Error.Code != expected { + t.Errorf("expected error %s, got %s", expected, message.Error.Code) + } else if message.Error.Details == nil { + t.Fatal("expected error details") + } + + var roomMsg RoomErrorDetails + if err := json.Unmarshal(message.Error.Details, &roomMsg); err != nil { + t.Fatal(err) + } else if roomMsg.Room == nil { + t.Fatalf("expected room details, got %+v", message) + } + + if roomMsg.Room.RoomId != roomId { + t.Fatalf("Expected room %s, got %+v", roomId, roomMsg.Room) + } else if !bytes.Equal(testRoomProperties, *roomMsg.Room.Properties) { + t.Fatalf("Expected room properties %s, got %s", string(testRoomProperties), string(*roomMsg.Room.Properties)) + } +} + func TestExpectAnonymousJoinRoom(t *testing.T) { hub, _, _, server := CreateHubForTest(t)