From eafa39a1c5c0d089896cd1dcc4c2c926281be523 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 29 Jan 2026 11:31:08 +0100 Subject: [PATCH 1/3] Support receiving and forwarding multiple chat messages from Talk. --- api/signaling.go | 8 ++ server/clientsession.go | 134 ++++++++++++++++++++--------- server/clientsession_test.go | 159 +++++++++++++++++++++++++++++++++++ 3 files changed, 260 insertions(+), 41 deletions(-) diff --git a/api/signaling.go b/api/signaling.go index 56b4c61..27ee638 100644 --- a/api/signaling.go +++ b/api/signaling.go @@ -1215,6 +1215,14 @@ type RoomEventMessageDataChat struct { // Comment will be included if the client supports the "chat-relay" feature. Comment json.RawMessage `json:"comment,omitempty"` + // Comments will be included if the client supports the "chat-relay" feature. + Comments []json.RawMessage `json:"comments,omitempty"` +} + +func (m *RoomEventMessageDataChat) HasComment() bool { + return len(m.Comment) > 0 || slices.ContainsFunc(m.Comments, func(comment json.RawMessage) bool { + return len(comment) > 0 + }) } type RoomEventMessageData struct { diff --git a/server/clientsession.go b/server/clientsession.go index 276c48e..10f1145 100644 --- a/server/clientsession.go +++ b/server/clientsession.go @@ -721,15 +721,14 @@ func (s *ClientSession) sendCandidate(client sfu.Client, sender api.PublicSessio } // +checklocks:s.mu -func (s *ClientSession) sendMessageUnlocked(message *api.ServerMessage) bool { +func (s *ClientSession) sendMessageUnlocked(message *api.ServerMessage) { if c := s.getClientUnlocked(); c != nil { if c.SendMessage(message) { - return true + return } } s.storePendingMessage(message) - return true } func (s *ClientSession) SendError(e *api.Error) bool { @@ -741,15 +740,21 @@ func (s *ClientSession) SendError(e *api.Error) bool { } func (s *ClientSession) SendMessage(message *api.ServerMessage) bool { - message = s.filterMessage(message) - if message == nil { + message, messages := s.filterMessage(message) + if message == nil && len(messages) == 0 { return true } s.mu.Lock() defer s.mu.Unlock() - return s.sendMessageUnlocked(message) + if message != nil { + s.sendMessageUnlocked(message) + } + for _, msg := range messages { + s.sendMessageUnlocked(msg) + } + return true } func (s *ClientSession) SendMessages(messages []*api.ServerMessage) bool { @@ -1333,7 +1338,7 @@ func (s *ClientSession) filterDuplicateFlags(message *api.RoomFlagsServerMessage return false } -func (s *ClientSession) filterMessage(message *api.ServerMessage) *api.ServerMessage { +func (s *ClientSession) filterMessage(message *api.ServerMessage) (*api.ServerMessage, []*api.ServerMessage) { switch message.Type { case "event": switch message.Event.Target { @@ -1356,7 +1361,7 @@ func (s *ClientSession) filterMessage(message *api.ServerMessage) *api.ServerMes m.Changed = nil case "flags": if s.filterDuplicateFlags(message.Event.Flags) { - return nil + return nil, nil } } case "room": @@ -1364,7 +1369,7 @@ func (s *ClientSession) filterMessage(message *api.ServerMessage) *api.ServerMes case "join": join := s.filterDuplicateJoin(message.Event.Join) if len(join) == 0 { - return nil + return nil, nil } copied := false if len(join) != len(message.Event.Join) { @@ -1402,7 +1407,7 @@ func (s *ClientSession) filterMessage(message *api.ServerMessage) *api.ServerMes leave := s.filterUnknownLeave(message.Event.Leave) if len(leave) == 0 { - return nil + return nil, nil } for _, e := range message.Event.Leave { @@ -1423,57 +1428,104 @@ func (s *ClientSession) filterMessage(message *api.ServerMessage) *api.ServerMes } case "message": if message.Event.Message == nil || len(message.Event.Message.Data) == 0 { - return message + return message, nil } data, err := message.Event.Message.GetData() if data == nil || err != nil { - return message + return message, nil } if data.Type == "chat" && data.Chat != nil { update := false - if data.Chat.Refresh && len(data.Chat.Comment) > 0 { + if data.Chat.Refresh && data.Chat.HasComment() { // New-style chat event, check what the client supports. if s.HasFeature(api.ClientFeatureChatRelay) { data.Chat.Refresh = false } else { data.Chat.Comment = nil + data.Chat.Comments = nil } update = true } - if len(data.Chat.Comment) > 0 && s.HasPermission(api.PERMISSION_HIDE_DISPLAYNAMES) { - var comment api.ChatComment - if err := json.Unmarshal(data.Chat.Comment, &comment); err != nil { - return message - } - - if displayName, found := comment["actorDisplayName"]; found && displayName != "" { - comment["actorDisplayName"] = "" - var err error - if data.Chat.Comment, err = json.Marshal(comment); err != nil { - return message + if data.Chat.HasComment() { + data.Chat.Comments = slices.DeleteFunc(data.Chat.Comments, func(comment json.RawMessage) bool { + return len(comment) == 0 + }) + if len(data.Chat.Comment) > 0 { + if len(data.Chat.Comments) == 0 { + data.Chat.Comments = []json.RawMessage{data.Chat.Comment} + } else { + data.Chat.Comments = append([]json.RawMessage{data.Chat.Comment}, data.Chat.Comments...) + } + data.Chat.Comment = nil + } + if len(data.Chat.Comments) > 0 && s.HasPermission(api.PERMISSION_HIDE_DISPLAYNAMES) { + for i, commentData := range data.Chat.Comments { + var comment api.ChatComment + if err := json.Unmarshal(commentData, &comment); err != nil { + continue + } + + if displayName, found := comment["actorDisplayName"]; found && displayName != "" { + comment["actorDisplayName"] = "" + var err error + if commentData, err = json.Marshal(comment); err != nil { + continue + } + data.Chat.Comments[i] = commentData + update = true + } } - update = true } } - if update { - if encoded, err := json.Marshal(data); err == nil { - // Create unique copy of message for only this client. - message = &api.ServerMessage{ - Id: message.Id, - Type: message.Type, - Event: &api.EventServerMessage{ - Type: message.Event.Type, - Target: message.Event.Target, - Message: &api.RoomEventMessage{ - RoomId: message.Event.Message.RoomId, - Data: encoded, + if update || len(data.Chat.Comments) > 0 { + if len(data.Chat.Comment) == 0 && len(data.Chat.Comments) == 0 { + if encoded, err := json.Marshal(data); err == nil { + // Create unique copy of message for only this client. + message = &api.ServerMessage{ + Id: message.Id, + Type: message.Type, + Event: &api.EventServerMessage{ + Type: message.Event.Type, + Target: message.Event.Target, + Message: &api.RoomEventMessage{ + RoomId: message.Event.Message.RoomId, + Data: encoded, + }, }, - }, + } } + } else { + // Forward different chat comments individually. + var result []*api.ServerMessage + for _, comment := range data.Chat.Comments { + commentData := api.RoomEventMessageData{ + Type: data.Type, + Chat: &api.RoomEventMessageDataChat{ + Refresh: data.Chat.Refresh, + Comment: comment, + }, + } + if encoded, err := json.Marshal(commentData); err == nil { + // Create unique copy of message for only this client. + result = append(result, &api.ServerMessage{ + Id: message.Id, + Type: message.Type, + Event: &api.EventServerMessage{ + Type: message.Event.Type, + Target: message.Event.Target, + Message: &api.RoomEventMessage{ + RoomId: message.Event.Message.RoomId, + Data: encoded, + }, + }, + }) + } + } + return nil, result } } } @@ -1483,16 +1535,16 @@ func (s *ClientSession) filterMessage(message *api.ServerMessage) *api.ServerMes if message.Message != nil && len(message.Message.Data) > 0 && s.HasPermission(api.PERMISSION_HIDE_DISPLAYNAMES) { var data api.MessageServerMessageData if err := json.Unmarshal(message.Message.Data, &data); err != nil { - return message + return message, nil } if data.Type == "nickChanged" { - return nil + return nil, nil } } } - return message + return message, nil } func (s *ClientSession) filterAsyncMessage(msg *events.AsyncMessage) *api.ServerMessage { diff --git a/server/clientsession_test.go b/server/clientsession_test.go index 208cc5d..5466618 100644 --- a/server/clientsession_test.go +++ b/server/clientsession_test.go @@ -249,6 +249,72 @@ func TestFeatureChatRelay(t *testing.T) { } } } + + chatComment2 := api.StringMap{ + "hello": "world", + } + message2 := api.StringMap{ + "type": "chat", + "chat": api.StringMap{ + "refresh": true, + "comments": []api.StringMap{ + chatComment, + chatComment2, + }, + }, + } + data2, err := json.Marshal(message2) + require.NoError(err) + + // Simulate request from the backend. + room.processAsyncMessage(&events.AsyncMessage{ + Type: "room", + Room: &talk.BackendServerRoomRequest{ + Type: "message", + Message: &talk.BackendRoomMessageRequest{ + Data: data2, + }, + }, + }) + + if msg, ok := client.RunUntilRoomMessage(ctx); ok { + assert.Equal(roomId, msg.RoomId) + var data api.StringMap + if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { + assert.Equal("chat", data["type"], "invalid type entry in %+v", data) + if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { + if feature { + assert.EqualValues(chatComment, chat["comment"]) + _, found := chat["refresh"] + assert.False(found, "refresh should not be included") + + // A second message with the second comment will be sent + if msg, ok := client.RunUntilRoomMessage(ctx); ok { + assert.Equal(roomId, msg.RoomId) + + if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { + assert.Equal("chat", data["type"], "invalid type entry in %+v", data) + if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { + assert.EqualValues(chatComment2, chat["comment"]) + _, found := chat["refresh"] + assert.False(found, "refresh should not be included") + } + } + } + } else { + // Only a single refresh will be sent + assert.Equal(true, chat["refresh"]) + _, found := chat["comment"] + assert.False(found, "the comment should not be included") + + ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel2() + + client.RunUntilErrorIs(ctx2, context.DeadlineExceeded) + } + } + } + } } } @@ -461,6 +527,99 @@ func TestFeatureChatRelayFederation(t *testing.T) { } } } + + chatComment2 := api.StringMap{ + "hello": "world", + } + message2 := api.StringMap{ + "type": "chat", + "chat": api.StringMap{ + "refresh": true, + "comments": []api.StringMap{ + chatComment, + chatComment2, + }, + }, + } + data2, err := json.Marshal(message2) + require.NoError(err) + + // Simulate request from the backend. + room.processAsyncMessage(&events.AsyncMessage{ + Type: "room", + Room: &talk.BackendServerRoomRequest{ + Type: "message", + Message: &talk.BackendRoomMessageRequest{ + Data: data2, + }, + }, + }) + + // The first client will receive the message for the local room (always including the actual message). + if msg, ok := client1.RunUntilRoomMessage(ctx); ok { + assert.Equal(roomId, msg.RoomId) + var data api.StringMap + if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { + assert.Equal("chat", data["type"], "invalid type entry in %+v", data) + if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { + AssertEqualSerialized(t, chatComment, chat["comment"]) + _, found := chat["refresh"] + assert.False(found, "refresh should not be included") + } + } + } + // A second message with the second comment will be sent + if msg, ok := client1.RunUntilRoomMessage(ctx); ok { + assert.Equal(roomId, msg.RoomId) + var data api.StringMap + if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { + assert.Equal("chat", data["type"], "invalid type entry in %+v", data) + if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { + assert.EqualValues(chatComment2, chat["comment"]) + _, found := chat["refresh"] + assert.False(found, "refresh should not be included") + } + } + } + + // The second client will receive the message from the federated room (either as refresh or with the message). + if msg, ok := client2.RunUntilRoomMessage(ctx); ok { + assert.Equal(federatedRoomId, msg.RoomId) + var data api.StringMap + if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { + assert.Equal("chat", data["type"], "invalid type entry in %+v", data) + if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { + if feature { + AssertEqualSerialized(t, federatedChatComment, chat["comment"]) + _, found := chat["refresh"] + assert.False(found, "refresh should not be included") + + // A second message with the second comment will be sent + if msg, ok := client2.RunUntilRoomMessage(ctx); ok { + assert.Equal(federatedRoomId, msg.RoomId) + if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { + assert.Equal("chat", data["type"], "invalid type entry in %+v", data) + if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { + assert.EqualValues(chatComment2, chat["comment"]) + _, found := chat["refresh"] + assert.False(found, "refresh should not be included") + } + } + } + } else { + // Only a single refresh will be sent + assert.Equal(true, chat["refresh"]) + _, found := chat["comment"] + assert.False(found, "the comment should not be included") + + ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel2() + + client2.RunUntilErrorIs(ctx2, context.DeadlineExceeded) + } + } + } + } } } From 77f06726821825404b49f6549e940e304fc7c136 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 29 Jan 2026 11:31:41 +0100 Subject: [PATCH 2/3] Update generated files. --- api/signaling_easyjson.go | 270 ++++++++++++++++++++++---------------- 1 file changed, 159 insertions(+), 111 deletions(-) diff --git a/api/signaling_easyjson.go b/api/signaling_easyjson.go index 4a4ce0e..c385b66 100644 --- a/api/signaling_easyjson.go +++ b/api/signaling_easyjson.go @@ -1396,6 +1396,35 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingApi9(in *jl in.AddError((out.Comment).UnmarshalJSON(data)) } } + case "comments": + if in.IsNull() { + in.Skip() + out.Comments = nil + } else { + in.Delim('[') + if out.Comments == nil { + if !in.IsDelim(']') { + out.Comments = make([]json.RawMessage, 0, 2) + } else { + out.Comments = []json.RawMessage{} + } + } else { + out.Comments = (out.Comments)[:0] + } + for !in.IsDelim(']') { + var v16 json.RawMessage + if in.IsNull() { + in.Skip() + } else { + if data := in.Raw(); in.Ok() { + in.AddError((v16).UnmarshalJSON(data)) + } + } + out.Comments = append(out.Comments, v16) + in.WantComma() + } + in.Delim(']') + } default: in.SkipRecursive() } @@ -1426,6 +1455,25 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingApi9(out *j } out.Raw((in.Comment).MarshalJSON()) } + if len(in.Comments) != 0 { + const prefix string = ",\"comments\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + { + out.RawByte('[') + for v17, v18 := range in.Comments { + if v17 > 0 { + out.RawByte(',') + } + out.Raw((v18).MarshalJSON()) + } + out.RawByte(']') + } + } out.RawByte('}') } @@ -1749,33 +1797,33 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingApi13(in *j out.Changed = (out.Changed)[:0] } for !in.IsDelim(']') { - var v16 StringMap + var v19 StringMap if in.IsNull() { in.Skip() } else { in.Delim('{') if !in.IsDelim('}') { - v16 = make(StringMap) + v19 = make(StringMap) } else { - v16 = nil + v19 = nil } for !in.IsDelim('}') { key := string(in.String()) in.WantColon() - var v17 interface{} - if m, ok := v17.(easyjson.Unmarshaler); ok { + var v20 interface{} + if m, ok := v20.(easyjson.Unmarshaler); ok { m.UnmarshalEasyJSON(in) - } else if m, ok := v17.(json.Unmarshaler); ok { + } else if m, ok := v20.(json.Unmarshaler); ok { _ = m.UnmarshalJSON(in.Raw()) } else { - v17 = in.Interface() + v20 = in.Interface() } - (v16)[key] = v17 + (v19)[key] = v20 in.WantComma() } in.Delim('}') } - out.Changed = append(out.Changed, v16) + out.Changed = append(out.Changed, v19) in.WantComma() } in.Delim(']') @@ -1796,33 +1844,33 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingApi13(in *j out.Users = (out.Users)[:0] } for !in.IsDelim(']') { - var v18 StringMap + var v21 StringMap if in.IsNull() { in.Skip() } else { in.Delim('{') if !in.IsDelim('}') { - v18 = make(StringMap) + v21 = make(StringMap) } else { - v18 = nil + v21 = nil } for !in.IsDelim('}') { key := string(in.String()) in.WantColon() - var v19 interface{} - if m, ok := v19.(easyjson.Unmarshaler); ok { + var v22 interface{} + if m, ok := v22.(easyjson.Unmarshaler); ok { m.UnmarshalEasyJSON(in) - } else if m, ok := v19.(json.Unmarshaler); ok { + } else if m, ok := v22.(json.Unmarshaler); ok { _ = m.UnmarshalJSON(in.Raw()) } else { - v19 = in.Interface() + v22 = in.Interface() } - (v18)[key] = v19 + (v21)[key] = v22 in.WantComma() } in.Delim('}') } - out.Users = append(out.Users, v18) + out.Users = append(out.Users, v21) in.WantComma() } in.Delim(']') @@ -1872,43 +1920,7 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingApi13(out * out.RawString(prefix) { out.RawByte('[') - for v20, v21 := range in.Changed { - if v20 > 0 { - out.RawByte(',') - } - if v21 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { - out.RawString(`null`) - } else { - out.RawByte('{') - v22First := true - for v22Name, v22Value := range v21 { - if v22First { - v22First = false - } else { - out.RawByte(',') - } - out.String(string(v22Name)) - out.RawByte(':') - if m, ok := v22Value.(easyjson.Marshaler); ok { - m.MarshalEasyJSON(out) - } else if m, ok := v22Value.(json.Marshaler); ok { - out.Raw(m.MarshalJSON()) - } else { - out.Raw(json.Marshal(v22Value)) - } - } - out.RawByte('}') - } - } - out.RawByte(']') - } - } - if len(in.Users) != 0 { - const prefix string = ",\"users\":" - out.RawString(prefix) - { - out.RawByte('[') - for v23, v24 := range in.Users { + for v23, v24 := range in.Changed { if v23 > 0 { out.RawByte(',') } @@ -1939,6 +1951,42 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingApi13(out * out.RawByte(']') } } + if len(in.Users) != 0 { + const prefix string = ",\"users\":" + out.RawString(prefix) + { + out.RawByte('[') + for v26, v27 := range in.Users { + if v26 > 0 { + out.RawByte(',') + } + if v27 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { + out.RawString(`null`) + } else { + out.RawByte('{') + v28First := true + for v28Name, v28Value := range v27 { + if v28First { + v28First = false + } else { + out.RawByte(',') + } + out.String(string(v28Name)) + out.RawByte(':') + if m, ok := v28Value.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := v28Value.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(v28Value)) + } + } + out.RawByte('}') + } + } + out.RawByte(']') + } + } if in.All { const prefix string = ",\"all\":" out.RawString(prefix) @@ -2623,15 +2671,15 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingApi21(in *j for !in.IsDelim('}') { key := string(in.String()) in.WantColon() - var v26 interface{} - if m, ok := v26.(easyjson.Unmarshaler); ok { + var v29 interface{} + if m, ok := v29.(easyjson.Unmarshaler); ok { m.UnmarshalEasyJSON(in) - } else if m, ok := v26.(json.Unmarshaler); ok { + } else if m, ok := v29.(json.Unmarshaler); ok { _ = m.UnmarshalJSON(in.Raw()) } else { - v26 = in.Interface() + v29 = in.Interface() } - (out.Payload)[key] = v26 + (out.Payload)[key] = v29 in.WantComma() } in.Delim('}') @@ -2702,21 +2750,21 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingApi21(out * out.RawString(`null`) } else { out.RawByte('{') - v27First := true - for v27Name, v27Value := range in.Payload { - if v27First { - v27First = false + v30First := true + for v30Name, v30Value := range in.Payload { + if v30First { + v30First = false } else { out.RawByte(',') } - out.String(string(v27Name)) + out.String(string(v30Name)) out.RawByte(':') - if m, ok := v27Value.(easyjson.Marshaler); ok { + if m, ok := v30Value.(easyjson.Marshaler); ok { m.MarshalEasyJSON(out) - } else if m, ok := v27Value.(json.Marshaler); ok { + } else if m, ok := v30Value.(json.Marshaler); ok { out.Raw(m.MarshalJSON()) } else { - out.Raw(json.Marshal(v27Value)) + out.Raw(json.Marshal(v30Value)) } } out.RawByte('}') @@ -3868,13 +3916,13 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingApi32(in *j out.Features = (out.Features)[:0] } for !in.IsDelim(']') { - var v28 string + var v31 string if in.IsNull() { in.Skip() } else { - v28 = string(in.String()) + v31 = string(in.String()) } - out.Features = append(out.Features, v28) + out.Features = append(out.Features, v31) in.WantComma() } in.Delim(']') @@ -3922,11 +3970,11 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingApi32(out * out.RawString(prefix) { out.RawByte('[') - for v29, v30 := range in.Features { - if v29 > 0 { + for v32, v33 := range in.Features { + if v32 > 0 { out.RawByte(',') } - out.String(string(v30)) + out.String(string(v33)) } out.RawByte(']') } @@ -4359,13 +4407,13 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingApi36(in *j out.Features = (out.Features)[:0] } for !in.IsDelim(']') { - var v31 string + var v34 string if in.IsNull() { in.Skip() } else { - v31 = string(in.String()) + v34 = string(in.String()) } - out.Features = append(out.Features, v31) + out.Features = append(out.Features, v34) in.WantComma() } in.Delim(']') @@ -4419,11 +4467,11 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingApi36(out * out.RawString(prefix) { out.RawByte('[') - for v32, v33 := range in.Features { - if v32 > 0 { + for v35, v36 := range in.Features { + if v35 > 0 { out.RawByte(',') } - out.String(string(v33)) + out.String(string(v36)) } out.RawByte(']') } @@ -4511,13 +4559,13 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingApi37(in *j out.Join = (out.Join)[:0] } for !in.IsDelim(']') { - var v34 EventServerMessageSessionEntry + var v37 EventServerMessageSessionEntry if in.IsNull() { in.Skip() } else { - (v34).UnmarshalEasyJSON(in) + (v37).UnmarshalEasyJSON(in) } - out.Join = append(out.Join, v34) + out.Join = append(out.Join, v37) in.WantComma() } in.Delim(']') @@ -4538,13 +4586,13 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingApi37(in *j out.Leave = (out.Leave)[:0] } for !in.IsDelim(']') { - var v35 PublicSessionId + var v38 PublicSessionId if in.IsNull() { in.Skip() } else { - v35 = PublicSessionId(in.String()) + v38 = PublicSessionId(in.String()) } - out.Leave = append(out.Leave, v35) + out.Leave = append(out.Leave, v38) in.WantComma() } in.Delim(']') @@ -4565,13 +4613,13 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingApi37(in *j out.Change = (out.Change)[:0] } for !in.IsDelim(']') { - var v36 EventServerMessageSessionEntry + var v39 EventServerMessageSessionEntry if in.IsNull() { in.Skip() } else { - (v36).UnmarshalEasyJSON(in) + (v39).UnmarshalEasyJSON(in) } - out.Change = append(out.Change, v36) + out.Change = append(out.Change, v39) in.WantComma() } in.Delim(']') @@ -4703,11 +4751,11 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingApi37(out * out.RawString(prefix) { out.RawByte('[') - for v37, v38 := range in.Join { - if v37 > 0 { + for v40, v41 := range in.Join { + if v40 > 0 { out.RawByte(',') } - (v38).MarshalEasyJSON(out) + (v41).MarshalEasyJSON(out) } out.RawByte(']') } @@ -4717,11 +4765,11 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingApi37(out * out.RawString(prefix) { out.RawByte('[') - for v39, v40 := range in.Leave { - if v39 > 0 { + for v42, v43 := range in.Leave { + if v42 > 0 { out.RawByte(',') } - out.String(string(v40)) + out.String(string(v43)) } out.RawByte(']') } @@ -4731,11 +4779,11 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingApi37(out * out.RawString(prefix) { out.RawByte('[') - for v41, v42 := range in.Change { - if v41 > 0 { + for v44, v45 := range in.Change { + if v44 > 0 { out.RawByte(',') } - (v42).MarshalEasyJSON(out) + (v45).MarshalEasyJSON(out) } out.RawByte(']') } @@ -5844,15 +5892,15 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingApi48(in *j for !in.IsDelim('}') { key := string(in.String()) in.WantColon() - var v43 interface{} - if m, ok := v43.(easyjson.Unmarshaler); ok { + var v46 interface{} + if m, ok := v46.(easyjson.Unmarshaler); ok { m.UnmarshalEasyJSON(in) - } else if m, ok := v43.(json.Unmarshaler); ok { + } else if m, ok := v46.(json.Unmarshaler); ok { _ = m.UnmarshalJSON(in.Raw()) } else { - v43 = in.Interface() + v46 = in.Interface() } - (out.Payload)[key] = v43 + (out.Payload)[key] = v46 in.WantComma() } in.Delim('}') @@ -5904,21 +5952,21 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingApi48(out * out.RawString(`null`) } else { out.RawByte('{') - v44First := true - for v44Name, v44Value := range in.Payload { - if v44First { - v44First = false + v47First := true + for v47Name, v47Value := range in.Payload { + if v47First { + v47First = false } else { out.RawByte(',') } - out.String(string(v44Name)) + out.String(string(v47Name)) out.RawByte(':') - if m, ok := v44Value.(easyjson.Marshaler); ok { + if m, ok := v47Value.(easyjson.Marshaler); ok { m.MarshalEasyJSON(out) - } else if m, ok := v44Value.(json.Marshaler); ok { + } else if m, ok := v47Value.(json.Marshaler); ok { out.Raw(m.MarshalJSON()) } else { - out.Raw(json.Marshal(v44Value)) + out.Raw(json.Marshal(v47Value)) } } out.RawByte('}') From 15d734d77b0c7534e0330ceb22f838a0ee2b5854 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 29 Jan 2026 11:31:56 +0100 Subject: [PATCH 3/3] Document sending multiple chat messages from Talk. --- docs/standalone-signaling-api-v1.md | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/standalone-signaling-api-v1.md b/docs/standalone-signaling-api-v1.md index e961dfb..e6c5d17 100644 --- a/docs/standalone-signaling-api-v1.md +++ b/docs/standalone-signaling-api-v1.md @@ -1867,6 +1867,36 @@ Message format (Backend -> Server) } +The signaling server also supports combining multiple chat comments into one +request which will then be sent out individually to clients. + +Message format (Backend -> Server) + + { + "type": "message" + "message" { + "data": { + "type": "chat", + "chat": { + "refresh": true, + "comments": [ + { + ...properties of the first comment written in the chat... + }, + { + ...properties of the second comment written in the chat... + } + ] + } + } + } + } + +In this case, clients will either receive a single `"refresh": true` message +(if they don't support `chat-relay`) or multiple messages with the different +comments. + + ### Notify sessions to switch to a different room This can be used to let sessions in a room know that they switch to a different