-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathLiveKitChatMessagesBus.cs
More file actions
282 lines (232 loc) · 13.4 KB
/
Copy pathLiveKitChatMessagesBus.cs
File metadata and controls
282 lines (232 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
using Cysharp.Threading.Tasks;
using DCL.Chat.History;
using DCL.Chat.MessageBus.Deduplication;
using DCL.Communities;
using DCL.Diagnostics;
using DCL.FeatureFlags;
using DCL.Friends.UserBlocking;
using DCL.Multiplayer.Connections.DecentralandUrls;
using DCL.Multiplayer.Connections.Messaging;
using DCL.Multiplayer.Connections.Messaging.Hubs;
using DCL.Multiplayer.Connections.Messaging.Pipe;
using DCL.Multiplayer.Connections.RoomHubs;
using DCL.Multiplayer.Deduplication;
using DCL.SceneBannedUsers;
using DCL.Web3;
using DCL.Web3.Identities;
using Decentraland.Kernel.Comms.Rfc4;
using DCL.LiveKit.Public;
using LiveKit.Rooms;
using System;
using System.Threading;
using Utility;
using ChatMessage = DCL.Chat.History.ChatMessage;
namespace DCL.Chat.MessageBus
{
public class LiveKitChatMessagesBus : IChatMessagesBus
{
// Hard ceiling on the dedup stamps retained per period. A flood of distinct timestamps
// restarts the window instead of growing it, bounding the memory a sender can make this
// cache hold.
private const int MAX_DEDUP_ENTRIES = 2048;
private readonly IMessagePipesHub messagePipesHub;
private readonly IMessageDeduplication<double> messageDeduplication;
private readonly CancellationTokenSource cancellationTokenSource = new ();
private readonly IUserBlockingCache userBlockingCache;
private readonly IWeb3IdentityCache identityCache;
private readonly ChatMessageFactory messageFactory;
private readonly ChatMessageRateLimiter? messageRateLimiter;
private readonly ChatChannelMessageBuffer? nearbyChannelBuffer;
private readonly string routingUser;
private readonly CancellationTokenSource setupExploreSectionsCts = new ();
private readonly bool isChatMessageRateLimiterEnabled;
private readonly bool isNearbyChannelBufferEnabled;
private readonly bool isPrivateChatRequiresTopicEnabled;
private bool isCommunitiesIncluded;
public event Action<ChatChannel.ChannelId, ChatChannel.ChatChannelType, ChatMessage>? MessageAdded;
public LiveKitChatMessagesBus(IMessagePipesHub messagePipesHub,
ChatMessageFactory messageFactory,
IUserBlockingCache userBlockingCache,
DecentralandEnvironment decentralandEnvironment,
IWeb3IdentityCache identityCache,
IRoomHub roomHub)
{
this.messagePipesHub = messagePipesHub;
messageDeduplication = new MessageDeduplication<double>(MAX_DEDUP_ENTRIES);
this.userBlockingCache = userBlockingCache;
this.identityCache = identityCache;
this.messageFactory = messageFactory;
isChatMessageRateLimiterEnabled = FeaturesRegistry.Instance.IsEnabled(FeatureId.ChatMessageRateLimit);
if (isChatMessageRateLimiterEnabled)
{
messageRateLimiter = new ChatMessageRateLimiter();
messageRateLimiter.LoadConfigurationFromFeatureFlag();
}
isNearbyChannelBufferEnabled = FeaturesRegistry.Instance.IsEnabled(FeatureId.ChatMessageBuffer);
if (isNearbyChannelBufferEnabled)
{
nearbyChannelBuffer = new ChatChannelMessageBuffer();
nearbyChannelBuffer.MessageReleased += OnBufferedMessageReleased;
roomHub.IslandRoom().ConnectionUpdated += OnIslandConnectionUpdated;
}
isPrivateChatRequiresTopicEnabled = FeaturesRegistry.Instance.IsEnabled(FeatureId.PrivateChatRequiresTopic);
identityCache.OnIdentityCleared += OnIdentityCleared;
// Depending on the selected environment, we send the community messages to one user or another
string serverEnv = decentralandEnvironment switch
{
DecentralandEnvironment.Org => "prd",
DecentralandEnvironment.Today => "prd",
DecentralandEnvironment.Zone => "dev",
_ => "local",
};
// Must match the participant identity that comms-message-sfu joins the room under: it is the
// only signal that authenticates a relayed message's ForwardedFrom stamp. If the two ever
// diverge, relayed community messages are attributed to the router instead of their sender.
routingUser = $"message-router-{serverEnv}-0";
ConfigureMessagePipesHubAsync(setupExploreSectionsCts.Token).Forget();
}
public void Dispose()
{
cancellationTokenSource.SafeCancelAndDispose();
setupExploreSectionsCts.SafeCancelAndDispose();
nearbyChannelBuffer?.Dispose();
}
private void OnIslandConnectionUpdated(IRoom room, ConnectionUpdate connectionUpdate, LKDisconnectReason? disconnectReason)
{
//We clear the buffer if we disconnect from the island, so we won't keep receiving messages from that nearby area.
if (connectionUpdate == ConnectionUpdate.Disconnected && disconnectReason == LKDisconnectReason.UnknownReason)
nearbyChannelBuffer!.Reset();
}
private async UniTaskVoid ConfigureMessagePipesHubAsync(CancellationToken ct)
{
isCommunitiesIncluded = await CommunitiesFeatureAccess.Instance.IsUserAllowedToUseTheFeatureAsync(ct);
messagePipesHub.IslandPipe().Subscribe<Decentraland.Kernel.Comms.Rfc4.Chat>(Packet.MessageOneofCase.Chat, HandleNearbyPipesMessage);
messagePipesHub.ScenePipe().Subscribe<Decentraland.Kernel.Comms.Rfc4.Chat>(Packet.MessageOneofCase.Chat, HandleNearbyPipesMessage);
messagePipesHub.ChatPipe().Subscribe<Decentraland.Kernel.Comms.Rfc4.Chat>(Packet.MessageOneofCase.Chat, HandleChatPipeMessage);
nearbyChannelBuffer?.Start(cancellationTokenSource.Token);
}
private void HandleNearbyPipesMessage(ReceivedMessage<Decentraland.Kernel.Comms.Rfc4.Chat> receivedMessage)
{
using (receivedMessage)
{
// The island and scene pipes are peer-to-peer, so the message router is never in their
// path and a populated ForwardedFrom can only have been set by the publishing peer.
// Keying on it would let any peer publish under an arbitrary wallet and rotate the
// value to get a fresh slot for each of the checks below.
string walletId = receivedMessage.FromWalletId;
// If the user that sends the message is banned from the current scene, we ignore it
if (RoomMetadataCurrentScene.Instance.IsUserBanned(walletId)) return;
// If the message was already received through the scene or island pipe, we ignore it
if (messageDeduplication.TryPass(walletId, receivedMessage.Payload.Timestamp) == false) return;
if (!TryCreateMessage(receivedMessage, walletId, out ChatMessage message)) return;
if (!isNearbyChannelBufferEnabled)
{
MessageAdded?.Invoke(ChatChannel.NEARBY_CHANNEL_ID, ChatChannel.ChatChannelType.NEARBY, message);
return;
}
if (!nearbyChannelBuffer!.TryEnqueue(message))
ReportHub.Log(ReportCategory.CHAT_MESSAGES, "Failed to enqueue message!");
}
}
private void HandleChatPipeMessage(ReceivedMessage<Decentraland.Kernel.Comms.Rfc4.Chat> receivedMessage)
{
using (receivedMessage)
{
if (string.IsNullOrEmpty(receivedMessage.Topic)) return;
ChatChannel.ChannelId parsedChannelId;
ChatChannel.ChatChannelType channelType;
if (ChatChannel.IsCommunityChannelId(receivedMessage.Topic))
{
// If the Communities shape is disabled, ignores the messages
if (!isCommunitiesIncluded)
return;
parsedChannelId = new ChatChannel.ChannelId(receivedMessage.Topic);
channelType = ChatChannel.ChatChannelType.COMMUNITY;
}
else if (!isPrivateChatRequiresTopicEnabled || string.Equals(receivedMessage.Topic, identityCache.Identity?.Address, StringComparison.InvariantCultureIgnoreCase))
{
parsedChannelId = new ChatChannel.ChannelId(receivedMessage.FromWalletId);
channelType = ChatChannel.ChatChannelType.USER;
}
else
{
ReportHub.LogWarning(ReportCategory.CHAT_MESSAGES, $"Received a Message with incorrect Topic {receivedMessage.Topic}");
return;
}
string walletId = ResolveChatPipeSenderWalletId(receivedMessage);
if (TryCreateMessage(receivedMessage, walletId, out ChatMessage newMessage))
MessageAdded?.Invoke(parsedChannelId, channelType, newMessage);
}
}
/// <summary>
/// Resolves the author of a message received on the chat pipe.
/// </summary>
/// <remarks>
/// ForwardedFrom is stamped only by the trusted message router, which relays community messages
/// under the <see cref="routingUser"/> identity. Honoring it from any other sender would let a
/// peer publish under an arbitrary wallet, since the field is an ordinary payload string while
/// FromWalletId is the LiveKit-authenticated participant identity.
/// </remarks>
private string ResolveChatPipeSenderWalletId(ReceivedMessage<Decentraland.Kernel.Comms.Rfc4.Chat> receivedMessage)
{
if (receivedMessage.Payload.HasForwardedFrom
&& string.Equals(receivedMessage.FromWalletId, routingUser, StringComparison.OrdinalIgnoreCase)
&& Web3Address.IsValidWalletAddress(receivedMessage.Payload.ForwardedFrom))
return receivedMessage.Payload.ForwardedFrom;
return receivedMessage.FromWalletId;
}
private bool TryCreateMessage(ReceivedMessage<Decentraland.Kernel.Comms.Rfc4.Chat> receivedMessage, string senderWalletId, out ChatMessage newMessage)
{
newMessage = default(ChatMessage);
if (IsUserBlockedAndMessagesHidden(senderWalletId)) return false;
if (isChatMessageRateLimiterEnabled && !messageRateLimiter!.TryAllow(senderWalletId)) return false;
newMessage = messageFactory.CreateChatMessage(senderWalletId, false, receivedMessage.Payload.Message, null, receivedMessage.Payload.Timestamp);
ReportHub.Log(ReportCategory.CHAT_MESSAGES, $"[ChatMessageBus] RECEIVED message: protoTimestamp={receivedMessage.Payload.Timestamp} messageId={newMessage.MessageId} from={senderWalletId}");
return true;
}
private bool IsUserBlockedAndMessagesHidden(string walletAddress) =>
userBlockingCache.HideChatMessages && userBlockingCache.UserIsBlocked(walletAddress);
private void OnBufferedMessageReleased(ChatMessage message)
{
MessageAdded?.Invoke(ChatChannel.NEARBY_CHANNEL_ID, ChatChannel.ChatChannelType.NEARBY, message);
}
private void OnIdentityCleared()
{
nearbyChannelBuffer?.Reset();
}
public void Send(ChatChannel channel, string message, ChatMessageOrigin origin, double timestamp)
{
if (cancellationTokenSource.IsCancellationRequested)
throw new Exception("ChatMessagesBus is disposed");
switch (channel.ChannelType)
{
case ChatChannel.ChatChannelType.NEARBY:
SendTo(message, timestamp, messagePipesHub.IslandPipe());
SendTo(message, timestamp, messagePipesHub.ScenePipe());
break;
case ChatChannel.ChatChannelType.USER:
SendTo(message, timestamp, channel.Id.Id, messagePipesHub.ChatPipe(), channel.Id.Id);
break;
case ChatChannel.ChatChannelType.COMMUNITY:
SendTo(message, timestamp, channel.Id.Id, messagePipesHub.ChatPipe(), routingUser);
break;
}
}
private void SendTo(string message, double timestamp, IMessagePipe messagePipe, string? recipient = null)
{
SendTo(message, timestamp, string.Empty, messagePipe, recipient);
}
private void SendTo(string message, double timestamp, string topic, IMessagePipe messagePipe, string? recipient = null)
{
MessageWrap<Decentraland.Kernel.Comms.Rfc4.Chat> chat = messagePipe.NewMessage<Decentraland.Kernel.Comms.Rfc4.Chat>(topic);
if (recipient != null)
chat.AddSpecialRecipient(recipient);
chat.Payload.ClearForwardedFrom(); // It has to be reset in every use. To be filled by the server.
chat.Payload.Message = message;
chat.Payload.Timestamp = timestamp;
string msgId = ChatUtils.GetId(identityCache.Identity?.Address ?? "", timestamp);
ReportHub.Log(ReportCategory.CHAT_MESSAGES, $"[ChatMessageBus] SENT message: timestamp={timestamp} messageId={msgId}");
chat.SendAndDisposeAsync(cancellationTokenSource.Token, LKDataPacketKind.KindReliable).Forget();
}
}
}