diff --git a/api_signaling.go b/api_signaling.go index 8f7e959..e1d0514 100644 --- a/api_signaling.go +++ b/api_signaling.go @@ -620,6 +620,18 @@ type RoomFlagsServerMessage struct { Flags uint32 `json:"flags"` } +type ChatComment map[string]interface{} + +type RoomEventMessageDataChat struct { + Comment *ChatComment `json:"comment,omitempty"` +} + +type RoomEventMessageData struct { + Type string `json:"type"` + + Chat *RoomEventMessageDataChat `json:"chat,omitempty"` +} + type EventServerMessage struct { Target string `json:"target"` Type string `json:"type"` @@ -646,6 +658,15 @@ type EventServerMessageSessionEntry struct { RoomSessionId string `json:"roomsessionid,omitempty"` } +func (e *EventServerMessageSessionEntry) Clone() *EventServerMessageSessionEntry { + return &EventServerMessageSessionEntry{ + SessionId: e.SessionId, + UserId: e.UserId, + User: e.User, + RoomSessionId: e.RoomSessionId, + } +} + // MCU-related types type AnswerOfferMessage struct { diff --git a/clientsession.go b/clientsession.go index a68d707..f85437e 100644 --- a/clientsession.go +++ b/clientsession.go @@ -214,6 +214,9 @@ func (s *ClientSession) hasAnyPermissionLocked(permission ...Permission) bool { func (s *ClientSession) hasPermissionLocked(permission Permission) bool { if !s.supportsPermissions { // Old-style session that doesn't receive permissions from Nextcloud. + if result, found := DefaultPermissionOverrides[permission]; found { + return result + } return true } @@ -641,6 +644,11 @@ func (s *ClientSession) SendError(e *Error) bool { } func (s *ClientSession) SendMessage(message *ServerMessage) bool { + message = s.filterMessage(message) + if message == nil { + return true + } + s.mu.Lock() defer s.mu.Unlock() @@ -1026,6 +1034,110 @@ func (s *ClientSession) storePendingMessage(message *ServerMessage) { } } +func filterDisplayNames(events []*EventServerMessageSessionEntry) []*EventServerMessageSessionEntry { + result := make([]*EventServerMessageSessionEntry, 0, len(events)) + for _, event := range events { + if event.User == nil { + result = append(result, event) + continue + } + + var userdata map[string]interface{} + if err := json.Unmarshal(*event.User, &userdata); err != nil { + result = append(result, event) + continue + } + + if _, found := userdata["displayname"]; !found { + result = append(result, event) + continue + } + + delete(userdata, "displayname") + if len(userdata) == 0 { + // No more userdata, no need to serialize empty map. + e := event.Clone() + e.User = nil + result = append(result, e) + continue + } + + data, err := json.Marshal(userdata) + if err != nil { + result = append(result, event) + continue + } + + e := event.Clone() + e.User = (*json.RawMessage)(&data) + result = append(result, e) + } + return result +} + +func (s *ClientSession) filterMessage(message *ServerMessage) *ServerMessage { + switch message.Type { + case "event": + switch message.Event.Target { + case "participants": + if message.Event.Type == "update" { + m := message.Event.Update + users := make(map[string]bool) + for _, entry := range m.Users { + users[entry["sessionId"].(string)] = true + } + for _, entry := range m.Changed { + if users[entry["sessionId"].(string)] { + continue + } + m.Users = append(m.Users, entry) + } + // TODO(jojo): Only send all users if current session id has + // changed its "inCall" flag to true. + m.Changed = nil + } + case "room": + switch message.Event.Type { + case "join": + if s.HasPermission(PERMISSION_HIDE_DISPLAYNAMES) { + message.Event.Join = filterDisplayNames(message.Event.Join) + } + case "message": + if message.Event.Message == nil || message.Event.Message.Data == nil || len(*message.Event.Message.Data) == 0 || !s.HasPermission(PERMISSION_HIDE_DISPLAYNAMES) { + return message + } + + var data RoomEventMessageData + if err := json.Unmarshal(*message.Event.Message.Data, &data); err != nil { + return message + } + + if data.Type == "chat" && data.Chat != nil && data.Chat.Comment != nil { + if displayName, found := (*data.Chat.Comment)["actorDisplayName"]; found && displayName != "" { + (*data.Chat.Comment)["actorDisplayName"] = "" + if encoded, err := json.Marshal(data); err == nil { + message.Event.Message.Data = (*json.RawMessage)(&encoded) + } + } + } + } + } + case "message": + if message.Message != nil && message.Message.Data != nil && len(*message.Message.Data) > 0 && s.HasPermission(PERMISSION_HIDE_DISPLAYNAMES) { + var data MessageServerMessageData + if err := json.Unmarshal(*message.Message.Data, &data); err != nil { + return message + } + + if data.Type == "nickChanged" { + return nil + } + } + } + + return message +} + func (s *ClientSession) processNatsMessage(msg *NatsMessage) *ServerMessage { switch msg.Type { case "message": @@ -1050,23 +1162,7 @@ func (s *ClientSession) processNatsMessage(msg *NatsMessage) *ServerMessage { return nil } case "event": - if msg.Message.Event.Target == "participants" && - msg.Message.Event.Type == "update" { - m := msg.Message.Event.Update - users := make(map[string]bool) - for _, entry := range m.Users { - users[entry["sessionId"].(string)] = true - } - for _, entry := range m.Changed { - if users[entry["sessionId"].(string)] { - continue - } - m.Users = append(m.Users, entry) - } - // TODO(jojo): Only send all users if current session id has - // changed its "inCall" flag to true. - m.Changed = nil - } else if msg.Message.Event.Target == "room" { + if msg.Message.Event.Target == "room" { // Can happen mostly during tests where an older room NATS message // could be received by a subscriber that joined after it was sent. if msg.SendTime.Before(s.getRoomJoinTime()) { diff --git a/hub_test.go b/hub_test.go index 9305275..a4e2e01 100644 --- a/hub_test.go +++ b/hub_test.go @@ -259,6 +259,14 @@ func processAuthRequest(t *testing.T, w http.ResponseWriter, r *http.Request, re UserId: params.UserId, }, } + userdata := map[string]string{ + "displayname": "Displayname " + params.UserId, + } + if data, err := json.Marshal(userdata); err != nil { + t.Fatal(err) + } else { + response.Auth.User = (*json.RawMessage)(&data) + } return response } @@ -1827,6 +1835,88 @@ func TestJoinMultiple(t *testing.T) { } } +func TestJoinMultipleDisplaynamesPermission(t *testing.T) { + hub, _, _, server := CreateHubForTest(t) + + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + if err := client1.SendHello(testDefaultUserId + "1"); err != nil { + t.Fatal(err) + } + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + if err := client2.SendHello(testDefaultUserId + "2"); err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello1, err := client1.RunUntilHello(ctx) + if err != nil { + t.Fatal(err) + } + hello2, err := client2.RunUntilHello(ctx) + if err != nil { + t.Fatal(err) + } + + session2 := hub.GetSessionByPublicId(hello2.Hello.SessionId).(*ClientSession) + if session2 == nil { + t.Fatalf("Session %s does not exist", hello2.Hello.SessionId) + } + + // Client 2 may not receive display names. + session2.SetPermissions([]Permission{PERMISSION_HIDE_DISPLAYNAMES}) + + // Join room by id (first client). + roomId := "test-room" + if room, err := client1.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) + } + + // We will receive a "joined" event. + if err := client1.RunUntilJoined(ctx, hello1.Hello); err != nil { + t.Error(err) + } + + // Join room by id (second client). + if room, err := client2.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) + } + + // We will receive a "joined" event for the first and the second client. + if events, unexpected, err := client2.RunUntilJoinedAndReturn(ctx, hello1.Hello, hello2.Hello); err != nil { + t.Error(err) + } else { + if len(unexpected) > 0 { + t.Errorf("Received unexpected messages: %+v", unexpected) + } else if len(events) != 2 { + t.Errorf("Expected two event, got %+v", events) + } else if events[0].User != nil { + t.Errorf("Expected empty userdata for first event, got %+v", events[0].User) + } else if events[1].User != nil { + t.Errorf("Expected empty userdata for second event, got %+v", events[1].User) + } + } + // The first client will also receive a "joined" event from the second client. + if events, unexpected, err := client1.RunUntilJoinedAndReturn(ctx, hello2.Hello); err != nil { + t.Error(err) + } else { + if len(unexpected) > 0 { + t.Errorf("Received unexpected messages: %+v", unexpected) + } else if len(events) != 1 { + t.Errorf("Expected one event, got %+v", events) + } else if events[0].User == nil { + t.Errorf("Expected userdata for first event, got nothing") + } + } +} + func TestJoinRoomSwitchClient(t *testing.T) { hub, _, _, server := CreateHubForTest(t) diff --git a/session.go b/session.go index 9a8da88..de04ca5 100644 --- a/session.go +++ b/session.go @@ -36,6 +36,14 @@ var ( PERMISSION_MAY_PUBLISH_SCREEN Permission = "publish-screen" PERMISSION_MAY_CONTROL Permission = "control" PERMISSION_TRANSIENT_DATA Permission = "transient-data" + PERMISSION_HIDE_DISPLAYNAMES Permission = "hide-displaynames" + + // DefaultPermissionOverrides contains permission overrides for users where + // no permissions have been set by the server. If a permission is not set in + // this map, it's assumed the user has that permission. + DefaultPermissionOverrides = map[Permission]bool{ + PERMISSION_HIDE_DISPLAYNAMES: false, + } ) type SessionIdData struct {