diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 0000000..39adb81 --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,8 @@ +--- +exclude_paths: + - '**/PubNubChatConsoleExample/**' + - '**/PubNubChatApi.Tests/**' + - '**/Plugins/**' + - '**/Snippets/**' + - '**/DiffMatchPatch.cs' + \ No newline at end of file diff --git a/.github/workflows/release/versions.json b/.github/workflows/release/versions.json index 5f5a369..f9379ab 100644 --- a/.github/workflows/release/versions.json +++ b/.github/workflows/release/versions.json @@ -21,5 +21,11 @@ "clearedPrefix": true, "clearedSuffix": false } + ], + "unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChatPNSDKSource.cs": [ + { + "pattern": "^\\s{2,}private const string build = \"(v?(\\.?\\d+){2,}([a-zA-Z0-9-]+(\\.?\\d+)?)?)\";", + "cleared": true + } ] } diff --git a/.pubnub.yml b/.pubnub.yml index 753356f..4fc5d2b 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,6 +1,15 @@ --- -version: v0.4.4 +version: v1.0.0 changelog: + - date: 2025-10-02 + version: v1.0.0 + changes: + - type: feature + text: "Added WebGL build support because of the migration from C-Core to Unity SDK." + - type: improvement + text: "Changed the underlying SDK used by the Chat from C-Core to Unity SDK." + - type: improvement + text: "Added ChatOperationResult and ChatOperationResult as a return types for async network operations with additional debug and error data on potential failure." - date: 2025-05-12 version: v0.4.4 changes: diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs index 747fc70..052b773 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -using PubNubChatAPI.Entities; -using PubnubChatApi.Entities.Data; +using PubnubApi; +using PubnubChatApi; namespace PubNubChatApi.Tests; @@ -14,19 +14,16 @@ public class ChannelTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "ctuuuuuuuuuuuuuuuuuuuuuuuuuuuuu") - ); - if (!chat.TryGetCurrentUser(out user)) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("ctuuuuuuuuuuuuuuuuuuuuuuuuuuuuu")) { - Assert.Fail(); - } - await user.Update(new ChatUserData() + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); + user = TestUtils.AssertOperation(await chat.GetCurrentUser()); + TestUtils.AssertOperation(await user.Update(new ChatUserData() { Username = "Testificate" - }); + })); talkUser = await chat.GetOrCreateUser("talk_user"); } @@ -40,7 +37,7 @@ public async Task CleanUp() [Test] public async Task TestUpdateChannel() { - var channel = await chat.CreatePublicConversation(); + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); channel.SetListeningForUpdates(true); await Task.Delay(3000); @@ -48,22 +45,22 @@ public async Task TestUpdateChannel() var updateReset = new ManualResetEvent(false); var updatedData = new ChatChannelData() { - ChannelDescription = "some description", - ChannelCustomDataJson = "{\"key\":\"value\"}", - ChannelName = "some name", - ChannelStatus = "yes", - ChannelType = "sometype" + Description = "some description", + CustomData = new Dictionary(){{"key", "value"}}, + Name = "some name", + Status = "yes", + Type = "sometype" }; channel.OnChannelUpdate += updatedChannel => { - Assert.True(updatedChannel.Description == updatedData.ChannelDescription, "updatedChannel.Description != updatedData.ChannelDescription"); - Assert.True(updatedChannel.CustomDataJson == updatedData.ChannelCustomDataJson, "updatedChannel.CustomDataJson != updatedData.ChannelCustomDataJson"); - Assert.True(updatedChannel.Name == updatedData.ChannelName, "updatedChannel.Name != updatedData.ChannelDescription"); - Assert.True(updatedChannel.Status == updatedData.ChannelStatus, "updatedChannel.Status != updatedData.ChannelStatus"); - Assert.True(updatedChannel.Type == updatedData.ChannelType, "updatedChannel.Type != updatedData.ChannelType"); + Assert.True(updatedChannel.Description == updatedData.Description, "updatedChannel.Description != updatedData.ChannelDescription"); + Assert.True(updatedChannel.CustomData.TryGetValue("key", out var value) && value.ToString() == "value", "updatedChannel.CustomDataJson != updatedData.ChannelCustomDataJson"); + Assert.True(updatedChannel.Name == updatedData.Name, "updatedChannel.Name != updatedData.ChannelDescription"); + Assert.True(updatedChannel.Status == updatedData.Status, "updatedChannel.Status != updatedData.ChannelStatus"); + Assert.True(updatedChannel.Type == updatedData.Type, "updatedChannel.Type != updatedData.ChannelType"); updateReset.Set(); }; - await channel.Update(updatedData); + TestUtils.AssertOperation(await channel.Update(updatedData)); var updated = updateReset.WaitOne(15000); Assert.True(updated); } @@ -71,32 +68,34 @@ public async Task TestUpdateChannel() [Test] public async Task TestDeleteChannel() { - var channel = await chat.CreatePublicConversation(); + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); await Task.Delay(3000); - Assert.True(chat.TryGetChannel(channel.Id, out _), "Couldn't fetch created channel from chat"); - + var channelExists = await chat.GetChannel(channel.Id); + Assert.False(channelExists.Error, "Couldn't fetch created channel from chat"); + await channel.Delete(); await Task.Delay(3000); - Assert.False(chat.TryGetChannel(channel.Id, out _), "Fetched the supposedly-deleted channel from chat"); + var channelAfterDelete = await chat.GetChannel(channel.Id); + Assert.True(channelAfterDelete.Error, "Fetched the supposedly-deleted channel from chat"); } [Test] public async Task TestLeaveChannel() { - var currentChatUser = await chat.GetCurrentUserAsync(); + var currentChatUser = TestUtils.AssertOperation(await chat.GetCurrentUser()); Assert.IsNotNull(currentChatUser, "currentChatUser was null"); - var channel = await chat.CreatePublicConversation(); + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); channel.Join(); await Task.Delay(3000); - var memberships = await channel.GetMemberships(); + var memberships = TestUtils.AssertOperation(await channel.GetMemberships()); Assert.True(memberships.Memberships.Any(x => x.UserId == currentChatUser.Id), "Join failed, current user not found in channel memberships"); @@ -104,37 +103,48 @@ public async Task TestLeaveChannel() await Task.Delay(3000); - memberships = await channel.GetMemberships(); + memberships = TestUtils.AssertOperation(await channel.GetMemberships()); Assert.False(memberships.Memberships.Any(x => x.UserId == currentChatUser.Id), "Leave failed, current user found in channel memberships"); } - + [Test] - public async Task TestGetUserSuggestions() + public async Task TestGetMessagesHistory() { - var channel = await chat.CreatePublicConversation("user_suggestions_test_channel"); + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); + channel.OnMessageReceived += async message => + { + TestUtils.AssertOperation(await message.EditMessageText("some_new_text")); + }; channel.Join(); + await Task.Delay(3500); + TestUtils.AssertOperation(await channel.SendText("wololo")); + + await Task.Delay(10000); - await Task.Delay(5000); + var history = + TestUtils.AssertOperation(await channel.GetMessageHistory("99999999999999999", "00000000000000000", 1)); - var suggestions = await channel.GetUserSuggestions("@Test"); - Assert.True(suggestions.Any(x => x.UserId == user.Id)); + Assert.True(history != null, "history was null null"); + Assert.True(history.Count == 1, "history count was wrong"); + Assert.True(history[0].OriginalMessageText == "wololo", "message from history had wrong original text"); + Assert.True(history[0].MessageText == "some_new_text", "message from history had wrong text"); } [Test] public async Task TestGetMemberships() { - var channel = await chat.CreatePublicConversation("get_members_test_channel"); + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("get_members_test_channel")); channel.Join(); await Task.Delay(3500); - var memberships = await channel.GetMemberships(); + var memberships = TestUtils.AssertOperation(await channel.GetMemberships()); Assert.That(memberships.Memberships.Count, Is.GreaterThanOrEqualTo(1)); } [Test] public async Task TestStartTyping() { - var channel = (await chat.CreateDirectConversation(talkUser, "sttc")).CreatedChannel; + var channel = TestUtils.AssertOperation(await chat.CreateDirectConversation(talkUser, "sttc")).CreatedChannel; channel.Join(); await Task.Delay(2500); channel.SetListeningForTyping(true); @@ -147,7 +157,7 @@ public async Task TestStartTyping() Assert.That(typingUsers, Does.Contain(user.Id)); typingManualEvent.Set(); }; - await channel.StartTyping(); + TestUtils.AssertOperation(await channel.StartTyping()); var receivedTyping = typingManualEvent.WaitOne(12000); Assert.IsTrue(receivedTyping); @@ -156,13 +166,13 @@ public async Task TestStartTyping() [Test] public async Task TestStopTyping() { - var channel = (await chat.CreateDirectConversation(talkUser, "stop_typing_test_channel")).CreatedChannel; + var channel = TestUtils.AssertOperation(await chat.CreateDirectConversation(talkUser, "stop_typing_test_channel")).CreatedChannel; channel.Join(); await Task.Delay(2500); channel.SetListeningForTyping(true); await Task.Delay(2500); - await channel.StartTyping(); + TestUtils.AssertOperation(await channel.StartTyping()); await Task.Delay(2500); @@ -172,7 +182,7 @@ public async Task TestStopTyping() Assert.That(typingUsers, Is.Empty); typingManualEvent.Set(); }; - await channel.StopTyping(); + TestUtils.AssertOperation(await channel.StopTyping()); var typingEvent = typingManualEvent.WaitOne(6000); Assert.IsTrue(typingEvent); @@ -181,14 +191,14 @@ public async Task TestStopTyping() [Test] public async Task TestStopTypingFromTimer() { - var channel = (await chat.CreateDirectConversation(talkUser, "stop_typing_timeout_test_channel")).CreatedChannel; + var channel = TestUtils.AssertOperation(await chat.CreateDirectConversation(talkUser, "stop_typing_timeout_test_channel")).CreatedChannel; channel.Join(); await Task.Delay(2500); channel.SetListeningForTyping(true); await Task.Delay(4500); - await channel.StartTyping(); + TestUtils.AssertOperation(await channel.StartTyping()); await Task.Delay(3000); @@ -206,7 +216,7 @@ public async Task TestStopTypingFromTimer() [Test] public async Task TestPinMessage() { - var channel = await chat.CreatePublicConversation("pin_message_test_channel_37"); + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("pin_message_test_channel_37")); channel.Join(); await Task.Delay(3500); @@ -217,11 +227,12 @@ public async Task TestPinMessage() await Task.Delay(4000); - await channel.PinMessage(message); + TestUtils.AssertOperation(await channel.PinMessage(message)); await Task.Delay(2000); - Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.MessageText == "message to pin"); + var pinned = TestUtils.AssertOperation(await channel.GetPinnedMessage()); + Assert.True(pinned.MessageText == "message to pin"); receivedManualEvent.Set(); }; await channel.SendText("message to pin"); @@ -233,34 +244,36 @@ public async Task TestPinMessage() [Test] public async Task TestUnPinMessage() { - var channel = await chat.CreatePublicConversation("unpin_message_test_channel"); + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("unpin_message_test_channel")); channel.Join(); await Task.Delay(3500); var receivedManualEvent = new ManualResetEvent(false); channel.OnMessageReceived += async message => { - await channel.PinMessage(message); + TestUtils.AssertOperation(await channel.PinMessage(message)); await Task.Delay(2000); + + var pinned = TestUtils.AssertOperation(await channel.GetPinnedMessage()); + Assert.True(pinned.MessageText == "message to pin"); + TestUtils.AssertOperation(await channel.UnpinMessage()); - Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.MessageText == "message to pin"); - await channel.UnpinMessage(); - - await Task.Delay(2000); - - Assert.False(channel.TryGetPinnedMessage(out _)); + await Task.Delay(15000); + + var getPinned = await channel.GetPinnedMessage(); + Assert.True(getPinned.Error); receivedManualEvent.Set(); }; await channel.SendText("message to pin"); - var received = receivedManualEvent.WaitOne(12000); + var received = receivedManualEvent.WaitOne(35000); Assert.IsTrue(received); } [Test] public async Task TestCreateMessageDraft() { - var channel = await chat.CreatePublicConversation("message_draft_test_channel"); + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("message_draft_test_channel")); try { var draft = channel.CreateMessageDraft(); @@ -275,7 +288,7 @@ public async Task TestCreateMessageDraft() [Test] public async Task TestEmitUserMention() { - var channel = await chat.CreatePublicConversation("user_mention_test_channel"); + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("user_mention_test_channel")); channel.Join(); await Task.Delay(2500); var receivedManualEvent = new ManualResetEvent(false); @@ -294,12 +307,12 @@ public async Task TestEmitUserMention() [Test] public async Task TestChannelIsPresent() { - var someChannel = await chat.CreatePublicConversation(); + var someChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); someChannel.Join(); await Task.Delay(4000); - var isPresent = await someChannel.IsUserPresent(user.Id); + var isPresent = TestUtils.AssertOperation(await someChannel.IsUserPresent(user.Id)); Assert.True(isPresent, "someChannel.IsUserPresent() doesn't return true for most recently joined channel!"); } @@ -307,13 +320,58 @@ public async Task TestChannelIsPresent() [Test] public async Task TestChannelWhoIsPresent() { - var someChannel = await chat.CreatePublicConversation(); + var someChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); someChannel.Join(); await Task.Delay(4000); - var who = await someChannel.WhoIsPresent(); + var who = TestUtils.AssertOperation(await someChannel.WhoIsPresent()); Assert.Contains(user.Id, who, "channel.WhoIsPresent() doesn't have most recently joine user!"); } + + [Test] + public async Task TestPresenceCallback() + { + var someChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); + someChannel.SetListeningForPresence(true); + + var reset = new ManualResetEvent(false); + someChannel.OnPresenceUpdate += userIds => + { + Assert.True(userIds.Contains(user.Id), "presence callback doesn't contain joined user id"); + reset.Set(); + }; + someChannel.Join(); + var presenceReceived = reset.WaitOne(12000); + + Assert.True(presenceReceived, "did not receive presence callback"); + } + + [Test] + public async Task TestReportCallback() + { + var someChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); + someChannel.SetListeningForReportEvents(true); + var reset = new ManualResetEvent(false); + someChannel.OnReportEvent += reportEvent => + { + var data = chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(reportEvent.Payload); + Assert.True(data.TryGetValue("reason", out var reason) && reason.ToString() == "some_reason", "incorrect report reason received"); + reset.Set(); + }; + + someChannel.Join(); + await Task.Delay(3000); + + someChannel.OnMessageReceived += async message => + { + await message.Report("some_reason"); + }; + await someChannel.SendText("message_to_be_reported"); + + var reportReceived = reset.WaitOne(12000); + + Assert.True(reportReceived, "did not receive report callback"); + } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs index 102fc0a..fce6b53 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs @@ -1,6 +1,6 @@ -using System.Diagnostics; -using PubNubChatAPI.Entities; -using PubnubChatApi.Entities.Data; +using PubnubApi; +using PubnubChatApi; +using Channel = PubnubChatApi.Channel; namespace PubNubChatApi.Tests; @@ -14,16 +14,13 @@ public class ChatEventTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "event_tests_user") - ); - channel = await chat.CreatePublicConversation("event_tests_channel"); - if (!chat.TryGetCurrentUser(out user)) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("event_tests_user")) { - Assert.Fail(); - } + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("event_tests_channel")); + user = TestUtils.AssertOperation(await chat.GetCurrentUser()); channel.Join(); await Task.Delay(3500); } diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs index 4a0f187..ed9dd30 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -1,7 +1,6 @@ -using System.Diagnostics; -using PubNubChatAPI.Entities; -using PubnubChatApi.Entities.Data; -using PubnubChatApi.Enums; +using PubnubApi; +using PubnubChatApi; +using Channel = PubnubChatApi.Channel; namespace PubNubChatApi.Tests; @@ -14,15 +13,14 @@ public class ChatTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "chats_tests_user_10_no_calkiem_nowy_2")); - channel = await chat.CreatePublicConversation("chat_tests_channel_2"); - if (!chat.TryGetCurrentUser(out currentUser)) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), + new PNConfiguration(new UserId("chats_tests_user_fresh_3")) { - Assert.Fail(); - } + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("chat_tests_channel_2")); + currentUser = TestUtils.AssertOperation(await chat.GetCurrentUser()); channel.Join(); await Task.Delay(3500); } @@ -42,15 +40,19 @@ public async Task TestGetCurrentUserMentions() var messageContent = "wololo"; await channel.SendText(messageContent, new SendTextParams() { - MentionedUsers = new Dictionary() + MentionedUsers = new Dictionary() { - {0, currentUser} + {0, new MentionedUser() + { + Id = currentUser.Id, + Name = currentUser.UserName + }} } }); await Task.Delay(3000); - var mentions = await chat.GetCurrentUserMentions("99999999999999999", "00000000000000000", 10); + var mentions = TestUtils.AssertOperation(await chat.GetCurrentUserMentions("99999999999999999", "00000000000000000", 10)); Assert.True(mentions != null); Assert.True(mentions.Mentions.Any(x => x.ChannelId == channel.Id && x.Message.MessageText == messageContent)); @@ -59,24 +61,28 @@ public async Task TestGetCurrentUserMentions() [Test] public async Task TestGetCurrentUser() { - Assert.True(chat.TryGetCurrentUser(out var currentUser) && currentUser.Id == this.currentUser.Id); + var fetchedCurrentUser = TestUtils.AssertOperation(await chat.GetCurrentUser()); + Assert.True(fetchedCurrentUser.Id == currentUser.Id); } [Test] public async Task TestGetEventHistory() { - await chat.EmitEvent(PubnubChatEventType.Custom, channel.Id, "{\"test\":\"some_nonsense\"}"); + var testChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); + await chat.EmitEvent(PubnubChatEventType.Custom, testChannel.Id, "{\"test\":\"some_nonsense\"}"); await Task.Delay(5000); - var history = await chat.GetEventsHistory(channel.Id, "99999999999999999", "00000000000000000", 50); - Assert.True(history.Events.Any(x => x.ChannelId == channel.Id)); + var history = TestUtils.AssertOperation( + await chat.GetEventsHistory(testChannel.Id, "99999999999999999", "00000000000000000", 50)); + Assert.True(history.Events.Any(x => x.ChannelId == testChannel.Id && x.Payload.Contains("\"test\":\"some_nonsense\"")), + "Emitted event wasn't present in events history"); } [Test] public async Task TestGetUsers() { - var users = await chat.GetUsers(); + var users = TestUtils.AssertOperation(await chat.GetUsers()); Assert.True(users.Users.Any()); } @@ -92,12 +98,16 @@ public async Task TestGetChannels() public async Task TestCreateDirectConversation() { var convoUser = await chat.GetOrCreateUser("direct_conversation_user"); - var directConversation = - await chat.CreateDirectConversation(convoUser, "direct_conversation_test"); - Assert.True(directConversation.CreatedChannel is { Id: "direct_conversation_test" }); + var id = Guid.NewGuid().ToString(); + var directConversation = TestUtils.AssertOperation( + await chat.CreateDirectConversation(convoUser, id)); + Assert.True(directConversation.CreatedChannel.Id == id); Assert.True(directConversation.HostMembership != null && directConversation.HostMembership.UserId == currentUser.Id); Assert.True(directConversation.InviteesMemberships != null && directConversation.InviteesMemberships.First().UserId == convoUser.Id); + + //Cleanup + await directConversation.CreatedChannel.Delete(); } [Test] @@ -106,13 +116,17 @@ public async Task TestCreateGroupConversation() var convoUser1 = await chat.GetOrCreateUser("group_conversation_user_1"); var convoUser2 = await chat.GetOrCreateUser("group_conversation_user_2"); var convoUser3 = await chat.GetOrCreateUser("group_conversation_user_3"); - var groupConversation = await - chat.CreateGroupConversation([convoUser1, convoUser2, convoUser3], "group_conversation_test"); - Assert.True(groupConversation.CreatedChannel is { Id: "group_conversation_test" }); + var id = Guid.NewGuid().ToString(); + var groupConversation = TestUtils.AssertOperation(await + chat.CreateGroupConversation([convoUser1, convoUser2, convoUser3], id)); + Assert.True(groupConversation.CreatedChannel.Id == id); Assert.True(groupConversation.HostMembership != null && groupConversation.HostMembership.UserId == currentUser.Id); Assert.True(groupConversation.InviteesMemberships is { Count: 3 }); Assert.True(groupConversation.InviteesMemberships.Any(x => - x.UserId == convoUser1.Id && x.ChannelId == "group_conversation_test")); + x.UserId == convoUser1.Id && x.ChannelId == id)); + + //Cleanup + await groupConversation.CreatedChannel.Delete(); } [Test] @@ -120,7 +134,7 @@ public async Task TestForwardMessage() { var messageForwardReceivedManualEvent = new ManualResetEvent(false); - var forwardingChannel = await chat.CreatePublicConversation("forwarding_channel"); + var forwardingChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation("forwarding_channel")); forwardingChannel.OnMessageReceived += message => { Assert.True(message.MessageText == "message_to_forward"); @@ -129,7 +143,7 @@ public async Task TestForwardMessage() forwardingChannel.Join(); await Task.Delay(2500); - channel.OnMessageReceived += async message => { await chat.ForwardMessage(message, forwardingChannel); }; + channel.OnMessageReceived += async message => { await message.Forward(forwardingChannel.Id); }; await channel.SendText("message_to_forward"); @@ -143,7 +157,8 @@ public async Task TestEmitEvent() var reportManualEvent = new ManualResetEvent(false); channel.OnCustomEvent += customEvent => { - Assert.True(customEvent.Payload == "{\"test\":\"some_nonsense\", \"type\": \"custom\"}"); + Assert.True(customEvent.Payload.Contains("test")); + Assert.True(customEvent.Payload.Contains("some_nonsense")); reportManualEvent.Set(); }; channel.SetListeningForCustomEvents(true); @@ -161,40 +176,44 @@ public async Task TestGetUnreadMessagesCounts() await Task.Delay(3000); - Assert.True((await chat.GetUnreadMessagesCounts(limit: 50)).Any(x => x.Channel.Id == channel.Id && x.Count > 0)); + Assert.True(TestUtils.AssertOperation(await chat.GetUnreadMessagesCounts(limit: 50)).Any(x => x.ChannelId == channel.Id && x.Count > 0)); } [Test] public async Task TestMarkAllMessagesAsRead() { - await channel.SendText("wololo"); - - await Task.Delay(10000); + var markTestChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); + markTestChannel.Join(); + + await Task.Delay(3000); + + await markTestChannel.SendText("wololo", new SendTextParams(){StoreInHistory = true}); - Assert.True((await chat.GetUnreadMessagesCounts()).Any(x => x.Channel.Id == channel.Id && x.Count > 0)); + await Task.Delay(3000); - var res = chat.MarkAllMessagesAsRead(); + Assert.True(TestUtils.AssertOperation(await chat.GetUnreadMessagesCounts()).Any(x => x.ChannelId == markTestChannel.Id && x.Count > 0)); - await Task.Delay(2000); + TestUtils.AssertOperation(await chat.MarkAllMessagesAsRead()); - var counts = await chat.GetUnreadMessagesCounts(); + await Task.Delay(5000); + + var counts = TestUtils.AssertOperation(await chat.GetUnreadMessagesCounts()); - Assert.False(counts.Any(x => x.Channel.Id == channel.Id && x.Count > 0)); + markTestChannel.Leave(); + await markTestChannel.Delete(); + + Assert.False(counts.Any(x => x.ChannelId == markTestChannel.Id && x.Count > 0)); } [Test] public async Task TestReadReceipts() { - var otherChat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "other_chat_user") - ); - if (!otherChat.TryGetChannel(channel.Id, out var otherChatChannel)) + var otherChat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("other_chat_user")) { - Assert.Fail(); - return; - } + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); + var otherChatChannel = TestUtils.AssertOperation(await otherChat.GetChannel(channel.Id)); otherChatChannel.Join(); await Task.Delay(2500); @@ -225,13 +244,12 @@ public async Task TestCanI() { await Task.Delay(4000); - var accessChat = await Chat.CreateInstance( - new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "can_i_test_user", - authKey: "qEF2AkF0Gma8TDFDdHRsGX0AQ3Jlc6VEY2hhbqFyY2FuX2lfdGVzdF9jaGFubmVsEUNncnCgQ3NwY6BDdXNyoER1dWlkoW9jYW5faV90ZXN0X3VzZXIY_0NwYXSlRGNoYW6gQ2dycKBDc3BjoEN1c3KgRHV1aWSgRG1ldGGgRHV1aWRvY2FuX2lfdGVzdF91c2VyQ3NpZ1ggAEijACv1wHoiwQulMhEPFRKEb1C4MYIgfS0wyYMCj3Y=" - )); + var accessChat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("can_i_test_user")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey, + AuthKey = "qEF2AkF0Gma8TDFDdHRsGX0AQ3Jlc6VEY2hhbqFyY2FuX2lfdGVzdF9jaGFubmVsEUNncnCgQ3NwY6BDdXNyoER1dWlkoW9jYW5faV90ZXN0X3VzZXIY_0NwYXSlRGNoYW6gQ2dycKBDc3BjoEN1c3KgRHV1aWSgRG1ldGGgRHV1aWRvY2FuX2lfdGVzdF91c2VyQ3NpZ1ggAEijACv1wHoiwQulMhEPFRKEb1C4MYIgfS0wyYMCj3Y=" + })); Assert.False(await accessChat.ChatAccessManager.CanI(PubnubAccessPermission.Write, PubnubAccessResourceType.Channels, "can_i_test_channel")); Assert.True(await accessChat.ChatAccessManager.CanI(PubnubAccessPermission.Read, PubnubAccessResourceType.Channels, diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs index 958a0e9..309e162 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs @@ -1,6 +1,6 @@ -using System.Diagnostics; -using PubNubChatAPI.Entities; -using PubnubChatApi.Entities.Data; +using PubnubApi; +using PubnubChatApi; +using Channel = PubnubChatApi.Channel; namespace PubNubChatApi.Tests; @@ -14,17 +14,13 @@ public class MembershipTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "membership_tests_user_54") - ); - channel = await chat.CreatePublicConversation("membership_tests_channel"); - if (!chat.TryGetCurrentUser(out user)) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("membership_tests_user_54")) { - Assert.Fail(); - } - + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("membership_tests_channel")); + user = TestUtils.AssertOperation(await chat.GetCurrentUser()); channel.Join(); await Task.Delay(3500); } @@ -32,6 +28,8 @@ public async Task Setup() [TearDown] public async Task CleanUp() { + await chat.PubnubInstance.RemoveMemberships().Channels(new List() { "membership_tests_channel", "test_invite_group_channel" }) + .Uuid("membership_tests_user_54").ExecuteAsync(); channel.Leave(); await Task.Delay(3000); chat.Destroy(); @@ -41,14 +39,14 @@ public async Task CleanUp() [Test] public async Task TestGetMemberships() { - var memberships = await user.GetMemberships(); + var memberships = TestUtils.AssertOperation(await user.GetMemberships()); Assert.True(memberships.Memberships.Any(x => x.ChannelId == channel.Id && x.UserId == user.Id)); } [Test] public async Task TestUpdateMemberships() { - var memberships = await user.GetMemberships(); + var memberships = TestUtils.AssertOperation(await user.GetMemberships()); var testMembership = memberships.Memberships.Last(); if (testMembership == null) { @@ -58,46 +56,52 @@ public async Task TestUpdateMemberships() var updateData = new ChatMembershipData() { - CustomDataJson = "{\"key\":\"" + Guid.NewGuid() + "\"}" + CustomData = new Dictionary() + { + {"key", Guid.NewGuid().ToString()} + }, + Type = "someMembership", + Status = "active" }; var manualUpdatedEvent = new ManualResetEvent(false); testMembership.OnMembershipUpdated += membership => { + Assert.True(membership.MembershipData.Type == testMembership.MembershipData.Type); + Assert.True(membership.MembershipData.Status == testMembership.MembershipData.Status); Assert.True(membership.Id == testMembership.Id); - var updatedData = membership.MembershipData.CustomDataJson; - Assert.True(updatedData == updateData.CustomDataJson, $"{updatedData} != {updateData.CustomDataJson}"); + Assert.True(membership.MembershipData.CustomData["key"].ToString() == updateData.CustomData["key"].ToString()); manualUpdatedEvent.Set(); }; testMembership.SetListeningForUpdates(true); await Task.Delay(4000); - await testMembership.Update(updateData); - var updated = manualUpdatedEvent.WaitOne(8000); + TestUtils.AssertOperation(await testMembership.Update(updateData)); + var updated = manualUpdatedEvent.WaitOne(10000); Assert.IsTrue(updated); } [Test] public async Task TestInvite() { - var testChannel = (await chat.CreateGroupConversation([user], "test_invite_group_channel")).CreatedChannel; + var testChannel = TestUtils.AssertOperation(await chat.CreateGroupConversation([user], "test_invite_group_channel")).CreatedChannel; var testUser = await chat.GetOrCreateUser("test_invite_user"); - var returnedMembership = await testChannel.Invite(testUser); + var returnedMembership = TestUtils.AssertOperation(await testChannel.Invite(testUser)); Assert.True(returnedMembership.ChannelId == testChannel.Id && returnedMembership.UserId == testUser.Id); } [Test] public async Task TestInviteMultiple() { - var testChannel = (await chat.CreateGroupConversation([user], "invite_multiple_test_group_channel_3")) + var testChannel = TestUtils.AssertOperation(await chat.CreateGroupConversation([user], "invite_multiple_test_group_channel_3")) .CreatedChannel; var secondUser = await chat.GetOrCreateUser("second_invite_user"); var thirdUser = await chat.GetOrCreateUser("third_invite_user"); - var returnedMemberships = await testChannel.InviteMultiple([ + var returnedMemberships = TestUtils.AssertOperation(await testChannel.InviteMultiple([ secondUser, thirdUser - ]); + ])); Assert.True( returnedMemberships.Count == 2 && returnedMemberships.Any(x => x.UserId == secondUser.Id && x.ChannelId == testChannel.Id) && @@ -107,12 +111,12 @@ public async Task TestInviteMultiple() [Test] public async Task TestLastRead() { - var testChannel = await chat.CreatePublicConversation("last_read_test_channel_57"); + var testChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation("last_read_test_channel_57")); testChannel.Join(); await Task.Delay(4000); - var membership = (await user.GetMemberships(limit: 20)).Memberships + var membership = TestUtils.AssertOperation(await user.GetMemberships(limit: 20)).Memberships .FirstOrDefault(x => x.ChannelId == testChannel.Id); if (membership == null) { @@ -128,13 +132,13 @@ public async Task TestLastRead() await Task.Delay(7000); - var lastTimeToken = membership.GetLastReadMessageTimeToken(); + var lastTimeToken = membership.LastReadMessageTimeToken; Assert.True(lastTimeToken == message.TimeToken); await membership.SetLastReadMessageTimeToken("99999999999999999"); await Task.Delay(3000); - Assert.True(membership.GetLastReadMessageTimeToken() == "99999999999999999"); + Assert.True(membership.LastReadMessageTimeToken == "99999999999999999"); messageReceivedManual.Set(); }; await testChannel.SendText("some_message"); @@ -146,7 +150,7 @@ public async Task TestLastRead() [Test] public async Task TestUnreadMessagesCount() { - var unreadChannel = await chat.CreatePublicConversation($"test_channel_{Guid.NewGuid()}"); + var unreadChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation($"test_channel_{Guid.NewGuid()}")); unreadChannel.Join(); await Task.Delay(3500); @@ -157,19 +161,19 @@ public async Task TestUnreadMessagesCount() await Task.Delay(8000); - var membership = (await unreadChannel.GetMemberships()) + var membership = TestUtils.AssertOperation(await unreadChannel.GetMemberships()) .Memberships.FirstOrDefault(x => x.UserId == user.Id); - var unreadCount = membership == null ? -1 : await membership.GetUnreadMessagesCount(); + var unreadCount = membership == null ? -1 : TestUtils.AssertOperation(await membership.GetUnreadMessagesCount()); Assert.True(unreadCount >= 3, $"Expected >=3 unread but got: {unreadCount}"); } [Test] //Test added after a specific user issue where calling membership.GetUnreadMessagesCount() - //after a history fetch would throw a C-Core PNR_RX_BUFF_NOT_EMPTY error + //after a history fetch would throw an exception public async Task TestUnreadCountAfterFetchHistory() { await channel.SendText("some_text"); - var membership = (await user.GetMemberships()) + var membership = TestUtils.AssertOperation(await user.GetMemberships()) .Memberships.FirstOrDefault(x => x.ChannelId == channel.Id); if (membership == null) { @@ -178,7 +182,7 @@ public async Task TestUnreadCountAfterFetchHistory() } await Task.Delay(5000); var history = await channel.GetMessageHistory("99999999999999999", "00000000000000000", 1); - var unread = await membership.GetUnreadMessagesCount(); + var unread = TestUtils.AssertOperation(await membership.GetUnreadMessagesCount()); Assert.True(unread >= 1); } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs index fa620b3..2265446 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs @@ -1,5 +1,6 @@ -using PubNubChatAPI.Entities; -using PubnubChatApi.Entities.Data; +using PubnubApi; +using PubnubChatApi; +using Channel = PubnubChatApi.Channel; namespace PubNubChatApi.Tests; @@ -9,40 +10,26 @@ public class MessageDraftTests private Chat chat; private Channel channel; private User dummyUser; - private Channel dummyChannel; [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "message_draft_tests_user") - ); - channel = await chat.CreatePublicConversation("message_draft_tests_channel", new ChatChannelData() + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("message_draft_tests_user")) { - ChannelName = "MessageDraftTestingChannel" - }); - if (!chat.TryGetCurrentUser(out var user)) + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("message_draft_tests_channel", new ChatChannelData() { - Assert.Fail(); - } - + Name = "MessageDraftTestingChannel" + })); channel.Join(); await Task.Delay(3000); - - if (!chat.TryGetUser("mock_user", out dummyUser)) - { - dummyUser = await chat.CreateUser("mock_user", new ChatUserData() - { - Username = "Mock Usernamiski" - }); - } - if (!chat.TryGetChannel("dummy_channel", out dummyChannel)) + dummyUser = await chat.GetOrCreateUser("mock_user", new ChatUserData() { - dummyChannel = await chat.CreatePublicConversation("dummy_channel"); - } + Username = "Mock Usernamiski" + }); } [Test] @@ -75,10 +62,9 @@ void InsertDelegateCallback(List elements, List @@ -99,8 +85,7 @@ public async Task TestInsertSuggestedMention() Assert.True(elements.Any(x => x.Text.Contains("Mock Usernamiski"))); successReset.Set(); break; - //TODO: to be re-enabled after Channel and Link mentions approach unification - /*case "channel_suggestion": + case "channel_suggestion": var channelSuggestion = mentions.FirstOrDefault(x => x.Target is { Target: "message_draft_tests_channel", Type: MentionType.Channel }); @@ -111,9 +96,13 @@ public async Task TestInsertSuggestedMention() messageDraft.InsertSuggestedMention(channelSuggestion, channelSuggestion.ReplaceTo); break; case "channel_inserted": - Assert.True(elements.Any(x => x.Text.Contains("MessageDraftTestingChannel"))); + Assert.True(elements.Any(x => x.Text.Contains("MessageDraftTestingChannel")), "channel wasn't inserted into MD"); + successReset.Set(); + break; + case "link_inserted": + Assert.True(elements.Any(x => x.MentionTarget is {Type:MentionType.Url, Target:"www.pubnub.com"}), "text link wasn't insterted into MD"); successReset.Set(); - break;*/ + break; default: Assert.Fail("Unexpected draft update callback flow in test"); break; @@ -122,57 +111,36 @@ public async Task TestInsertSuggestedMention() messageDraft.InsertText(0, "maybe i'll mention @Mock"); var userInserted = successReset.WaitOne(5000); Assert.True(userInserted, "didn't receive user insertion callback"); - - //TODO: to be re-enabled after Channel and Link mentions approach unification - /*step = "channel_suggestion"; + + step = "channel_suggestion"; successReset = new ManualResetEvent(false); messageDraft.InsertText(0, "now mention #MessageDraft "); var channelInserted = successReset.WaitOne(5000); Assert.True(channelInserted, "didn't receive channel insertion callback"); + step = "link_inserted"; + successReset = new ManualResetEvent(false); + messageDraft.AddMention(0, 3, new MentionTarget(){Target = "www.pubnub.com", Type = MentionType.Url}); + var linkAdded = successReset.WaitOne(5000); + Assert.True(channelInserted, "didn't receive text link insertion callback"); + var messageReset = new ManualResetEvent(false); + Message messageFromDraft = null; channel.OnMessageReceived += message => { - Assert.True(message.ReferencedChannels.Any(x => x.Id == channel.Id), "received message doesn't contain expected referenced channel"); - Assert.True(message.MentionedUsers.Any(x => x.Id == dummyUser.Id), "received message doesn't contain expected mentioned user"); + messageFromDraft = message; messageReset.Set(); }; await messageDraft.Send(); var receivedMessage = messageReset.WaitOne(10000); - Assert.True(receivedMessage, "didn't receive message callback");*/ - } - - //TODO: to be re-enabled after Channel and Link mentions approach unification - /*[Test] - public async Task TestAddAndSendTextLink() - { - var messageDraft = channel.CreateMessageDraft(); - messageDraft.InsertText(0, "some text goes here"); - var updateReset = new ManualResetEvent(false); - messageDraft.OnDraftUpdated += (elements, mentions) => + Assert.True(receivedMessage, "didn't receive message callback"); + if (messageFromDraft != null) { - Assert.True(elements.Any(x => x is { Text: "some", MentionTarget: {Target: "www.pubnub.com", Type: MentionType.Url} }) - , "updated message draft doesn't contain expected element"); - updateReset.Set(); - }; - messageDraft.AddMention(0, 4, new MentionTarget() - { - Target = "www.pubnub.com", - Type = MentionType.Url - }); - var updated = updateReset.WaitOne(3000); - Assert.True(updated, "didn't receive md update callback"); - - var messageReset = new ManualResetEvent(false); - channel.OnMessageReceived += message => - { - Assert.True(message.TextLinks.Any(x => x.Link == "www.pubnub.com"), "didn't find expected link in received message"); - messageReset.Set(); - }; - await messageDraft.Send(); - var received = messageReset.WaitOne(6000); - Assert.True(received, "didn't receive message callback after md send"); - }*/ + Assert.True(messageFromDraft.TextLinks.Any(x => x.Link == "www.pubnub.com"), "received message doesn't contain expected text link"); + Assert.True(messageFromDraft.ReferencedChannels.Any(x => x.Id == channel.Id), "received message doesn't contain expected referenced channel"); + Assert.True(messageFromDraft.MentionedUsers.Any(x => x.Id == dummyUser.Id), "received message doesn't contain expected mentioned user"); + } + } [Test] public async Task TestAddAndRemoveMention() diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs index b226123..8a51705 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs @@ -1,7 +1,7 @@ using System.Diagnostics; -using PubNubChatAPI.Entities; -using PubnubChatApi.Entities.Data; -using PubnubChatApi.Enums; +using PubnubApi; +using PubnubChatApi; +using Channel = PubnubChatApi.Channel; namespace PubNubChatApi.Tests; @@ -14,16 +14,13 @@ public class MessageTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "message_tests_user_2") - ); - channel = await chat.CreatePublicConversation("message_tests_channel_2"); - if (!chat.TryGetCurrentUser(out user)) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("message_tests_user_2")) { - Assert.Fail(); - } + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("message_tests_channel_2")); + user = TestUtils.AssertOperation(await chat.GetCurrentUser()); channel.Join(); await Task.Delay(3500); } @@ -48,10 +45,7 @@ public async Task TestSendAndReceive() Assert.True(message.Type == PubnubChatMessageType.Text); manualReceiveEvent.Set(); }; - await channel.SendText("Test message text", new SendTextParams() - { - MentionedUsers = new Dictionary() { { 0, user } }, - }); + await channel.SendText("Test message text"); var received = manualReceiveEvent.WaitOne(6000); Assert.IsTrue(received); } @@ -60,7 +54,7 @@ public async Task TestSendAndReceive() public async Task TestReceivingMessageData() { var manualReceiveEvent = new ManualResetEvent(false); - var testChannel = await chat.CreatePublicConversation("message_data_test_channel"); + var testChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation("message_data_test_channel")); testChannel.Join(); await Task.Delay(2500); testChannel.OnMessageReceived += async message => @@ -69,15 +63,19 @@ public async Task TestReceivingMessageData() { await testChannel.SendText("message_with_data", new SendTextParams() { - MentionedUsers = new Dictionary() { { 0, user } }, + MentionedUsers = new Dictionary() { { 0, new MentionedUser() + { + Id = user.Id, + Name = user.UserName + } } }, QuotedMessage = message }); } else if (message.MessageText == "message_with_data") { Assert.True(message.MentionedUsers.Any(x => x.Id == user.Id)); - Assert.True(message.TryGetQuotedMessage(out var quotedMessage) && - quotedMessage.MessageText == "message_to_be_quoted"); + var quoted = TestUtils.AssertOperation(await message.GetQuotedMessage()); + Assert.True(quoted.MessageText == "message_to_be_quoted"); manualReceiveEvent.Set(); } }; @@ -88,21 +86,24 @@ public async Task TestReceivingMessageData() } [Test] - public async Task TestTryGetMessage() + public async Task TestGetMessage() { var manualReceiveEvent = new ManualResetEvent(false); - channel.OnMessageReceived += message => + ChatOperationResult receivedMessage = null; + channel.OnMessageReceived += async message => { + await Task.Delay(5000); if (message.ChannelId == channel.Id) { - Assert.True(chat.TryGetMessage(channel.Id, message.TimeToken, out _)); + receivedMessage = await chat.GetMessage(channel.Id, message.TimeToken); manualReceiveEvent.Set(); } }; await channel.SendText("something"); - var received = manualReceiveEvent.WaitOne(4000); + var received = manualReceiveEvent.WaitOne(12000); Assert.IsTrue(received); + Assert.True(!receivedMessage.Error, $"Error when trying to GetMessage(): {receivedMessage.Exception?.Message}"); } [Test] @@ -200,7 +201,7 @@ public async Task TestRestoreMessage() [Test] public async Task TestPinMessage() { - var pinTestChannel = await chat.CreatePublicConversation(); + var pinTestChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); pinTestChannel.Join(); await Task.Delay(2500); pinTestChannel.SetListeningForUpdates(true); @@ -213,8 +214,8 @@ public async Task TestPinMessage() await Task.Delay(3000); - var got = pinTestChannel.TryGetPinnedMessage(out var pinnedMessage); - Assert.True(got && pinnedMessage.MessageText == "message to pin"); + var pinnedMessage = TestUtils.AssertOperation(await pinTestChannel.GetPinnedMessage()); + Assert.True(pinnedMessage.MessageText == "message to pin"); manualReceivedEvent.Set(); }; await pinTestChannel.SendText("message to pin"); @@ -272,7 +273,7 @@ public async Task TestCreateThread() try { message.SetListeningForUpdates(true); - var thread = await message.CreateThread(); + var thread = TestUtils.AssertOperation(message.CreateThread()); thread.Join(); await Task.Delay(3500); await thread.SendText("thread_init_text"); @@ -287,13 +288,12 @@ public async Task TestCreateThread() } Assert.True(hasThread); - Assert.True(message.TryGetThread(out var threadChannel)); + var getThread = await message.GetThread(); + Assert.True(!getThread.Error); await message.RemoveThread(); await Task.Delay(5000); - - //TODO: temporary way to get latest message pointer since remove_thread doesn't return a new pointer - chat.TryGetMessage(channel.Id, message.Id, out message); + Assert.False(message.HasThread()); manualReceiveEvent.Set(); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubnubTestsParameters.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubnubTestsParameters.cs index c8680ba..1cfc535 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubnubTestsParameters.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubnubTestsParameters.cs @@ -6,7 +6,7 @@ public static class PubnubTestsParameters private static readonly string EnvSubscribeKey = Environment.GetEnvironmentVariable("PN_SUB_KEY"); private static readonly string EnvSecretKey = Environment.GetEnvironmentVariable("PN_SEC_KEY"); - public static readonly string PublishKey = string.IsNullOrEmpty(EnvPublishKey) ? "demo-36" : EnvPublishKey; - public static readonly string SubscribeKey = string.IsNullOrEmpty(EnvSubscribeKey) ? "demo-36" : EnvSubscribeKey; + public static readonly string PublishKey = string.IsNullOrEmpty(EnvPublishKey) ? "pub-c-79c582a2-d7a4-4ee7-9f28-7a6f1b7fa11c" : EnvPublishKey; + public static readonly string SubscribeKey = string.IsNullOrEmpty(EnvSubscribeKey) ? "sub-c-ca0af928-f4f9-474c-b56e-d6be81bf8ed0" : EnvSubscribeKey; public static readonly string SecretKey = string.IsNullOrEmpty(EnvSecretKey) ? "demo-36" : EnvSecretKey; } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs index a688520..b05fc95 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs @@ -1,6 +1,5 @@ -using System.Diagnostics; -using PubNubChatAPI.Entities; -using PubnubChatApi.Entities.Data; +using PubnubApi; +using PubnubChatApi; namespace PubNubChatApi.Tests; @@ -12,11 +11,11 @@ public class RestrictionsTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "restrictions_tests_user") - ); + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("restrictions_tests_user")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); } [TearDown] @@ -30,8 +29,8 @@ public async Task CleanUp() public async Task TestSetRestrictions() { var user = await chat.GetOrCreateUser("user123"); - var channel = await chat.CreatePublicConversation("new_channel"); - + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("new_channel")); + await Task.Delay(2000); var restriction = new Restriction() @@ -40,16 +39,16 @@ public async Task TestSetRestrictions() Mute = true, Reason = "Some Reason" }; - await channel.SetRestrictions(user.Id, restriction); + TestUtils.AssertOperation(await channel.SetRestrictions(user.Id, restriction)); await Task.Delay(3000); - - var fetchedRestriction = await channel.GetUserRestrictions(user); + + var fetchedRestriction = TestUtils.AssertOperation(await channel.GetUserRestrictions(user)); Assert.True(restriction.Ban == fetchedRestriction.Ban && restriction.Mute == fetchedRestriction.Mute && restriction.Reason == fetchedRestriction.Reason); - var restrictionFromUser = await user.GetChannelRestrictions(channel); + var restrictionFromUser = TestUtils.AssertOperation(await user.GetChannelRestrictions(channel)); Assert.True(restriction.Ban == restrictionFromUser.Ban && restriction.Mute == restrictionFromUser.Mute && restriction.Reason == restrictionFromUser.Reason); @@ -59,7 +58,7 @@ public async Task TestSetRestrictions() public async Task TestGetRestrictionsSets() { var user = await chat.GetOrCreateUser("user1234"); - var channel = await chat.CreatePublicConversation("new_channel_2"); + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("new_channel")); await Task.Delay(4000); @@ -69,12 +68,12 @@ public async Task TestGetRestrictionsSets() Mute = true, Reason = "Some Reason" }; - await channel.SetRestrictions(user.Id, restriction); - + TestUtils.AssertOperation(await channel.SetRestrictions(user.Id, restriction)); + await Task.Delay(4000); - var a = await channel.GetUsersRestrictions(); - var b = await user.GetChannelsRestrictions(); + var a = TestUtils.AssertOperation(await channel.GetUsersRestrictions()); + var b = TestUtils.AssertOperation(await user.GetChannelsRestrictions()); Assert.True(a.Restrictions.Any(x => x.UserId == user.Id)); Assert.True(b.Restrictions.Any(x => x.ChannelId == channel.Id)); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs index 5108a6e..d0eebc4 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs @@ -1,18 +1,40 @@ -using PubNubChatAPI.Entities; +using PubnubChatApi; namespace PubNubChatApi.Tests; public static class TestUtils { - public static async Task GetOrCreateUser(this Chat chat, string userId) + public static async Task GetOrCreateUser(this Chat chat, string userId, ChatUserData? userData = null) { - if (chat.TryGetUser(userId, out var user)) + var getUser = await chat.GetUser(userId); + if (getUser.Error) { - return user; + userData ??= new ChatUserData(); + var createUser = await chat.CreateUser(userId, userData); + if (createUser.Error) + { + Assert.Fail($"Failed to create User! Error: {createUser.Exception.Message}"); + }else + { + return createUser.Result; + } } - else + return getUser.Result; + } + + public static void AssertOperation(ChatOperationResult chatOperationResult) + { + if (chatOperationResult.Error) + { + Assert.Fail($"Chat operation failed! Error: {chatOperationResult.Exception.Message}"); + } + } + public static T AssertOperation(ChatOperationResult chatOperationResult) + { + if (chatOperationResult.Error) { - return await chat.CreateUser(userId); + Assert.Fail($"Chat operation for getting {typeof(T).Name} failed! Error: {chatOperationResult.Exception.Message}"); } + return chatOperationResult.Result; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs index 270e8dc..46db29a 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs @@ -1,6 +1,7 @@ -using System.Diagnostics; -using PubNubChatAPI.Entities; -using PubnubChatApi.Entities.Data; +using PubnubApi; +using PubnubChatApi; +using Channel = PubnubChatApi.Channel; + namespace PubNubChatApi.Tests; @@ -14,17 +15,14 @@ public class ThreadsTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "threads_tests_user_2") - ); - var randomId = Guid.NewGuid().ToString()[..10]; - channel = await chat.CreatePublicConversation(randomId); - if (!chat.TryGetCurrentUser(out user)) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("threads_tests_user_2")) { - Assert.Fail(); - } + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); + var randomId = Guid.NewGuid().ToString()[..10]; + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation(randomId)); + user = TestUtils.AssertOperation(await chat.GetCurrentUser()); channel.Join(); await Task.Delay(3500); } @@ -46,7 +44,7 @@ public async Task TestGetThreadHistory() channel.OnMessageReceived += async message => { message.SetListeningForUpdates(true); - var thread = await message.CreateThread(); + var thread = TestUtils.AssertOperation(message.CreateThread()); thread.Join(); await Task.Delay(5000); @@ -57,7 +55,7 @@ public async Task TestGetThreadHistory() await Task.Delay(10000); - var history = await thread.GetThreadHistory("99999999999999999", "00000000000000000", 3); + var history = TestUtils.AssertOperation(await thread.GetThreadHistory("99999999999999999", "00000000000000000", 3)); Assert.True(history.Count == 3 && history.Any(x => x.MessageText == "one")); historyReadReset.Set(); }; @@ -73,26 +71,26 @@ public async Task TestThreadChannelParentChannelPinning() channel.OnMessageReceived += async message => { message.SetListeningForUpdates(true); - var thread = await message.CreateThread(); + var thread = TestUtils.AssertOperation(message.CreateThread()); thread.Join(); await thread.SendText("thread init message"); await Task.Delay(7000); var threadMessage = - (await thread.GetThreadHistory("99999999999999999", "00000000000000000", 1))[0]; + TestUtils.AssertOperation(await thread.GetThreadHistory("99999999999999999", "00000000000000000", 1))[0]; await thread.PinMessageToParentChannel(threadMessage); await Task.Delay(7000); - var hasPinned = channel.TryGetPinnedMessage(out var pinnedMessage); - var correctText = hasPinned && pinnedMessage.MessageText == "thread init message"; - Assert.True(hasPinned && correctText); + var pinned = TestUtils.AssertOperation(await channel.GetPinnedMessage()); + Assert.True(pinned.MessageText == "thread init message"); await thread.UnPinMessageFromParentChannel(); await Task.Delay(7000); - - Assert.False(channel.TryGetPinnedMessage(out _)); + + var getPinned = await channel.GetPinnedMessage(); + Assert.True(getPinned.Error); historyReadReset.Set(); }; await channel.SendText("thread_start_message"); @@ -106,7 +104,7 @@ public async Task TestThreadChannelEmitUserMention() var mentionedReset = new ManualResetEvent(false); channel.OnMessageReceived += async message => { - var thread = await message.CreateThread(); + var thread = TestUtils.AssertOperation(message.CreateThread()); thread.Join(); await Task.Delay(2500); user.SetListeningForMentionEvents(true); @@ -130,7 +128,7 @@ public async Task TestThreadMessageParentChannelPinning() channel.OnMessageReceived += async message => { message.SetListeningForUpdates(true); - var thread = await message.CreateThread(); + var thread = TestUtils.AssertOperation(message.CreateThread()); thread.Join(); await Task.Delay(3500); @@ -141,19 +139,21 @@ public async Task TestThreadMessageParentChannelPinning() await Task.Delay(8000); - var history = await thread.GetThreadHistory("99999999999999999", "00000000000000000", 3); + var history = TestUtils.AssertOperation(await thread.GetThreadHistory("99999999999999999", "00000000000000000", 3)); var threadMessage = history[0]; await threadMessage.PinMessageToParentChannel(); await Task.Delay(5000); - Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.MessageText == threadMessage.MessageText); + var pinned = TestUtils.AssertOperation(await channel.GetPinnedMessage()); + Assert.True(pinned.MessageText == threadMessage.MessageText); await threadMessage.UnPinMessageFromParentChannel(); await Task.Delay(5000); - Assert.False(channel.TryGetPinnedMessage(out _)); + var getPinned = await channel.GetPinnedMessage(); + Assert.True(getPinned.Error); historyReadReset.Set(); }; await channel.SendText("thread_start_message"); @@ -168,7 +168,7 @@ public async Task TestThreadMessageUpdate() channel.OnMessageReceived += async message => { message.SetListeningForUpdates(true); - var thread = await message.CreateThread(); + var thread = TestUtils.AssertOperation(message.CreateThread()); thread.Join(); await Task.Delay(3000); @@ -179,7 +179,7 @@ public async Task TestThreadMessageUpdate() await Task.Delay(10000); - var history = await thread.GetThreadHistory("99999999999999999", "00000000000000000", 3); + var history = TestUtils.AssertOperation(await thread.GetThreadHistory("99999999999999999", "00000000000000000", 3)); var threadMessage = history[0]; threadMessage.SetListeningForUpdates(true); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs index 92df044..81f57ab 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs @@ -1,6 +1,6 @@ -using System.Diagnostics; -using PubNubChatAPI.Entities; -using PubnubChatApi.Entities.Data; +using PubnubApi; +using PubnubChatApi; +using Channel = PubnubChatApi.Channel; namespace PubNubChatApi.Tests; @@ -14,17 +14,13 @@ public class UserTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "user_tests_user", - storeUserActivityTimestamp: true) - ); - channel = await chat.CreatePublicConversation("user_tests_channel"); - if (!chat.TryGetCurrentUser(out user)) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("user_tests_user")) { - Assert.Fail(); - } + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("user_tests_channel")); + user = TestUtils.AssertOperation(await chat.GetCurrentUser()); channel.Join(); await Task.Delay(3500); } @@ -59,15 +55,15 @@ public async Task TestLastUserActive() public async Task TestUserUpdate() { var updatedReset = new ManualResetEvent(false); - var testUser = await chat.GetOrCreateUser("wolololo_guy"); - - await Task.Delay(5000); - + var testUser = await chat.GetOrCreateUser(Guid.NewGuid().ToString()); + await Task.Delay(3000); + testUser.SetListeningForUpdates(true); + await Task.Delay(3000); var newRandomUserName = Guid.NewGuid().ToString(); testUser.OnUserUpdated += updatedUser => { Assert.True(updatedUser.UserName == newRandomUserName); - Assert.True(updatedUser.CustomData == "{\"some_key\":\"some_value\"}"); + Assert.True(updatedUser.CustomData.TryGetValue("some_key", out var value) && value.ToString() == "some_value"); Assert.True(updatedUser.Email == "some@guy.com"); Assert.True(updatedUser.ExternalId == "xxx_some_guy_420_xxx"); Assert.True(updatedUser.ProfileUrl == "www.some.guy"); @@ -75,12 +71,13 @@ public async Task TestUserUpdate() Assert.True(updatedUser.DataType == "someType"); updatedReset.Set(); }; - testUser.SetListeningForUpdates(true); - await Task.Delay(3000); await testUser.Update(new ChatUserData() { Username = newRandomUserName, - CustomDataJson = "{\"some_key\":\"some_value\"}", + CustomData = new Dictionary() + { + {"some_key", "some_value"} + }, Email = "some@guy.com", ExternalId = "xxx_some_guy_420_xxx", ProfileUrl = "www.some.guy", @@ -88,32 +85,40 @@ await testUser.Update(new ChatUserData() Type = "someType" }); var updated = updatedReset.WaitOne(15000); + testUser.SetListeningForUpdates(false); Assert.True(updated); + + //Cleanup + await testUser.DeleteUser(); } [Test] public async Task TestUserDelete() { - var someUser = await chat.CreateUser(Guid.NewGuid().ToString()); - - Assert.True(chat.TryGetUser(someUser.Id, out _), "Couldn't get freshly created user"); + var someUser = TestUtils.AssertOperation(await chat.CreateUser(Guid.NewGuid().ToString())); + + TestUtils.AssertOperation(await chat.GetUser(someUser.Id)); await someUser.DeleteUser(); await Task.Delay(3000); - - Assert.False(chat.TryGetUser(someUser.Id, out _), "Got the freshly deleted user"); + + var getAfterUser = await chat.GetUser(someUser.Id); + if (!getAfterUser.Error) + { + Assert.Fail("Got the freshly deleted user"); + } } [Test] public async Task TestUserWherePresent() { - var someChannel = await chat.CreatePublicConversation(); + var someChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); someChannel.Join(); await Task.Delay(4000); - var where = await user.WherePresent(); + var where = TestUtils.AssertOperation(await user.WherePresent()); Assert.Contains(someChannel.Id, where, "user.WherePresent() doesn't have most recently joined channel!"); } @@ -121,12 +126,12 @@ public async Task TestUserWherePresent() [Test] public async Task TestUserIsPresentOn() { - var someChannel = await chat.CreatePublicConversation(); + var someChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); someChannel.Join(); await Task.Delay(4000); - var isOn = await user.IsPresentOn(someChannel.Id); + var isOn = TestUtils.AssertOperation(await user.IsPresentOn(someChannel.Id)); Assert.True(isOn, "user.IsPresentOn() doesn't return true for most recently joined channel!"); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs index 0f318f2..7a5f6e2 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs @@ -1,91 +1,44 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; using System.Threading.Tasks; -using PubnubChatApi.Utilities; +using PubnubApi; -namespace PubNubChatAPI.Entities +namespace PubnubChatApi { public abstract class ChatEntity { - [DllImport("pubnub-chat")] - protected static extern void pn_callback_handle_dispose(IntPtr handle); + protected Chat chat; + protected Subscription updateSubscription; + protected abstract string UpdateChannelId { get; } - [DllImport("pubnub-chat")] - protected static extern void pn_callback_handle_close(IntPtr handle); - - protected IntPtr updateListeningHandle; - - protected IntPtr pointer; - internal IntPtr Pointer => pointer; - - internal ChatEntity(IntPtr pointer) - { - this.pointer = pointer; - } - - internal void UpdatePointer(IntPtr newPointer) - { - DisposePointer(); - pointer = newPointer; - } - - internal abstract void UpdateWithPartialPtr(IntPtr partialPointer); - - protected abstract void DisposePointer(); - - protected abstract IntPtr StreamUpdates(); - - public virtual async void SetListeningForUpdates(bool listen) + internal ChatEntity(Chat chat) { - updateListeningHandle = await SetListening(updateListeningHandle, listen, StreamUpdates); + this.chat = chat; } - internal async Task SetListening(IntPtr callbackHandle, bool listen, Func streamFunction) + protected void SetListening(ref Subscription subscription, SubscriptionOptions subscriptionOptions, bool listen, string channelId, SubscribeCallback listener) { if (listen) { - if (callbackHandle != IntPtr.Zero) + if (subscription != null) { - return callbackHandle; + return; } - callbackHandle = await Task.Run(streamFunction); - CUtilities.CheckCFunctionResult(callbackHandle); - return callbackHandle; + subscription = chat.PubnubInstance.Channel(channelId).Subscription(subscriptionOptions); + subscription.AddListener(listener); + subscription.Subscribe(); } else { - if (callbackHandle == IntPtr.Zero) - { - return callbackHandle; - } - await Task.Run(() => - { - pn_callback_handle_close(callbackHandle); - }); - if (callbackHandle != IntPtr.Zero) - { - pn_callback_handle_dispose(callbackHandle); - } - callbackHandle = IntPtr.Zero; - return callbackHandle; + subscription?.Unsubscribe(); } } - - protected virtual async Task CleanupConnectionHandles() - { - updateListeningHandle = await SetListening(updateListeningHandle, false, StreamUpdates); - } - - private async void CleanUpAsync() - { - await CleanupConnectionHandles(); - DisposePointer(); - } - - ~ChatEntity() + + public virtual void SetListeningForUpdates(bool listen) { - CleanUpAsync(); + SetListening(ref updateSubscription, SubscriptionOptions.None, listen, UpdateChannelId, CreateUpdateListener()); } + + protected abstract SubscribeCallback CreateUpdateListener(); + + public abstract Task Refresh(); } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/UniqueChatEntity.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/UniqueChatEntity.cs index 193407a..c6241fe 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/UniqueChatEntity.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/UniqueChatEntity.cs @@ -1,12 +1,12 @@ using System; -namespace PubNubChatAPI.Entities +namespace PubnubChatApi { public abstract class UniqueChatEntity : ChatEntity { public string Id { get; protected set; } - internal UniqueChatEntity(IntPtr pointer, string uniqueId) : base(pointer) + internal UniqueChatEntity(Chat chat, string uniqueId) : base(chat) { Id = uniqueId; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index b06921a..8b340cb 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -1,18 +1,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; using System.Timers; -using Newtonsoft.Json; -using PubnubChatApi.Entities.Data; -using PubnubChatApi.Entities.Events; -using PubnubChatApi.Enums; -using PubnubChatApi.Utilities; +using PubnubApi; -namespace PubNubChatAPI.Entities +namespace PubnubChatApi { /// /// Class Channel represents a chat channel. @@ -23,155 +16,6 @@ namespace PubNubChatAPI.Entities /// public class Channel : UniqueChatEntity { - #region DLL Imports - - [DllImport("pubnub-chat")] - private static extern void pn_channel_delete(IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_connect(IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_disconnect(IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_join(IntPtr channel, string additional_params); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_leave(IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_set_restrictions(IntPtr channel, string user_id, bool ban_user, - bool mute_user, string reason); - - [DllImport("pubnub-chat")] - private static extern void pn_channel_get_channel_id( - IntPtr channel, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_get_user_restrictions( - IntPtr channel, - IntPtr user, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_channel_get_data_channel_name( - IntPtr channel, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_channel_get_data_description( - IntPtr channel, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_channel_get_data_custom_data_json( - IntPtr channel, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_channel_get_data_updated( - IntPtr channel, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_channel_get_data_status( - IntPtr channel, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_channel_get_data_type( - IntPtr channel, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_is_present(IntPtr channel, string user_id); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_who_is_present(IntPtr channel, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_invite_user(IntPtr channel, IntPtr user); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_invite_multiple(IntPtr channel, IntPtr[] users, int users_length, - StringBuilder result_json); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_start_typing(IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_stop_typing(IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_pin_message(IntPtr channel, IntPtr message); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_unpin_message(IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_get_pinned_message(IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_create_message_draft_dirty(IntPtr channel, - int user_suggestion_source, - bool is_typing_indicator_triggered, - int user_limit, - int channel_limit); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_emit_user_mention(IntPtr channel, string user_id, string timetoken, - string text); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_update_with_base(IntPtr channel, IntPtr base_channel); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_get_user_suggestions(IntPtr channel, string text, int limit, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_send_text_dirty( - IntPtr channel, - string message, - bool store_in_history, - bool send_by_post, - string meta, - int mentioned_users_length, - int[] mentioned_users_indexes, - IntPtr[] mentioned_users, - IntPtr quoted_message); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_get_users_restrictions(IntPtr channel, string sort, int limit, string next, - string prev, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_stream_read_receipts(IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_stream_message_reports(IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_stream_updates(IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_get_typing(IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_stream_presence(IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_join_with_membership_data( - IntPtr channel, - string membership_custom_json, - string membership_type, - string membership_status - ); - - #endregion - /// /// The name of the channel. /// @@ -180,15 +24,7 @@ string membership_status /// /// /// The name of the channel. - public string Name - { - get - { - var buffer = new StringBuilder(512); - pn_channel_get_data_channel_name(pointer, buffer); - return buffer.ToString(); - } - } + public string Name => channelData.Name; /// /// The description of the channel. @@ -196,15 +32,7 @@ public string Name /// /// The description that allows users to understand the purpose of the channel. /// - public string Description - { - get - { - var buffer = new StringBuilder(512); - pn_channel_get_data_description(pointer, buffer); - return buffer.ToString(); - } - } + public string Description => channelData.Description; /// /// The custom data of the channel. @@ -213,18 +41,7 @@ public string Description /// The custom data that can be used to store additional information about the channel. /// /// - /// - /// The custom data is stored in JSON format. - /// - public string CustomDataJson - { - get - { - var buffer = new StringBuilder(2048); - pn_channel_get_data_custom_data_json(pointer, buffer); - return buffer.ToString(); - } - } + public Dictionary CustomData => channelData.CustomData ?? new (); /// /// The information about the last update of the channel. @@ -232,15 +49,7 @@ public string CustomDataJson /// The time when the channel was last updated. /// /// - public string Updated - { - get - { - var buffer = new StringBuilder(512); - pn_channel_get_data_updated(pointer, buffer); - return buffer.ToString(); - } - } + public string Updated => channelData.Updated; /// /// The status of the channel. @@ -248,15 +57,7 @@ public string Updated /// The last status response received from the server. /// /// - public string Status - { - get - { - var buffer = new StringBuilder(512); - pn_channel_get_data_status(pointer, buffer); - return buffer.ToString(); - } - } + public string Status => channelData.Status; /// /// The type of the channel. @@ -264,23 +65,12 @@ public string Status /// The type of the response received from the server when the channel was created. /// /// - public string Type - { - get - { - var buffer = new StringBuilder(512); - pn_channel_get_data_type(pointer, buffer); - return buffer.ToString(); - } - } + public string Type => channelData.Type; - protected Chat chat; - private IntPtr customEventsListeningHandle; - private IntPtr reportEventsListeningHandle; - private IntPtr readReceiptsListeningHandle; - private IntPtr typingListeningHandle; - private IntPtr presenceListeningHandle; - protected IntPtr connectionHandle; + protected ChatChannelData channelData; + + protected Subscription? subscription; + private Dictionary typingIndicators = new(); /// @@ -323,6 +113,7 @@ public string Type /// public event Action OnChannelUpdate; + private Subscription presenceEventsSubscription; /// /// Event that is triggered when any presence update occurs. /// @@ -343,227 +134,336 @@ public string Type /// public event Action> OnPresenceUpdate; + private Subscription typingEventsSubscription; public event Action> OnUsersTyping; - - public event Action?>> OnReadReceiptEvent; + private Subscription readReceiptsSubscription; + public event Action>> OnReadReceiptEvent; + private Subscription reportEventsSubscription; public event Action OnReportEvent; + private Subscription customEventsSubscription; public event Action OnCustomEvent; - internal Channel(Chat chat, string channelId, IntPtr channelPointer) : base(channelPointer, channelId) - { - this.chat = chat; - } - - protected override IntPtr StreamUpdates() - { - return pn_channel_stream_updates(pointer); - } - - public async void SetListeningForCustomEvents(bool listen) - { - customEventsListeningHandle = await SetListening(customEventsListeningHandle, listen, - () => chat.ListenForEvents(Id, PubnubChatEventType.Custom)); - } - - internal void BroadcastCustomEvent(ChatEvent chatEvent) - { - OnCustomEvent?.Invoke(chatEvent); - } - - public async void SetListeningForReportEvents(bool listen) - { - reportEventsListeningHandle = await SetListening(reportEventsListeningHandle, listen, - () => pn_channel_stream_message_reports(pointer)); - } - - internal void BroadcastReportEvent(ChatEvent chatEvent) - { - OnReportEvent?.Invoke(chatEvent); - } + protected override string UpdateChannelId => Id; - public async void SetListeningForReadReceiptsEvents(bool listen) + internal Channel(Chat chat, string channelId, ChatChannelData data) : base(chat, channelId) { - readReceiptsListeningHandle = await SetListening(readReceiptsListeningHandle, listen, - () => pn_channel_stream_read_receipts(pointer)); + UpdateLocalData(data); } - - public async void SetListeningForTyping(bool listen) + + protected override SubscribeCallback CreateUpdateListener() { - typingListeningHandle = await SetListening(typingListeningHandle, listen, - () => pn_channel_get_typing(pointer)); + return chat.ListenerFactory.ProduceListener(objectEventCallback: delegate(Pubnub pn, PNObjectEventResult e) + { + if (ChatParsers.TryParseChannelUpdate(chat, this, e, out var updatedData)) + { + UpdateLocalData(updatedData); + OnChannelUpdate?.Invoke(this); + } + }); } - public async void SetListeningForPresence(bool listen) + internal void UpdateLocalData(ChatChannelData? newData) { - presenceListeningHandle = await SetListening(presenceListeningHandle, listen, - () => pn_channel_stream_presence(pointer)); + if (newData == null) + { + return; + } + channelData = newData; } - internal static string GetChannelIdFromPtr(IntPtr channelPointer) + internal static async Task> UpdateChannelData(Chat chat, string channelId, ChatChannelData data) { - var buffer = new StringBuilder(512); - pn_channel_get_channel_id(channelPointer, buffer); - return buffer.ToString(); + var operation = chat.PubnubInstance.SetChannelMetadata().IncludeCustom(true) + .Channel(channelId); + if (!string.IsNullOrEmpty(data.Name)) + { + operation = operation.Name(data.Name); + } + if (!string.IsNullOrEmpty(data.Description)) + { + operation = operation.Description(data.Description); + } + if (!string.IsNullOrEmpty(data.Status)) + { + operation = operation.Status(data.Status); + } + if (data.CustomData != null) + { + operation = operation.Custom(data.CustomData); + } + if (!string.IsNullOrEmpty(data.Type)) + { + operation = operation.Type(data.Type); + } + return await operation.ExecuteAsync().ConfigureAwait(false); } - - internal void BroadcastMessageReceived(Message message) + + internal static async Task> GetChannelData(Chat chat, string channelId) { - OnMessageReceived?.Invoke(message); + return await chat.PubnubInstance.GetChannelMetadata().IncludeCustom(true) + .Channel(channelId) + .ExecuteAsync().ConfigureAwait(false); } - internal void BroadcastReadReceipt(Dictionary?> readReceiptEventData) + public override async Task Refresh() { - OnReadReceiptEvent?.Invoke(readReceiptEventData); + var result = new ChatOperationResult("Channel.Refresh()", chat); + var getData = await GetChannelData(chat, Id).ConfigureAwait(false); + if (result.RegisterOperation(getData)) + { + return result; + } + UpdateLocalData(getData.Result); + return result; } - internal override void UpdateWithPartialPtr(IntPtr partialPointer) + /// + /// Sets whether to listen for custom events on this channel. + /// + /// True to start listening, false to stop listening. + public void SetListeningForCustomEvents(bool listen) { - var newFullPointer = pn_channel_update_with_base(partialPointer, pointer); - CUtilities.CheckCFunctionResult(newFullPointer); - UpdatePointer(newFullPointer); + SetListening(ref customEventsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Custom, out var customEvent)) + { + OnCustomEvent?.Invoke(customEvent); + chat.BroadcastAnyEvent(customEvent); + } + })); } - internal void BroadcastChannelUpdate() + /// + /// Sets whether to listen for report events on this channel. + /// + /// True to start listening, false to stop listening. + public void SetListeningForReportEvents(bool listen) { - OnChannelUpdate?.Invoke(this); + SetListening(ref reportEventsSubscription, SubscriptionOptions.None, listen, $"{Chat.INTERNAL_MODERATION_PREFIX}_{Id}", chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Report, out var reportEvent)) + { + OnReportEvent?.Invoke(reportEvent); + chat.BroadcastAnyEvent(reportEvent); + } + })); } - - internal async void BroadcastPresenceUpdate() + + /// + /// Sets whether to listen for read receipt events on this channel. + /// + /// True to start listening, false to stop listening. + public void SetListeningForReadReceiptsEvents(bool listen) { - OnPresenceUpdate?.Invoke(await WhoIsPresent()); + SetListening(ref readReceiptsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + async delegate(Pubnub _, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Receipt, out var readEvent)) + { + var getMembers = await chat.GetChannelMemberships(Id).ConfigureAwait(false); + if (getMembers.Error) + { + return; + } + var members = getMembers.Result; + var outputDict = members.Memberships + .GroupBy(membership => membership.LastReadMessageTimeToken) + .ToDictionary( + g => g.Key, + g => g.Select(membership => membership.UserId).ToList() ?? new List() + ) ?? new Dictionary>(); + OnReadReceiptEvent?.Invoke(outputDict); + chat.BroadcastAnyEvent(readEvent); + } + })); } - internal void TryParseAndBroadcastTypingEvent(List userIds) + /// + /// Sets whether to listen for typing events on this channel. + /// + /// True to start listening, false to stop listening. + public void SetListeningForTyping(bool listen) { - //stop typing - var keys = typingIndicators.Keys.ToArray(); - for (int i = 0; i < keys.Length; i++) - { - var key = keys[i]; - var indicator = typingIndicators[key]; - if (!userIds.Contains(key)) - { - indicator.Stop(); - typingIndicators.Remove(key); - indicator.Dispose(); - ; - } - } - - foreach (var typingUserId in userIds) - { - //Stop the old timer - if (typingIndicators.TryGetValue(typingUserId, out var typingTimer)) - { - typingTimer.Stop(); - } - - //Create and start new timer - var newTimer = new Timer(chat.Config.TypingTimeout); - newTimer.Elapsed += (_, _) => + SetListening(ref typingEventsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) { - typingIndicators.Remove(typingUserId); - OnUsersTyping?.Invoke(typingIndicators.Keys.ToList()); - }; - typingIndicators[typingUserId] = newTimer; - newTimer.Start(); - } - - OnUsersTyping?.Invoke(userIds); + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Typing, out var rawTypingEvent)) + { + try + { + var typingEvent = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(rawTypingEvent + .Payload); + var isTyping = (bool)typingEvent["value"]; + var userId = rawTypingEvent.UserId; + + chat.BroadcastAnyEvent(rawTypingEvent); + + //stop typing + if (!isTyping) + { + if (typingIndicators.TryGetValue(userId, out var timer)) + { + timer.Stop(); + typingIndicators.Remove(userId); + timer.Dispose(); + } + } + //start or restart typing + else + { + //Stop the old timer + if (typingIndicators.TryGetValue(userId, out var typingTimer)) + { + typingTimer.Stop(); + } + + //Create and start new timer + var newTimer = new Timer(chat.Config.TypingTimeout); + newTimer.Elapsed += (_, _) => + { + typingIndicators.Remove(userId); + OnUsersTyping?.Invoke(typingIndicators.Keys.ToList()); + }; + typingIndicators[userId] = newTimer; + newTimer.Start(); + } + OnUsersTyping?.Invoke(typingIndicators.Keys.ToList()); + } + catch (Exception e) + { + chat.Logger.Error($"Error when trying to broadcast typing event on channel \"{Id}\": {e.Message}"); + } + } + })); } - public async Task ForwardMessage(Message message) + /// + /// Sets whether to listen for presence events on this channel. + /// + /// True to start listening, false to stop listening. + public void SetListeningForPresence(bool listen) { - await chat.ForwardMessage(message, this); + SetListening(ref presenceEventsSubscription, SubscriptionOptions.ReceivePresenceEvents, listen, Id, chat.ListenerFactory.ProduceListener(presenceCallback: + async delegate + { + var whoIs = await WhoIsPresent().ConfigureAwait(false); + if (whoIs.Error) + { + chat.Logger.Error($"Error when trying to broadcast presence update after WhoIs(): {whoIs.Exception.Message}"); + } + else + { + OnPresenceUpdate?.Invoke(whoIs.Result); + } + })); } - public async Task EmitUserMention(string userId, string timeToken, string text) + /// + /// Forwards a message to this channel. + /// + /// The message to forward. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task ForwardMessage(Message message) { - CUtilities.CheckCFunctionResult(await Task.Run(() => - pn_channel_emit_user_mention(pointer, userId, timeToken, text))); + return await SendText(message.MessageText, new SendTextParams() + { + Meta = message.Meta + }).ConfigureAwait(false); } - public async Task StartTyping() + /// + /// Emits a user mention event for this channel. + /// + /// The ID of the user being mentioned. + /// The time token of the message containing the mention. + /// The text of the mention. + /// A ChatOperationResult indicating the success or failure of the operation. + public virtual async Task EmitUserMention(string userId, string timeToken, string text) { - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_channel_start_typing(pointer))); + var jsonDict = new Dictionary() + { + {"text",text}, + {"messageTimetoken",timeToken}, + {"channel",Id} + }; + return await chat.EmitEvent(PubnubChatEventType.Mention, userId, + chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)).ConfigureAwait(false); } - public async Task StopTyping() + /// + /// Starts a typing indicator for the current user in this channel. + /// + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task StartTyping() { - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_channel_stop_typing(pointer))); + return await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":true}}").ConfigureAwait(false); } - public virtual async Task PinMessage(Message message) + /// + /// Stops the typing indicator for the current user in this channel. + /// + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task StopTyping() { - var newPointer = await Task.Run(() => pn_channel_pin_message(pointer, message.Pointer)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + return await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":false}}").ConfigureAwait(false); } - public virtual async Task UnpinMessage() + /// + /// Pins a message to this channel. + /// + /// The message to pin. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task PinMessage(Message message) { - var newPointer = await Task.Run(() => pn_channel_unpin_message(pointer)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + channelData.CustomData ??= new (); + channelData.CustomData["pinnedMessageChannelID"] = message.ChannelId; + channelData.CustomData["pinnedMessageTimetoken"] = message.TimeToken; + return (await UpdateChannelData(chat, Id, channelData).ConfigureAwait(false)).ToChatOperationResult("Channel.PinMessage()", chat); } - //TODO: currently same result whether error or no pinned message present /// - /// Tries to get the Message pinned to this Channel. + /// Unpins the currently pinned message from this channel. /// - /// The pinned Message object, null if there wasn't one. - /// True of a pinned Message was found, false otherwise. - /// - public bool TryGetPinnedMessage(out Message pinnedMessage) + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task UnpinMessage() { - var pinnedMessagePointer = pn_channel_get_pinned_message(pointer); - if (pinnedMessagePointer != IntPtr.Zero) - { - var id = Message.GetMessageIdFromPtr(pinnedMessagePointer); - //TODO: this loose wrapper will cause problems of it's own but I don't see another solution for now - //TODO: will be improved with the final ThreadMessage/Message divorce anyway - pinnedMessage = new Message(chat, pinnedMessagePointer, id); - return true; - } - else - { - pinnedMessage = null; - Debug.WriteLine($"Error when fetching pinned message: {CUtilities.GetErrorMessage()}"); - return false; - } + channelData.CustomData ??= new (); + channelData.CustomData.Remove("pinnedMessageChannelID"); + channelData.CustomData.Remove("pinnedMessageTimetoken"); + return (await UpdateChannelData(chat, Id, channelData).ConfigureAwait(false)).ToChatOperationResult("Channel.UnPinMessage()", chat); } + /// /// Asynchronously tries to get the Message pinned to this Channel. /// - /// The pinned Message object if there was one, null otherwise. - public async Task GetPinnedMessageAsync() + /// A ChatOperationResult containing the pinned Message object if there was one, null otherwise. + public async Task> GetPinnedMessage() { - return await Task.Run(() => + var result = new ChatOperationResult("Channel.GetPinnedMessage()", chat); + if (result.RegisterOperation(await Refresh().ConfigureAwait(false))) { - var result = TryGetPinnedMessage(out var pinnedMessage); - return result ? pinnedMessage : null; - }); - } - - public async Task> GetUserSuggestions(string text, int limit = 10) - { - var buffer = new StringBuilder(2048); - CUtilities.CheckCFunctionResult(await Task.Run(() => - pn_channel_get_user_suggestions(pointer, text, limit, buffer))); - var resultJson = buffer.ToString(); - if (!CUtilities.IsValidJson(resultJson)) + return result; + } + if(!CustomData.TryGetValue("pinnedMessageChannelID", out var pinnedChannelId) + || !CustomData.TryGetValue("pinnedMessageTimetoken", out var pinnedMessageTimeToken)) { - return new List(); + result.Error = true; + result.Exception = new PNException($"Channel \"{Id}\" doesn't have a pinned message."); + return result; } - var jsonDict = JsonConvert.DeserializeObject>(resultJson); - if (jsonDict == null || !jsonDict.TryGetValue("value", out var pointers) || pointers == null) + var getMessage = await chat.GetMessage(pinnedChannelId.ToString(), pinnedMessageTimeToken.ToString()).ConfigureAwait(false); + if (result.RegisterOperation(getMessage)) { - return new List(); + return result; } - return PointerParsers.ParseJsonMembershipPointers(chat, pointers); + result.Result = getMessage.Result; + return result; } /// @@ -573,158 +473,162 @@ public async Task> GetUserSuggestions(string text, int limit = /// Typing indicator trigger status. /// User limit. /// Channel limit. - /// + /// Whether the MessageDraft should search for suggestions whenever the text is changed. + /// The created MessageDraft. public MessageDraft CreateMessageDraft(UserSuggestionSource userSuggestionSource = UserSuggestionSource.GLOBAL, - bool isTypingIndicatorTriggered = true, int userLimit = 10, int channelLimit = 10) + bool isTypingIndicatorTriggered = true, int userLimit = 10, int channelLimit = 10, bool shouldSearchForSuggestions = false) { - var draftPointer = pn_channel_create_message_draft_dirty( - pointer, (int)userSuggestionSource, isTypingIndicatorTriggered, userLimit, channelLimit); - CUtilities.CheckCFunctionResult(draftPointer); - return new MessageDraft(draftPointer); + return new MessageDraft(chat, this, userSuggestionSource, isTypingIndicatorTriggered, userLimit, channelLimit, shouldSearchForSuggestions); } - + /// - /// Connects to the channel. + /// Disconnects from the channel. /// - /// Connects to the channel and starts receiving messages. - /// After connecting, the event is triggered when a message is received. + /// Disconnects from the channel and stops receiving messages. + /// Additionally, all the other listeners gets the presence update that the user has left the channel. /// /// /// /// /// var channel = //... - /// channel.OnMessageReceived += (message) => { - /// Console.WriteLine($"Message received: {message.Text}"); - /// }; /// channel.Connect(); + /// //... + /// channel.Disconnect(); /// /// - /// Thrown when an error occurs while connecting to the channel. - /// - /// + /// /// - public async void Connect() + public void Disconnect() { - if (connectionHandle != IntPtr.Zero) - { - return; - } - - connectionHandle = await SetListening(connectionHandle, true, () => pn_channel_connect(pointer)); + SetListening(ref subscription, SubscriptionOptions.None, false, Id, null); } /// - /// Joins the channel. + /// Leaves the channel. /// - /// Joins the channel and starts receiving messages. - /// After joining, the event is triggered when a message is received. - /// Additionally, there is a possibility to add additional parameters to the join request. - /// It also adds the membership to the channel. + /// Leaves the channel and stops receiving messages. + /// Additionally, all the other listeners gets the presence update that the user has left the channel. + /// The membership is also removed from the channel. /// /// /// /// /// var channel = //... - /// channel.OnMessageReceived += (message) => { - /// Console.WriteLine($"Message received: {message.Text}"); - /// }; /// channel.Join(); + /// //... + /// channel.Leave(); /// /// - /// Thrown when an error occurs while joining the channel. - /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// /// /// - public async void Join(ChatMembershipData? membershipData = null) + public async Task Leave() { - if (connectionHandle != IntPtr.Zero) - { - return; - } - - if (membershipData == null) - { - connectionHandle = - await SetListening(connectionHandle, true, () => pn_channel_join(pointer, string.Empty)); - } - else - { - connectionHandle = await SetListening(connectionHandle, true, - () => pn_channel_join_with_membership_data(pointer, membershipData.CustomDataJson, - membershipData.Type, membershipData.Status)); - } + Disconnect(); + var currentUserId = chat.PubnubInstance.GetCurrentUserId(); + return (await chat.PubnubInstance.RemoveMemberships().Uuid(currentUserId).Include(new[] + { + PNMembershipField.TYPE, + PNMembershipField.CUSTOM, + PNMembershipField.STATUS, + PNMembershipField.CHANNEL, + PNMembershipField.CHANNEL_CUSTOM, + PNMembershipField.CHANNEL_TYPE, + PNMembershipField.CHANNEL_STATUS + }).Channels(new List() { Id }) + .ExecuteAsync().ConfigureAwait(false)).ToChatOperationResult("Channel.Leave()", chat); } /// - /// Disconnects from the channel. + /// Connects to the channel. /// - /// Disconnects from the channel and stops receiving messages. - /// Additionally, all the other listeners gets the presence update that the user has left the channel. + /// Connects to the channel and starts receiving messages. + /// After connecting, the event is triggered when a message is received. /// /// /// /// /// var channel = //... + /// channel.OnMessageReceived += (message) => { + /// Console.WriteLine($"Message received: {message.Text}"); + /// }; /// channel.Connect(); - /// //... - /// channel.Disconnect(); /// /// - /// Thrown when an error occurs while disconnecting from the channel. - /// + /// + /// /// - public void Disconnect() + public void Connect() { - if (connectionHandle == IntPtr.Zero || pointer == IntPtr.Zero) - { - return; - } - - CUtilities.CheckCFunctionResult(pn_channel_disconnect(pointer)); - pn_callback_handle_dispose(connectionHandle); - connectionHandle = IntPtr.Zero; + SetListening(ref subscription, SubscriptionOptions.None, true, Id, chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseMessageResult(chat, m, out var message)) + { + OnMessageReceived?.Invoke(message); + } + })); } - + /// - /// Leaves the channel. + /// Joins the channel. /// - /// Leaves the channel and stops receiving messages. - /// Additionally, all the other listeners gets the presence update that the user has left the channel. - /// The membership is also removed from the channel. + /// Joins the channel and starts receiving messages. + /// After joining, the event is triggered when a message is received. + /// Additionally, there is a possibility to add additional parameters to the join request. + /// It also adds the membership to the channel. /// /// /// /// /// var channel = //... + /// channel.OnMessageReceived += (message) => { + /// Console.WriteLine($"Message received: {message.Text}"); + /// }; /// channel.Join(); - /// //... - /// channel.Leave(); /// /// - /// Thrown when an error occurs while leaving the channel. - /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// /// /// - public async void Leave() + public async Task Join(ChatMembershipData? membershipData = null) { - if (connectionHandle == IntPtr.Zero || pointer == IntPtr.Zero) + var result = new ChatOperationResult("Channel.Join()", chat); + membershipData ??= new ChatMembershipData(); + var currentUserId = chat.PubnubInstance.GetCurrentUserId(); + var setMembership = await chat.PubnubInstance.SetMemberships().Uuid(currentUserId) + .Channels(new List() + { + new PNMembership() + { + Channel = Id, + Custom = membershipData.CustomData, + Status = membershipData.Status, + Type = membershipData.Type + } + }) + .Include(new [] + { + PNMembershipField.TYPE, + PNMembershipField.CUSTOM, + PNMembershipField.STATUS, + PNMembershipField.CHANNEL, + PNMembershipField.CHANNEL_CUSTOM + }).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(setMembership)) { - return; + return result; } - - var connectionHandleCopy = connectionHandle; - connectionHandle = IntPtr.Zero; - CUtilities.CheckCFunctionResult(await Task.Run(() => + var joinMembership = new Membership(chat, currentUserId, Id, membershipData); + var setLast = await joinMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()).ConfigureAwait(false); + if (result.RegisterOperation(setLast)) { - if (pointer == IntPtr.Zero) - { - return 0; - } - - pn_channel_leave(pointer); - pn_callback_handle_dispose(connectionHandleCopy); - return 0; - })); + return result; + } + Connect(); + return result; } /// @@ -738,24 +642,28 @@ public async void Leave() /// if set to true the user is banned. /// if set to true the user is muted. /// The reason for the restrictions. + /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// var channel = //... - /// channel.SetRestrictions("user1", true, false, "Spamming"); + /// var result = await channel.SetRestrictions("user1", true, false, "Spamming"); /// /// - /// Thrown when an error occurs while setting the restrictions. /// - public async Task SetRestrictions(string userId, bool banUser, bool muteUser, string reason) + public async Task SetRestrictions(string userId, bool banUser, bool muteUser, string reason) { - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_channel_set_restrictions(pointer, userId, banUser, - muteUser, - reason))); + return await chat.SetRestriction(userId, Id, banUser, muteUser, reason).ConfigureAwait(false); } - public async Task SetRestrictions(string userId, Restriction restriction) + /// + /// Sets the restrictions for the user using a Restriction object. + /// + /// The user identifier. + /// The restriction object containing ban, mute, and reason information. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task SetRestrictions(string userId, Restriction restriction) { - await SetRestrictions(userId, restriction.Ban, restriction.Mute, restriction.Reason); + return await SetRestrictions(userId, restriction.Ban, restriction.Mute, restriction.Reason).ConfigureAwait(false); } /// @@ -766,31 +674,99 @@ public async Task SetRestrictions(string userId, Restriction restriction) /// /// /// The message to be sent. + /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// var channel = //... - /// channel.SendText("Hello, World!"); + /// var result = await channel.SendText("Hello, World!"); /// /// - /// Thrown when an error occurs while sending the message. /// - public virtual async Task SendText(string message) + public async Task SendText(string message) { - await SendText(message, new SendTextParams()); + return await SendText(message, new SendTextParams()).ConfigureAwait(false); } - public virtual async Task SendText(string message, SendTextParams sendTextParams) + /// + /// Sends the text message with additional parameters. + /// + /// Sends the text message to the channel with additional options such as metadata, quoted messages, and mentioned users. + /// + /// + /// The message to be sent. + /// Additional parameters for sending the message. + /// A ChatOperationResult indicating the success or failure of the operation. + public virtual async Task SendText(string message, SendTextParams sendTextParams) { - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_channel_send_text_dirty( - pointer, - message, - sendTextParams.StoreInHistory, - sendTextParams.SendByPost, - sendTextParams.Meta, - sendTextParams.MentionedUsers.Count, - sendTextParams.MentionedUsers.Keys.ToArray(), - sendTextParams.MentionedUsers.Values.Select(x => x.Pointer).ToArray(), - sendTextParams.QuotedMessage == null ? IntPtr.Zero : sendTextParams.QuotedMessage.Pointer))); + var result = new ChatOperationResult("Channel.SendText()", chat); + + var baseInterval = Type switch + { + "public" => chat.Config.RateLimitsPerChannel.PublicConversation, + "direct" => chat.Config.RateLimitsPerChannel.DirectConversation, + "group" => chat.Config.RateLimitsPerChannel.GroupConversation, + _ => chat.Config.RateLimitsPerChannel.UnknownConversation + }; + + TaskCompletionSource completionSource = new (); + chat.RateLimiter.RunWithinLimits(Id, baseInterval, async () => + { + var messageDict = new Dictionary() + { + {"text", message}, + {"type", "text"} + }; + var meta = sendTextParams.Meta ?? new Dictionary(); + if (sendTextParams.QuotedMessage != null) + { + //TODO: may create some "ToJSON()" methods for chat entities + //TODO: what about edited messages?? + meta.Add("quotedMessage", new Dictionary() + { + {"timetoken", sendTextParams.QuotedMessage.TimeToken}, + {"text", sendTextParams.QuotedMessage.OriginalMessageText}, + {"userId", sendTextParams.QuotedMessage.UserId}, + {"channelId", sendTextParams.QuotedMessage.ChannelId}, + }); + } + if (sendTextParams.MentionedUsers.Any()) + { + meta.Add("mentionedUsers", sendTextParams.MentionedUsers); + } + + var publishResult = await chat.PubnubInstance.Publish() + .Channel(Id) + .ShouldStore(sendTextParams.StoreInHistory) + .UsePOST(sendTextParams.SendByPost) + .Message(chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(messageDict)) + .Meta(meta) + .ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(publishResult)) + { + return result; + } + foreach (var mention in sendTextParams.MentionedUsers) + { + result.RegisterOperation(await EmitUserMention(mention.Value.Id, + publishResult.Result.Timetoken.ToString(), message).ConfigureAwait(false)); + } + return result; + }, response => + { + if (result.Error) + { + chat.Logger.Error($"Error occured when trying to SendText(): {result.Exception.Message}"); + } + completionSource.SetResult(true); + }, exception => + { + chat.Logger.Error($"Error occured when trying to SendText(): {exception.Message}"); + completionSource.SetResult(true); + }); + + await completionSource.Task.ConfigureAwait(false); + + return result; } /// @@ -801,10 +777,11 @@ public virtual async Task SendText(string message, SendTextParams sendTextParams /// /// /// The updated data of the channel. + /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// var channel = //... - /// channel.UpdateChannel(new ChatChannelData { + /// var result = await channel.Update(new ChatChannelData { /// Name = "newName", /// Description = "newDescription", /// CustomDataJson = "{\"key\": \"value\"}", @@ -812,12 +789,11 @@ public virtual async Task SendText(string message, SendTextParams sendTextParams /// }); /// /// - /// Thrown when an error occurs while updating the channel. /// /// - public async Task Update(ChatChannelData updatedData) + public async Task Update(ChatChannelData updatedData) { - await chat.UpdateChannel(Id, updatedData); + return await chat.UpdateChannel(Id, updatedData).ConfigureAwait(false); } /// @@ -826,74 +802,120 @@ public async Task Update(ChatChannelData updatedData) /// Deletes the channel and removes all the messages and memberships from the channel. /// /// + /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// var channel = //... - /// channel.DeleteChannel(); + /// var result = await channel.Delete(); /// /// - /// Thrown when an error occurs while deleting the channel. - public async Task Delete() + public async Task Delete() { - await chat.DeleteChannel(Id); + return await chat.DeleteChannel(Id).ConfigureAwait(false); } /// - /// Gets the user restrictions. + /// Gets the user restrictions for a specific user. /// - /// Gets the user restrictions that include the information about the bans and mutes. + /// Gets the user restrictions that include the information about the bans and mutes for the specified user. /// /// - /// The user identifier. - /// The maximum amount of the restrictions received. - /// The start timetoken of the restrictions. - /// The end timetoken of the restrictions. - /// The user restrictions in JSON format. + /// The user to get restrictions for. + /// A ChatOperationResult containing the Restriction object if restrictions exist for the user, error otherwise. /// /// /// var channel = //... - /// var restrictions = channel.GetUserRestrictions( - /// "user1", - /// 10, - /// "16686902600029072" - /// "16686902600028961", - /// ); + /// var user = //... + /// var result = await channel.GetUserRestrictions(user); + /// var restriction = result.Result; /// /// - /// Thrown when an error occurs while getting the user restrictions. /// - public async Task GetUserRestrictions(User user) + public async Task> GetUserRestrictions(User user) { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(await Task.Run(() => - pn_channel_get_user_restrictions(pointer, user.Pointer, buffer))); - var restrictionJson = buffer.ToString(); - var restriction = new Restriction(); - if (CUtilities.IsValidJson(restrictionJson)) + var result = new ChatOperationResult("Channel.GetUserRestrictions()", chat); + var membershipsResult = await chat.PubnubInstance.GetMemberships().Uuid(user.Id).Include(new[] { - restriction = JsonConvert.DeserializeObject(restrictionJson); + PNMembershipField.CUSTOM + }).Filter($"channel.id == \"{Chat.INTERNAL_MODERATION_PREFIX}_{Id}\"").IncludeCount(true).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(membershipsResult) || membershipsResult.Result.Memberships == null || !membershipsResult.Result.Memberships.Any()) + { + result.Error = true; + return result; } - - return restriction; + var membership = membershipsResult.Result.Memberships[0]; + try + { + result.Result = new Restriction() + { + Ban = (bool)membership.Custom["ban"], + Mute = (bool)membership.Custom["mute"], + Reason = (string)membership.Custom["reason"] + }; + } + catch (Exception e) + { + result.Error = true; + result.Exception = e; + } + return result; } - public async Task GetUsersRestrictions(string sort = "", int limit = 0, - Page page = null) + /// + /// Gets all user restrictions for this channel. + /// + /// Sort criteria for restrictions. + /// The maximum number of restrictions to retrieve. + /// Pagination object for retrieving specific page results. + /// A ChatOperationResult containing the wrapper with all user restrictions for this channel. + public async Task> GetUsersRestrictions(string sort = "", int limit = 0, + PNPageObject page = null) { - page ??= new Page(); - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult( - await Task.Run(() => - pn_channel_get_users_restrictions(pointer, sort, limit, page.Next, page.Previous, buffer))); - var restrictionsJson = buffer.ToString(); - if (!CUtilities.IsValidJson(restrictionsJson)) + var result = new ChatOperationResult("Channel.GetUsersRestrictions()", chat){Result = new UsersRestrictionsWrapper()}; + var operation = chat.PubnubInstance.GetChannelMembers().Channel($"{Chat.INTERNAL_MODERATION_PREFIX}_{Id}") + .Include(new[] + { + PNChannelMemberField.CUSTOM, + PNChannelMemberField.UUID + }).IncludeCount(true); + if (!string.IsNullOrEmpty(sort)) + { + operation = operation.Sort(new List() { sort }); + } + if (limit > 0) + { + operation = operation.Limit(limit); + } + if (page != null) { - return new UsersRestrictionsWrapper(); + operation = operation.Page(page); + } + var membersResult = await operation.ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(membersResult)) + { + return result; } - var wrapper = JsonConvert.DeserializeObject(restrictionsJson); - wrapper ??= new UsersRestrictionsWrapper(); - return wrapper; + result.Result.Page = membersResult.Result.Page; + result.Result.Total = membersResult.Result.TotalCount; + foreach (var member in membersResult.Result.ChannelMembers) + { + try + { + result.Result.Restrictions.Add(new UserRestriction() + { + Ban = (bool)member.Custom["ban"], + Mute = (bool)member.Custom["mute"], + Reason = (string)member.Custom["reason"], + UserId = member.UuidMetadata.Uuid + }); + } + catch (Exception e) + { + chat.Logger.Warn($"Incorrect data was encountered when parsing Channel Restriction for User \"{member.UuidMetadata.Uuid}\" in Channel \"{Id}\". Exception was: {e.Message}"); + } + } + return result; } /// @@ -903,21 +925,26 @@ await Task.Run(() => /// /// /// The user identifier. - /// true if the user is present in the channel; otherwise, false. + /// A ChatOperationResult containing true if the user is present in the channel; otherwise, false. /// /// /// var channel = //... - /// var isUserPresent = channel.IsUserPresent("user1"); + /// var result = await channel.IsUserPresent("user1"); + /// var isUserPresent = result.Result; /// Console.WriteLine($"User present: {isUserPresent}"); /// /// - /// Thrown when an error occurs while checking the presence of the user. /// - public async Task IsUserPresent(string userId) + public async Task> IsUserPresent(string userId) { - var result = await Task.Run(() => pn_channel_is_present(pointer, userId)); - CUtilities.CheckCFunctionResult(result); - return result == 1; + var result = new ChatOperationResult("Channel.IsUserPresent()", chat); + var wherePresent = await chat.WherePresent(userId).ConfigureAwait(false); + if (result.RegisterOperation(wherePresent)) + { + return result; + } + result.Result = wherePresent.Result.Contains(Id); + return result; } /// @@ -926,31 +953,33 @@ public async Task IsUserPresent(string userId) /// Gets all the users that are present in the channel. /// /// - /// The list of users present in the channel. + /// A ChatOperationResult containing the list of users present in the channel. /// /// /// var channel = //... - /// var users = channel.WhoIsPresent(); + /// var result = await channel.WhoIsPresent(); + /// var users = result.Result; /// foreach (var user in users) { /// Console.WriteLine($"User present: {user}"); /// } /// /// - /// Thrown when an error occurs while getting the list of users present in the channel. /// - public async Task> WhoIsPresent() + public async Task>> WhoIsPresent() { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_channel_who_is_present(pointer, buffer))); - var jsonResult = buffer.ToString(); - var ret = new List(); - if (CUtilities.IsValidJson(jsonResult)) + var result = new ChatOperationResult>("Channel.WhoIsPresent()", chat) { Result = new List() }; + var response = await chat.PubnubInstance.HereNow().Channels(new[] { Id }).IncludeState(true) + .IncludeUUIDs(true).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(response)) { - ret = JsonConvert.DeserializeObject>(jsonResult); - ret ??= new List(); + return result; } - return ret; + foreach (var occupant in response.Result.Channels[Id].Occupants) + { + result.Result.Add(occupant.Uuid); + } + return result; } /// @@ -960,106 +989,69 @@ public async Task> WhoIsPresent() /// of the channel and the relationships between the users and the channel. /// /// + /// The filter parameter. + /// The sort parameter. /// The maximum amount of the memberships received. - /// The start timetoken of the memberships. - /// The end timetoken of the memberships. - /// The list of the Membership objects. + /// The page object for pagination. + /// A ChatOperationResult containing the list of the Membership objects. /// /// /// var channel = //... - /// var memberships = channel.GetMemberships(10, "16686902600029072", "16686902600028961"); + /// var result = await channel.GetMemberships(limit: 10); + /// var memberships = result.Result.Memberships; /// foreach (var membership in memberships) { /// Console.WriteLine($"Membership: {membership.UserId}"); /// } /// /// - /// Thrown when an error occurs while getting the list of memberships. /// - public async Task GetMemberships(string filter = "", string sort = "", int limit = 0, - Page page = null) - { - return await chat.GetChannelMemberships(Id, filter, sort, limit, page); - } - - /// - /// Gets the Message object for the given timetoken. - /// - /// Gets the Message object for the given timetoken. - /// The timetoken is used to identify the message. - /// - /// - /// The timetoken of the message. - /// The out parameter that contains the Message object. - /// true if the message is found; otherwise, false. - /// - /// - /// var channel = //... - /// if (channel.TryGetMessage("16686902600029072", out var message)) { - /// Console.WriteLine($"Message: {message.Text}"); - /// } - /// - /// - /// - /// - public bool TryGetMessage(string timeToken, out Message message) + public async Task> GetMemberships(string filter = "", string sort = "", int limit = 0, + PNPageObject page = null) { - return chat.TryGetMessage(Id, timeToken, out message); + return await chat.GetChannelMemberships(Id, filter, sort, limit, page).ConfigureAwait(false); } /// /// Asynchronously gets the Message object for the given timetoken sent from this Channel. /// /// TimeToken of the searched-for message. - /// Message object if one was found, null otherwise. - public async Task GetMessageAsync(string timeToken) + /// A ChatOperationResult containing the Message object if one was found, null otherwise. + public async Task> GetMessage(string timeToken) { - return await chat.GetMessageAsync(Id, timeToken); + return await chat.GetMessage(Id, timeToken).ConfigureAwait(false); } - public async Task> GetMessageHistory(string startTimeToken, string endTimeToken, + /// + /// Gets the message history for this channel within a specified time range. + /// + /// The start time token for the history range. + /// The end time token for the history range. + /// The maximum number of messages to retrieve. + /// A ChatOperationResult containing the list of messages from this channel. + public async Task>> GetMessageHistory(string startTimeToken, string endTimeToken, int count) { - return await chat.GetChannelMessageHistory(Id, startTimeToken, endTimeToken, count); - } - - public async Task Invite(User user) - { - var membershipPointer = await Task.Run(() => pn_channel_invite_user(pointer, user.Pointer)); - CUtilities.CheckCFunctionResult(membershipPointer); - var membershipId = Membership.GetMembershipIdFromPtr(membershipPointer); - chat.TryGetMembership(membershipId, membershipPointer, out var membership); - return membership; + return await chat.GetChannelMessageHistory(Id, startTimeToken, endTimeToken, count).ConfigureAwait(false); } - - public async Task> InviteMultiple(List users) - { - var buffer = new StringBuilder(8192); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_channel_invite_multiple(pointer, - users.Select(x => x.Pointer).ToArray(), - users.Count, buffer))); - return PointerParsers.ParseJsonMembershipPointers(chat, buffer.ToString()); - } - - protected override async Task CleanupConnectionHandles() + + /// + /// Invites a user to this channel. + /// + /// The user to invite. + /// A ChatOperationResult containing the created membership for the invited user. + public async Task> Invite(User user) { - await base.CleanupConnectionHandles(); - customEventsListeningHandle = await SetListening(customEventsListeningHandle, false, - () => chat.ListenForEvents(Id, PubnubChatEventType.Custom)); - reportEventsListeningHandle = await SetListening(reportEventsListeningHandle, false, - () => pn_channel_stream_message_reports(pointer)); - readReceiptsListeningHandle = await SetListening(readReceiptsListeningHandle, false, - () => pn_channel_stream_read_receipts(pointer)); - typingListeningHandle = await SetListening(typingListeningHandle, false, - () => pn_channel_get_typing(pointer)); - presenceListeningHandle = await SetListening(presenceListeningHandle, false, - () => pn_channel_stream_presence(pointer)); - Disconnect(); + return await chat.InviteToChannel(Id, user.Id).ConfigureAwait(false); } - - protected override void DisposePointer() + + /// + /// Invites multiple users to this channel. + /// + /// The list of users to invite. + /// A ChatOperationResult containing a list of created memberships for the invited users. + public async Task>> InviteMultiple(List users) { - pn_channel_delete(pointer); - pointer = IntPtr.Zero; + return await chat.InviteMultipleToChannel(Id, users).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 4711f76..170400f 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -1,18 +1,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using PubnubChatApi.Entities.Data; -using PubnubChatApi.Entities.Events; -using PubnubChatApi.Enums; -using PubnubChatApi.Utilities; - -namespace PubNubChatAPI.Entities +using PubnubApi; + +namespace PubnubChatApi { //TODO: make IDisposable? /// @@ -27,661 +19,117 @@ namespace PubNubChatAPI.Entities /// public class Chat { - #region DLL Imports - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_chat_new( - string publish, - string subscribe, - string user_id, - string auth_key, - int typing_timeout, - int typing_timeout_difference, - int store_user_activity_interval, - bool store_user_activity_timestamps); - - [DllImport("pubnub-chat")] - private static extern void pn_chat_delete(IntPtr chat); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_chat_create_public_conversation_dirty(IntPtr chat, - string channel_id, - string channel_name, - string channel_description, - string channel_custom_data_json, - string channel_updated, - string channel_status, - string channel_type); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_chat_update_channel_dirty(IntPtr chat, - string channel_id, - string channel_name, - string channel_description, - string channel_custom_data_json, - string channel_updated, - string channel_status, - string channel_type); - - [DllImport("pubnub-chat")] - private static extern int pn_chat_set_restrictions(IntPtr chat, - string user_id, - string channel_id, - bool ban_user, - bool mute_user, - string reason); - - [DllImport("pubnub-chat")] - private static extern int pn_chat_delete_channel(IntPtr chat, string channel_id); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_chat_create_user_dirty(IntPtr chat, - string user_id, - string user_name, - string external_id, - string profile_url, - string email, - string custom_data_json, - string status, - string type); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_chat_update_user_dirty(IntPtr chat, - string user_id, - string user_name, - string external_id, - string profile_url, - string email, - string custom_data_json, - string status, - string type); - - [DllImport("pubnub-chat")] - private static extern int pn_chat_delete_user(IntPtr chat, string user_id); - - [DllImport("pubnub-chat")] - private static extern int pn_c_consume_response_buffer(IntPtr chat, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_chat_get_users( - IntPtr chat, - string filter, - string sort, - int limit, - string next, - string prev, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_chat_get_user( - IntPtr chat, - string user_id); - - [DllImport("pubnub-chat")] - public static extern IntPtr pn_chat_get_channel( - IntPtr chat, - string channel_id); - - [DllImport("pubnub-chat")] - private static extern int pn_user_get_memberships( - IntPtr user, - string filter, - string sort, - int limit, - string next, - string prev, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_get_members( - IntPtr channel, - string filter, - string sort, - int limit, - string next, - string prev, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_channel_get_message(IntPtr channel, string timetoken); - - [DllImport("pubnub-chat")] - private static extern int pn_channel_get_history( - IntPtr channel, - string start, - string end, - int count, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_chat_get_channels( - IntPtr chat, - string filter, - string sort, - int limit, - string next, - string prev, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_chat_listen_for_events( - IntPtr chat, - string channel_id, - byte event_type); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_chat_create_direct_conversation_dirty( - IntPtr chat, - IntPtr user, string channel_id, - string channel_name, - string channel_description, - string channel_custom_data_json, - string channel_updated, - string channel_status, - string channel_type); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_chat_create_group_conversation_dirty( - IntPtr chat, - IntPtr[] users, - int users_length, string channel_id, - string channel_name, - string channel_description, - string channel_custom_data_json, - string channel_updated, - string channel_status, - string channel_type); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_chat_get_created_channel_wrapper_channel( - IntPtr wrapper); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_chat_get_created_channel_wrapper_host_membership( - IntPtr wrapper); - - [DllImport("pubnub-chat")] - private static extern int pn_chat_get_created_channel_wrapper_invited_memberships( - IntPtr wrapper, StringBuilder result_json); - - [DllImport("pubnub-chat")] - private static extern void pn_chat_dispose_created_channel_wrapper(IntPtr wrapper); - - [DllImport("pubnub-chat")] - private static extern int pn_chat_forward_message(IntPtr chat, IntPtr message, IntPtr channel); - - [DllImport("pubnub-chat")] - private static extern int pn_chat_emit_event(IntPtr chat, byte chat_event_type, string channel_id, - string payload); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_message_create_thread(IntPtr message); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_message_get_thread(IntPtr message); - - [DllImport("pubnub-chat")] - private static extern int pn_message_remove_thread(IntPtr message); - - [DllImport("pubnub-chat")] - private static extern int pn_chat_get_unread_messages_counts( - IntPtr chat, - string filter, - string sort, - int limit, - string next, - string prev, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_chat_mark_all_messages_as_read( - IntPtr chat, - string filter, - string sort, - int limit, - string next, - string prev, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_chat_get_events_history( - IntPtr chat, - string channel_id, - string start_timetoken, - string end_timetoken, - int count, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_chat_get_user_suggestions(IntPtr chat, string text, int limit, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_chat_current_user(IntPtr chat); - - [DllImport("pubnub-chat")] - private static extern int pn_chat_get_current_user_mentions(IntPtr chat, string start_timetoken, - string end_timetoken, int count, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr - pn_chat_create_direct_conversation_dirty_with_membership_data( - IntPtr chat, - IntPtr user, - string channel_id, - string channel_name, - string channel_description, - string channel_custom_data_json, - string channel_updated, - string channel_status, - string channel_type, - string membership_custom_json, - string membership_type, - string membership_status - ); - - [DllImport("pubnub-chat")] - private static extern IntPtr - pn_chat_create_group_conversation_dirty_with_membership_data( - IntPtr chat, - IntPtr[] users, - int users_length, - string channel_id, - string channel_name, - string channel_description, - string channel_custom_data_json, - string channel_updated, - string channel_status, - string channel_type, - string membership_custom_json, - string membership_type, - string membership_status - ); - - #endregion - - private IntPtr chatPointer; - internal IntPtr Pointer => chatPointer; - private Dictionary channelWrappers = new(); - private Dictionary userWrappers = new(); - private Dictionary membershipWrappers = new(); - private Dictionary messageWrappers = new(); - private bool fetchUpdates = true; + internal const string INTERNAL_MODERATION_PREFIX = "PUBNUB_INTERNAL_MODERATION"; + internal const string MESSAGE_THREAD_ID_PREFIX = "PUBNUB_INTERNAL_THREAD"; + + public Pubnub PubnubInstance { get; } + public PubnubLogModule Logger => PubnubInstance.PNConfig.Logger; + + internal ChatListenerFactory ListenerFactory { get; } public event Action OnAnyEvent; public ChatAccessManager ChatAccessManager { get; } public PubnubChatConfig Config { get; } + internal ExponentialRateLimiter RateLimiter { get; } + + private bool storeActivity = false; /// - /// Asynchronously initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Creates a new chat instance. /// /// - /// Config with PubNub keys and values + /// Config with Chat specific parameters + /// Config with PubNub keys and values + /// Optional injectable listener factory, used in Unity to allow for dispatching Chat callbacks on main thread. + /// A ChatOperationResult containing the created Chat instance. /// - /// The constructor initializes the chat instance with the provided keys and user ID from the Config. + /// The constructor initializes the Chat object with a new Pubnub instance. /// - public static async Task CreateInstance(PubnubChatConfig config) - { - var chat = await Task.Run(() => new Chat(config)); - chat.FetchUpdatesLoop(); - return chat; - } - - internal Chat(PubnubChatConfig config) + public static async Task> CreateInstance(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatListenerFactory? listenerFactory = null) { - chatPointer = pn_chat_new(config.PublishKey, config.SubscribeKey, config.UserId, config.AuthKey, - config.TypingTimeout, config.TypingTimeoutDifference, config.StoreUserActivityInterval, - config.StoreUserActivityTimestamp); - CUtilities.CheckCFunctionResult(chatPointer); - - Config = config; - ChatAccessManager = new ChatAccessManager(chatPointer); - } - - #region Updates handling - - //TODO: cancellation token? - internal async Task FetchUpdatesLoop() - { - while (fetchUpdates) + var chat = new Chat(chatConfig, pubnubConfig, listenerFactory); + if (chatConfig.StoreUserActivityTimestamp) { - var updates = GetUpdates(); - try - { - ParseJsonUpdatePointers(updates); - } - catch (Exception e) - { - Debug.WriteLine($"Error when parsing JSON updates: {e}"); - } - - await Task.Delay(200); + chat.StoreActivityTimeStamp(); } + var result = new ChatOperationResult("Chat.CreateInstance()", chat){Result = chat}; + var getUser = await chat.GetCurrentUser().ConfigureAwait(false); + if (getUser.Error) + { + result.RegisterOperation(await chat.CreateUser(chat.PubnubInstance.GetCurrentUserId()).ConfigureAwait(false)); + } + return result; } - - internal void ParseJsonUpdatePointers(string jsonString) + + /// + /// Initializes a new instance of the class. + /// + /// Creates a new chat instance. + /// + /// + /// Config with Chat specific parameters + /// An existing Pubnub object instance + /// Optional injectable listener factory, used in Unity to allow for dispatching Chat callbacks on main thread. + /// A ChatOperationResult containing the created Chat instance. + /// + /// The constructor initializes the Chat object with an existing Pubnub instance. + /// + public static async Task> CreateInstance(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? listenerFactory = null) { - if (string.IsNullOrEmpty(jsonString) || jsonString == "[]") + var chat = new Chat(chatConfig, pubnub, listenerFactory); + if (chatConfig.StoreUserActivityTimestamp) { - return; + chat.StoreActivityTimeStamp(); } - - Debug.WriteLine($"Received JSON to parse: {jsonString}"); - - var jArray = JArray.Parse(jsonString); - - var updates = jArray - .Children() - .SelectMany(jo => jo.Properties()) - .GroupBy(jp => jp.Name) - .ToDictionary( - grp => grp.Key, - grp => grp.SelectMany(jp => - jp.Value is JArray ? jp.Value.Values() : new[] { jp.Value.ToString() }).ToList() - ); - - foreach (var update in updates) + var result = new ChatOperationResult("Chat.CreateInstance()", chat){Result = chat}; + var getUser = await chat.GetCurrentUser().ConfigureAwait(false); + if (getUser.Error) { - foreach (var json in update.Value) - { - if (json == null) - { - continue; - } - - Debug.WriteLine($"Parsing JSON:\n--Key: {update.Key},\n--Value: {json}"); - - switch (update.Key) - { - // {"channel_id": "", "data" : [{"": ["", ""]}]} - case "read_receipts": - var jObject = JObject.Parse(json); - if (!jObject.TryGetValue("channel_id", out var readChannelId) - || !jObject.TryGetValue("data", out var data)) - { - Debug.WriteLine("Incorrect read recepits JSON payload!"); - continue; - } - - if (!TryGetChannel(readChannelId.ToString(), out var readReceiptChannel)) - { - Debug.WriteLine("Can't find the read receipt channel!"); - continue; - } - - var receipts = data.Children() - .SelectMany(j => j.Children()) - .ToDictionary(jp => jp.Name, jp => jp.Value.ToObject>()); - readReceiptChannel.BroadcastReadReceipt(receipts); - OnAnyEvent?.Invoke(new ChatEvent() - { - ChannelId = readChannelId.ToString(), - Type = PubnubChatEventType.Receipt, - Payload = json - }); - break; - case "typing_users": - var typings = JsonConvert.DeserializeObject>>(json); - if (typings == null) - { - continue; - } - - foreach (var kvp in typings) - { - if (TryGetChannel(kvp.Key, out var typingChannel)) - { - typingChannel.TryParseAndBroadcastTypingEvent(kvp.Value); - OnAnyEvent?.Invoke(new ChatEvent() - { - ChannelId = kvp.Key, - Payload = json, - Type = PubnubChatEventType.Typing - }); - } - } - - break; - case "event": - case "message_report": - Debug.WriteLine("Deserialized event / message report"); - - if (!CUtilities.IsValidJson(json)) - { - break; - } - - var chatEvent = JsonConvert.DeserializeObject(json); - var invoked = false; - //TODO: not a big fan of this big-ass switch - switch (chatEvent.Type) - { - case PubnubChatEventType.Report: - var moderationPrefix = "PUBNUB_INTERNAL_MODERATION_"; - var index = chatEvent.ChannelId.IndexOf(moderationPrefix, StringComparison.Ordinal); - var properChannelId = (index < 0) - ? chatEvent.ChannelId - : chatEvent.ChannelId.Remove(index, moderationPrefix.Length); - if (TryGetChannel(properChannelId, out var reportChannel)) - { - reportChannel.BroadcastReportEvent(chatEvent); - invoked = true; - } - - break; - case PubnubChatEventType.Mention: - if (TryGetUser(chatEvent.UserId, out var mentionedUser)) - { - mentionedUser.BroadcastMentionEvent(chatEvent); - invoked = true; - } - - break; - case PubnubChatEventType.Invite: - if (TryGetUser(chatEvent.UserId, out var invitedUser)) - { - invitedUser.BroadcastInviteEvent(chatEvent); - invoked = true; - } - - break; - case PubnubChatEventType.Custom: - if (TryGetChannel(chatEvent.ChannelId, out var customEventChannel)) - { - customEventChannel.BroadcastCustomEvent(chatEvent); - invoked = true; - } - - break; - case PubnubChatEventType.Moderation: - if (TryGetUser(chatEvent.UserId, out var moderatedUser)) - { - moderatedUser.BroadcastModerationEvent(chatEvent); - invoked = true; - } - - break; - default: - throw new ArgumentOutOfRangeException(); - } - - if (invoked) - { - OnAnyEvent?.Invoke(chatEvent); - } - - break; - case "message": - var messagePointer = JsonConvert.DeserializeObject(json); - if (messagePointer != IntPtr.Zero) - { - Debug.WriteLine("Deserialized new message"); - - var id = Message.GetChannelIdFromMessagePtr(messagePointer); - if (channelWrappers.TryGetValue(id, out var channel)) - { - var timeToken = Message.GetMessageIdFromPtr(messagePointer); - var message = new Message(this, messagePointer, timeToken); - //We don't store ThreadMessage wrappers by default, only add them if - //specifically requested/created in TryGetThreadHistory - if (!id.Contains("PUBNUB_INTERNAL_THREAD_")) - { - messageWrappers[timeToken] = message; - } - - channel.BroadcastMessageReceived(message); - } - } - - break; - case "thread_message_update": - var updatedThreadMessagePointer = JsonConvert.DeserializeObject(json); - if (updatedThreadMessagePointer != IntPtr.Zero) - { - Debug.WriteLine("Deserialized thread message update"); - var id = ThreadMessage.GetThreadMessageIdFromPtr(updatedThreadMessagePointer); - if (messageWrappers.TryGetValue(id, out var existingMessageWrapper)) - { - if (existingMessageWrapper is ThreadMessage existingThreadMessageWrapper) - { - existingThreadMessageWrapper.UpdateWithPartialPtr(updatedThreadMessagePointer); - existingThreadMessageWrapper.BroadcastMessageUpdate(); - } - else - { - Debug.WriteLine( - "Thread message was stored as a regular message - SHOULD NEVER HAPPEN!"); - } - } - } - - break; - case "message_update": - var updatedMessagePointer = JsonConvert.DeserializeObject(json); - if (updatedMessagePointer != IntPtr.Zero) - { - Debug.WriteLine("Deserialized message update"); - var id = Message.GetMessageIdFromPtr(updatedMessagePointer); - if (messageWrappers.TryGetValue(id, out var existingMessageWrapper)) - { - existingMessageWrapper.UpdateWithPartialPtr(updatedMessagePointer); - existingMessageWrapper.BroadcastMessageUpdate(); - } - } - - break; - case "channel_update": - var channelPointer = JsonConvert.DeserializeObject(json); - if (channelPointer != IntPtr.Zero) - { - Debug.WriteLine("Deserialized channel update"); - - var id = Channel.GetChannelIdFromPtr(channelPointer); - - //TODO: temporary get_channel update for ThreadChannels - if (id.Contains("PUBNUB_INTERNAL_THREAD")) - { - //This has a check for "PUBNUB_INTERNAL_THREAD" and will correctly update the pointer - TryGetChannel(id, out var existingThreadChannel); - //TODO: broadcast thread channel update (very low priority because I don't think they have that in JS chat) - existingThreadChannel.BroadcastChannelUpdate(); - } - else if (channelWrappers.TryGetValue(id, out var existingChannelWrapper)) - { - existingChannelWrapper.UpdateWithPartialPtr(channelPointer); - existingChannelWrapper.BroadcastChannelUpdate(); - } - } - - break; - case "user_update": - var userPointer = JsonConvert.DeserializeObject(json); - if (userPointer != IntPtr.Zero) - { - Debug.WriteLine("Deserialized user update"); - - var id = User.GetUserIdFromPtr(userPointer); - if (userWrappers.TryGetValue(id, out var existingUserWrapper)) - { - existingUserWrapper.UpdateWithPartialPtr(userPointer); - existingUserWrapper.BroadcastUserUpdate(); - } - } - - break; - case "membership_update": - var membershipPointer = JsonConvert.DeserializeObject(json); - if (membershipPointer != IntPtr.Zero) - { - Debug.WriteLine("Deserialized membership"); - - var id = Membership.GetMembershipIdFromPtr(membershipPointer); - if (membershipWrappers.TryGetValue(id, out var existingMembershipWrapper)) - { - existingMembershipWrapper.UpdateWithPartialPtr(membershipPointer); - existingMembershipWrapper.BroadcastMembershipUpdate(); - } - } - - break; - case "presence_users": - Debug.WriteLine("Deserialized presence update"); - var presenceDictionary = - JsonConvert.DeserializeObject>>(json); - if (presenceDictionary == null) - { - break; - } - - foreach (var pair in presenceDictionary) - { - var channelId = pair.Key; - if (channelId.EndsWith("-pnpres")) - { - channelId = channelId.Substring(0, - channelId.LastIndexOf("-pnpres", StringComparison.Ordinal)); - } - - if (TryGetChannel(channelId, out var channel)) - { - channel.BroadcastPresenceUpdate(); - } - } - - break; - default: - Debug.WriteLine("Wasn't able to deserialize incoming pointer into any known type!"); - break; - } - } + result.RegisterOperation(await chat.CreateUser(chat.PubnubInstance.GetCurrentUserId()).ConfigureAwait(false)); } + return result; } - - internal string GetUpdates() + + internal Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatListenerFactory? listenerFactory = null) { - var messagesBuffer = new StringBuilder(32768); - CUtilities.CheckCFunctionResult(pn_c_consume_response_buffer(chatPointer, messagesBuffer)); - return messagesBuffer.ToString(); + PubnubInstance = new Pubnub(pubnubConfig); + ListenerFactory = listenerFactory ?? new DotNetListenerFactory(); + Config = chatConfig; + ChatAccessManager = new ChatAccessManager(this); + RateLimiter = new ExponentialRateLimiter(chatConfig.RateLimitFactor); } - - #endregion - + + internal Chat(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? listenerFactory = null) + { + Config = chatConfig; + PubnubInstance = pubnub; + ListenerFactory = listenerFactory ?? new DotNetListenerFactory(); + ChatAccessManager = new ChatAccessManager(this); + RateLimiter = new ExponentialRateLimiter(chatConfig.RateLimitFactor); + } + #region Channels - public void AddListenerToChannelsUpdate(List channelIds, Action listener) + /// + /// Adds a listener for channel update events on multiple channels. + /// + /// List of channel IDs to listen to. + /// The listener callback to invoke on channel updates. + public async Task AddListenerToChannelsUpdate(List channelIds, Action listener) { foreach (var channelId in channelIds) { - if (TryGetChannel(channelId, out var channel)) + var getResult = await GetChannel(channelId).ConfigureAwait(false); + if (!getResult.Error) { - channel.OnChannelUpdate += listener; + getResult.Result.OnChannelUpdate += listener; } } } - + /// /// Creates a new public conversation. /// @@ -690,25 +138,26 @@ public void AddListenerToChannelsUpdate(List channelIds, Action /// /// /// The channel ID. - /// The created channel. + /// A ChatOperationResult containing the created Channel object. /// /// The method creates a chat channel with the provided channel ID. /// /// /// /// var chat = // ... - /// var channel = chat.CreatePublicConversation("channel_id"); + /// var result = await chat.CreatePublicConversation("channel_id"); + /// var channel = result.Result; /// /// /// - public async Task CreatePublicConversation(string channelId = "") + public async Task> CreatePublicConversation(string channelId = "") { if (string.IsNullOrEmpty(channelId)) { channelId = Guid.NewGuid().ToString(); } - return await CreatePublicConversation(channelId, new ChatChannelData()); + return await CreatePublicConversation(channelId, new ChatChannelData()).ConfigureAwait(false); } /// @@ -720,245 +169,347 @@ public async Task CreatePublicConversation(string channelId = "") /// /// The channel ID. /// The additional data for the channel. - /// The created channel. + /// A ChatOperationResult containing the created Channel object. /// /// The method creates a chat channel with the provided channel ID. /// /// /// /// var chat = // ... - /// var channel = chat.CreatePublicConversation("channel_id"); + /// var result = await chat.CreatePublicConversation("channel_id"); + /// var channel = result.Result; /// /// /// /// - public async Task CreatePublicConversation(string channelId, ChatChannelData additionalData) + public async Task> CreatePublicConversation(string channelId, ChatChannelData additionalData) { - if (channelWrappers.TryGetValue(channelId, out var existingChannel)) - { - Debug.WriteLine("Trying to create a channel with ID that already exists! Returning existing one."); - return existingChannel; - } - - var channelPointer = await Task.Run(() => pn_chat_create_public_conversation_dirty(chatPointer, channelId, - additionalData.ChannelName, - additionalData.ChannelDescription, - additionalData.ChannelCustomDataJson, - additionalData.ChannelUpdated, - additionalData.ChannelStatus, - additionalData.ChannelType)); - CUtilities.CheckCFunctionResult(channelPointer); - var channel = new Channel(this, channelId, channelPointer); - channelWrappers.Add(channelId, channel); - return channel; + var result = new ChatOperationResult("Chat.CreatePublicConversation()", this); + var existingChannel = await GetChannel(channelId).ConfigureAwait(false); + if (!result.RegisterOperation(existingChannel)) + { + Logger.Debug("Trying to create a channel with ID that already exists! Returning existing one."); + result.Result = existingChannel.Result; + return result; + } + + additionalData.Type = "public"; + var updated = await Channel.UpdateChannelData(this, channelId, additionalData).ConfigureAwait(false); + if (result.RegisterOperation(updated)) + { + return result; + } + var channel = new Channel(this, channelId, additionalData); + result.Result = channel; + return result; } - public async Task CreateDirectConversation(User user, string channelId = "", - ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) + private async Task> CreateConversation( + string type, + List users, + string channelId = "", + ChatChannelData? channelData = null, + ChatMembershipData? membershipData = null) { + var result = new ChatOperationResult($"Chat.CreateConversation-{type}", this){Result = new CreatedChannelWrapper()}; + if (string.IsNullOrEmpty(channelId)) { channelId = Guid.NewGuid().ToString(); } - + + var existingChannel = await GetChannel(channelId).ConfigureAwait(false); + if (!result.RegisterOperation(existingChannel)) + { + Logger.Debug("Trying to create a channel with ID that already exists! Returning existing one."); + result.Result.CreatedChannel = existingChannel.Result; + return result; + } + channelData ??= new ChatChannelData(); - - IntPtr wrapperPointer; - - if (membershipData == null) + channelData.Type = type; + var updated = await Channel.UpdateChannelData(this, channelId, channelData).ConfigureAwait(false); + if (result.RegisterOperation(updated)) { - wrapperPointer = await Task.Run(() => pn_chat_create_direct_conversation_dirty(chatPointer, - user.Pointer, channelId, - channelData.ChannelName, - channelData.ChannelDescription, channelData.ChannelCustomDataJson, channelData.ChannelUpdated, - channelData.ChannelStatus, channelData.ChannelType)); + return result; } - else + + membershipData ??= new ChatMembershipData(); + var currentUserId = PubnubInstance.GetCurrentUserId(); + var setMembershipResult = await PubnubInstance.SetMemberships() + .Uuid(currentUserId) + .Include( + new[] + { + PNMembershipField.CHANNEL_CUSTOM, + PNMembershipField.CUSTOM, + PNMembershipField.CHANNEL, + PNMembershipField.STATUS, + PNMembershipField.TYPE, + }) + .Channels(new List() { new () + { + Channel = channelId, + Custom = membershipData.CustomData, + Status = membershipData.Status, + Type = membershipData.Type + }}) + .ExecuteAsync().ConfigureAwait(false); + + if (result.RegisterOperation(setMembershipResult)) { - wrapperPointer = await Task.Run(() => pn_chat_create_direct_conversation_dirty_with_membership_data( - chatPointer, - user.Pointer, channelId, - channelData.ChannelName, - channelData.ChannelDescription, channelData.ChannelCustomDataJson, channelData.ChannelUpdated, - channelData.ChannelStatus, channelData.ChannelType, membershipData.CustomDataJson, - membershipData.Type, membershipData.Status)); + return result; } - - CUtilities.CheckCFunctionResult(wrapperPointer); - - var createdChannelPointer = pn_chat_get_created_channel_wrapper_channel(wrapperPointer); - CUtilities.CheckCFunctionResult(createdChannelPointer); - TryGetChannel(createdChannelPointer, out var createdChannel); - - var hostMembershipPointer = pn_chat_get_created_channel_wrapper_host_membership(wrapperPointer); - CUtilities.CheckCFunctionResult(hostMembershipPointer); - TryGetMembership(hostMembershipPointer, out var hostMembership); - - var buffer = new StringBuilder(8192); - CUtilities.CheckCFunctionResult( - pn_chat_get_created_channel_wrapper_invited_memberships(wrapperPointer, buffer)); - var inviteeMembership = PointerParsers.ParseJsonMembershipPointers(this, buffer.ToString())[0]; - - pn_chat_dispose_created_channel_wrapper(wrapperPointer); - - return new CreatedChannelWrapper() + + var hostMembership = new Membership(this, currentUserId, channelId, membershipData); + result.Result.HostMembership = hostMembership; + + var channel = new Channel(this, channelId, channelData); + result.Result.CreatedChannel = channel; + + if (type == "direct") { - CreatedChannel = createdChannel, - HostMembership = hostMembership, - InviteesMemberships = new List() { inviteeMembership } - }; + var inviteMembership = await InviteToChannel(channelId, users[0].Id).ConfigureAwait(false); + if (result.RegisterOperation(inviteMembership)) + { + return result; + } + result.Result.InviteesMemberships = new List() { inviteMembership.Result }; + }else if (type == "group") + { + var inviteMembership = await InviteMultipleToChannel(channelId, users).ConfigureAwait(false); + if (result.RegisterOperation(inviteMembership)) + { + return result; + } + result.Result.InviteesMemberships = new List(inviteMembership.Result); + } + return result; } - public async Task CreateGroupConversation(List users, string channelId = "", + /// + /// Creates a direct conversation between the current user and the specified user. + /// + /// The user to create a direct conversation with. + /// Optional channel ID. If not provided, a new GUID will be used. + /// Optional additional channel data. + /// Optional membership data for the conversation. + /// A ChatOperationResult containing the created channel wrapper with channel and membership information. + public async Task> CreateDirectConversation(User user, string channelId = "", ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) { - if (string.IsNullOrEmpty(channelId)) - { - channelId = Guid.NewGuid().ToString(); - } - channelData ??= new ChatChannelData(); + return await CreateConversation("direct", new List() { user }, channelId, channelData, + membershipData).ConfigureAwait(false); + } - IntPtr wrapperPointer; - if (membershipData == null) + /// + /// Creates a group conversation with multiple users. + /// + /// The list of users to include in the group conversation. + /// Optional channel ID. If not provided, a new GUID will be used. + /// Optional additional channel data. + /// Optional membership data for the conversation. + /// A ChatOperationResult containing the created channel wrapper with channel and membership information. + public async Task> CreateGroupConversation(List users, string channelId = "", + ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) + { + return await CreateConversation("group", users, channelId, channelData, + membershipData).ConfigureAwait(false); + } + + /// + /// Invites a user to a channel. + /// + /// The ID of the channel to invite the user to. + /// The ID of the user to invite. + /// A ChatOperationResult containing the created membership for the invited user. + public async Task> InviteToChannel(string channelId, string userId) + { + var result = new ChatOperationResult("Chat.InviteToChannel()", this); + //Check if already a member first + var members = await GetChannelMemberships(channelId, filter:$"uuid.id == \"{userId}\"").ConfigureAwait(false); + if (!result.RegisterOperation(members) && members.Result.Memberships.Any()) { - wrapperPointer = await Task.Run(() => pn_chat_create_group_conversation_dirty(chatPointer, - users.Select(x => x.Pointer).ToArray(), users.Count, channelId, channelData.ChannelName, - channelData.ChannelDescription, channelData.ChannelCustomDataJson, channelData.ChannelUpdated, - channelData.ChannelStatus, channelData.ChannelType)); + //Already a member, just return current membership + result.Result = members.Result.Memberships[0]; + return result; } - else + + var channel = await GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(channel)) { - wrapperPointer = await Task.Run(() => pn_chat_create_group_conversation_dirty_with_membership_data( - chatPointer, - users.Select(x => x.Pointer).ToArray(), users.Count, channelId, channelData.ChannelName, - channelData.ChannelDescription, channelData.ChannelCustomDataJson, channelData.ChannelUpdated, - channelData.ChannelStatus, channelData.ChannelType, membershipData.CustomDataJson, - membershipData.Type, membershipData.Status)); + return result; } - CUtilities.CheckCFunctionResult(wrapperPointer); - - var createdChannelPointer = pn_chat_get_created_channel_wrapper_channel(wrapperPointer); - CUtilities.CheckCFunctionResult(createdChannelPointer); - TryGetChannel(createdChannelPointer, out var createdChannel); - - var hostMembershipPointer = pn_chat_get_created_channel_wrapper_host_membership(wrapperPointer); - CUtilities.CheckCFunctionResult(hostMembershipPointer); - TryGetMembership(hostMembershipPointer, out var hostMembership); + var setMemberships = await PubnubInstance.SetMemberships().Uuid(userId).Include(new[] + { + PNMembershipField.CUSTOM, + PNMembershipField.TYPE, + PNMembershipField.CHANNEL, + PNMembershipField.CHANNEL_CUSTOM, + PNMembershipField.STATUS + }).Channels(new List() + { + new() + { + Channel = channelId, + //TODO: these too here? + //TODO: again, should ChatMembershipData from Create(...)Channel also be passed here? + /*Custom = , + Status = , + Type = */ + } + }).ExecuteAsync().ConfigureAwait(false); - var buffer = new StringBuilder(8192); - CUtilities.CheckCFunctionResult( - pn_chat_get_created_channel_wrapper_invited_memberships(wrapperPointer, buffer)); - var inviteeMemberships = PointerParsers.ParseJsonMembershipPointers(this, buffer.ToString()); + if (result.RegisterOperation(setMemberships)) + { + return result; + } + + var newMataData = setMemberships.Result.Memberships?.FirstOrDefault(x => x.ChannelMetadata.Channel == channelId)? + .ChannelMetadata; + if (newMataData != null) + { + channel.Result.UpdateLocalData(newMataData); + } - pn_chat_dispose_created_channel_wrapper(wrapperPointer); + var inviteEventPayload = $"{{\"channelType\": \"{channel.Result.Type}\", \"channelId\": {channelId}}}"; + await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload).ConfigureAwait(false); + + var newMembership = new Membership(this, userId, channelId, new ChatMembershipData()); + await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()).ConfigureAwait(false); - return new CreatedChannelWrapper() - { - CreatedChannel = createdChannel, - HostMembership = hostMembership, - InviteesMemberships = inviteeMemberships - }; + result.Result = newMembership; + return result; } /// - /// Gets the channel by the provided channel ID. - /// - /// Tries to get the channel by the provided channel ID. - /// + /// Invites multiple users to a channel. /// - /// The channel ID. - /// The out channel. - /// True if the channel was found, false otherwise. - /// - /// - /// var chat = // ... - /// if (chat.TryGetChannel("channel_id", out var channel)) { - /// // Channel found - /// } - /// - /// - /// - /// - public bool TryGetChannel(string channelId, out Channel channel) - { - //Fetching and updating a ThreadChannel - if (channelId.Contains("PUBNUB_INTERNAL_THREAD_")) - { - var channelAndTimeToken = channelId.Replace("PUBNUB_INTERNAL_THREAD_", ""); - var split = channelAndTimeToken.LastIndexOf("_", StringComparison.Ordinal); - var parentChannelId = channelAndTimeToken.Remove(split); - var messageTimeToken = channelAndTimeToken.Remove(0, split + 1); - if (TryGetMessage(parentChannelId, messageTimeToken, out var message) && - TryGetThreadChannel(message, out var threadChannel)) - { - channel = threadChannel; - return true; - } - else - { - Debug.WriteLine("Didn't manage to find the ThreadChannel!"); - channel = null; - return false; - } + /// The ID of the channel to invite users to. + /// The list of users to invite. + /// A ChatOperationResult containing a list of created memberships for the invited users. + public async Task>> InviteMultipleToChannel(string channelId, List users) + { + var result = new ChatOperationResult>("Chat.InviteMultipleToChannel()", this) { Result = new List() }; + var channel = await GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(channel)) + { + return result; } - //Fetching and updating a regular channel - else + var inviteResponse = await PubnubInstance.SetChannelMembers().Channel(channelId) + .Include( + new[] { + PNChannelMemberField.UUID, + PNChannelMemberField.CUSTOM, + PNChannelMemberField.UUID_CUSTOM, + PNChannelMemberField.TYPE, + PNChannelMemberField.STATUS, + PNChannelMemberField.UUID_TYPE, + PNChannelMemberField.UUID_STATUS + }) + //TODO: again, should ChatMembershipData from Create(...)Channel also be passed here? + .Uuids(users.Select(x => new PNChannelMember() { Custom = x.CustomData, Uuid = x.Id }).ToList()) + .ExecuteAsync().ConfigureAwait(false); + + if (result.RegisterOperation(inviteResponse)) { - var channelPointer = pn_chat_get_channel(chatPointer, channelId); - return TryGetChannel(channelId, channelPointer, out channel); + return result; } + + foreach (var channelMember in inviteResponse.Result.ChannelMembers) + { + var userId = channelMember.UuidMetadata.Uuid; + if (!users.Any(x => x.Id == userId)) + { + continue; + } + var newMembership = new Membership(this, userId, channelId, channelMember); + await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()).ConfigureAwait(false); + result.Result.Add(newMembership); + + var inviteEventPayload = $"{{\"channelType\": \"{channel.Result.Type}\", \"channelId\": {channelId}}}"; + await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload).ConfigureAwait(false); + } + + await channel.Result.Refresh().ConfigureAwait(false); + + return result; } /// /// Performs an async retrieval of a Channel object with a given ID. /// /// ID of the channel. - /// Channel object if it exists, null otherwise. - public async Task GetChannelAsync(string channelId) + /// A ChatOperationResult containing the Channel object if it exists, null otherwise. + public async Task> GetChannel(string channelId) { - return await Task.Run(() => + var result = new ChatOperationResult("Chat.GetChannel()", this); + var getResult = await Channel.GetChannelData(this, channelId).ConfigureAwait(false); + if (result.RegisterOperation(getResult)) { - var result = TryGetChannel(channelId, out var channel); - return result ? channel : null; - }); - } - - internal bool TryGetChannel(IntPtr channelPointer, out Channel channel) - { - var channelId = Channel.GetChannelIdFromPtr(channelPointer); - return TryGetChannel(channelId, channelPointer, out channel); - } - - internal bool TryGetChannel(string channelId, IntPtr channelPointer, out Channel channel) - { - return TryGetWrapper(channelWrappers, channelId, channelPointer, - () => new Channel(this, channelId, channelPointer), out channel); - } - - //The TryGetChannel updates the pointer, these methods are for internal logic explicity sake - internal void UpdateChannelPointer(IntPtr newPointer) - { - TryGetChannel(newPointer, out _); - } - - internal void UpdateChannelPointer(string id, IntPtr newPointer) - { - TryGetChannel(id, newPointer, out _); + return result; + } + if (channelId.Contains(MESSAGE_THREAD_ID_PREFIX) + && getResult.Result.Custom.TryGetValue("parentChannelId", out var parentChannelId) + && getResult.Result.Custom.TryGetValue("parentMessageTimetoken", out var parentMessageTimeToken)) + { + result.Result = new ThreadChannel(this, channelId, parentChannelId.ToString(), parentMessageTimeToken.ToString(), getResult.Result); + } + else + { + result.Result = new Channel(this, channelId, getResult.Result); + } + return result; } + /// + /// Gets the list of channels with the provided parameters. + /// + /// Filter criteria for channels. + /// Sort criteria for channels. + /// The maximum number of channels to get. + /// Pagination object for retrieving specific page results. + /// A wrapper containing the list of channels and pagination information. public async Task GetChannels(string filter = "", string sort = "", int limit = 0, - Page page = null) + PNPageObject page = null) { - var buffer = new StringBuilder(8192); - page ??= new Page(); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_chat_get_channels(chatPointer, filter, sort, limit, - page.Next, - page.Previous, buffer))); - var jsonChannelsWrapper = buffer.ToString(); - var internalChannelsWrapper = - JsonConvert.DeserializeObject(jsonChannelsWrapper); - return new ChannelsResponseWrapper(this, internalChannelsWrapper); + var operation = PubnubInstance.GetAllChannelMetadata().IncludeCustom(true).IncludeCount(true).IncludeStatus(true).IncludeType(true); + if (!string.IsNullOrEmpty(filter)) + { + operation = operation.Filter(filter); + } + if (!string.IsNullOrEmpty(sort)) + { + operation = operation.Sort(new List(){sort}); + } + if (limit > 0) + { + operation = operation.Limit(limit); + } + if (page != null) + { + operation = operation.Page(page); + } + var response = await operation.ExecuteAsync().ConfigureAwait(false); + + if (response.Status.Error) + { + Logger.Error($"Error when trying to GetChannels(): {response.Status.ErrorData.Information}"); + return default; + } + var wrapper = new ChannelsResponseWrapper() + { + Channels = new List(), + Total = response.Result.TotalCount, + Page = response.Result.Page + }; + foreach (var resultMetadata in response.Result.Channels) + { + var channel = new Channel(this, resultMetadata.Channel, resultMetadata); + wrapper.Channels.Add(channel); + } + return wrapper; } /// @@ -969,35 +520,22 @@ public async Task GetChannels(string filter = "", strin /// /// The channel ID. /// The updated data for the channel. - /// Throws an exception if the channel with the provided ID does not exist or any connection problem persists. + /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// var chat = // ... - /// chat.UpdateChannel("channel_id", new ChatChannelData { + /// var result = await chat.UpdateChannel("channel_id", new ChatChannelData { /// ChannelName = "new_name" /// // ... /// }); /// /// /// - public async Task UpdateChannel(string channelId, ChatChannelData updatedData) + public async Task UpdateChannel(string channelId, ChatChannelData updatedData) { - var newPointer = await Task.Run(() => pn_chat_update_channel_dirty(chatPointer, channelId, - updatedData.ChannelName, - updatedData.ChannelDescription, - updatedData.ChannelCustomDataJson, - updatedData.ChannelUpdated, - updatedData.ChannelStatus, - updatedData.ChannelType)); - CUtilities.CheckCFunctionResult(newPointer); - if (channelWrappers.TryGetValue(channelId, out var existingChannelWrapper)) - { - existingChannelWrapper.UpdatePointer(newPointer); - } - else - { - channelWrappers.Add(channelId, new Channel(this, channelId, newPointer)); - } + var result = new ChatOperationResult("Chat.UpdateChannel()", this); + result.RegisterOperation(await Channel.UpdateChannelData(this, channelId, updatedData).ConfigureAwait(false)); + return result; } /// @@ -1007,72 +545,120 @@ public async Task UpdateChannel(string channelId, ChatChannelData updatedData) /// /// /// The channel ID. - /// Throws an exception if the channel with the provided ID does not exist or any connection problem persists. + /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// var chat = // ... - /// chat.DeleteChannel("channel_id"); + /// var result = await chat.DeleteChannel("channel_id"); /// /// - public async Task DeleteChannel(string channelId) + public async Task DeleteChannel(string channelId) { - if (channelWrappers.ContainsKey(channelId)) - { - channelWrappers.Remove(channelId); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_chat_delete_channel(chatPointer, channelId))); - } + var result = new ChatOperationResult("Chat.DeleteChannel()", this); + result.RegisterOperation(await PubnubInstance.RemoveChannelMetadata().Channel(channelId).ExecuteAsync().ConfigureAwait(false)); + return result; } #endregion #region Users - public async Task GetCurrentUserMentions(string startTimeToken, string endTimeToken, - int count) + internal async void StoreActivityTimeStamp() { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(await Task.Run(() => - pn_chat_get_current_user_mentions(chatPointer, startTimeToken, endTimeToken, count, buffer))); - var internalWrapperJson = buffer.ToString(); - var emptyResponse = new UserMentionsWrapper(this, new InternalUserMentionsWrapper()); - if (!CUtilities.IsValidJson(internalWrapperJson)) + var currentUserId = PubnubInstance.GetCurrentUserId(); + while (storeActivity) { - return emptyResponse; - } - - var internalWrapper = JsonConvert.DeserializeObject(internalWrapperJson); - if (internalWrapper == null) - { - return emptyResponse; + var getResult = await User.GetUserData(this, currentUserId).ConfigureAwait(false); + var data = (ChatUserData)getResult.Result; + if (getResult.Status.Error) + { + Logger.Error($"Error when trying to store user activity timestamp: {getResult.Status.ErrorData}"); + await Task.Delay(Config.StoreUserActivityInterval).ConfigureAwait(false); + continue; + } + data.CustomData ??= new Dictionary(); + data.CustomData["lastActiveTimestamp"] = ChatUtils.TimeTokenNow(); + var setData = await User.UpdateUserData(this, currentUserId, data).ConfigureAwait(false); + if (setData.Status.Error) + { + Logger.Error($"Error when trying to store user activity timestamp: {setData.Status.ErrorData}"); + } + await Task.Delay(Config.StoreUserActivityInterval).ConfigureAwait(false); } - - return new UserMentionsWrapper(this, internalWrapper); } - + /// - /// Tries to retrieve the current User object for this chat. + /// Gets the current user's mentions within a specified time range. /// - /// The retrieved current User object. - /// True if chat has a current user, false otherwise. - /// - public bool TryGetCurrentUser(out User user) + /// The start time token for the search range. + /// The end time token for the search range. + /// The maximum number of mentions to retrieve. + /// A ChatOperationResult containing the user mentions wrapper with mention data. + public async Task> GetCurrentUserMentions(string startTimeToken, string endTimeToken, + int count) { - var userPointer = pn_chat_current_user(chatPointer); - CUtilities.CheckCFunctionResult(userPointer); - return TryGetUser(userPointer, out user); + var result = new ChatOperationResult("Chat.GetCurrentUserMentions()", this); + var id = PubnubInstance.GetCurrentUserId(); + var getEventHistory = await GetEventsHistory(id, startTimeToken, endTimeToken, count).ConfigureAwait(false); + if (result.RegisterOperation(getEventHistory)) + { + return result; + } + var wrapper = new UserMentionsWrapper() + { + IsMore = getEventHistory.Result.IsMore, + Mentions = new List() + }; + var mentionEvents = getEventHistory.Result.Events.Where(x => x.Type == PubnubChatEventType.Mention); + foreach (var mentionEvent in mentionEvents) + { + var payloadDict = + PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(mentionEvent.Payload); + if (!payloadDict.TryGetValue("text", out var mentionText) + || !payloadDict.TryGetValue("messageTimetoken", out var messageTimeToken) + || !payloadDict.TryGetValue("channel", out var mentionChannel)) + { + continue; + } + var getMessage = await GetMessage(mentionChannel.ToString(), messageTimeToken.ToString()).ConfigureAwait(false); + if (getMessage.Error) + { + Logger.Warn($"Could not find message with ID/Timetoken from mention event. Event payload: {mentionEvent.Payload}"); + continue; + } + + var mention = new UserMentionData() + { + ChannelId = mentionChannel.ToString(), + Event = mentionEvent, + Message = getMessage.Result, + UserId = mentionEvent.UserId + }; + if (payloadDict.TryGetValue("parentChannel", out var parentChannelId)) + { + mention.ParentChannelId = parentChannelId.ToString(); + } + wrapper.Mentions.Add(mention); + } + result.Result = wrapper; + return result; } /// /// Asynchronously tries to retrieve the current User object for this chat. /// - /// User object if there is a current user, null otherwise. - public async Task GetCurrentUserAsync() + /// A ChatOperationResult containing the current User object if there is one, null otherwise. + public async Task> GetCurrentUser() { - return await Task.Run(() => + var result = new ChatOperationResult("Chat.GetCurrentUser()", this); + var userId = PubnubInstance.GetCurrentUserId(); + var getUser = await GetUser(userId).ConfigureAwait(false); + if (result.RegisterOperation(getUser)) { - var result = TryGetCurrentUser(out var currentUser); - return result ? currentUser : null; - }); + return result; + } + result.Result = getUser.Result; + return result; } /// @@ -1086,36 +672,101 @@ public bool TryGetCurrentUser(out User user) /// The ban user flag. /// The mute user flag. /// The reason for the restrictions. - /// Throws an exception if the user with the provided ID does not exist or any connection problem persists. + /// A ChatOperationResult indicating the success or failure of the operation. /// /// - /// var chat = // ... - /// chat.SetRestrictions("user_id", "channel_id", true, true, "Spamming"); + /// var result = await chat.SetRestriction("user_id", "channel_id", true, true, "Spamming"); /// /// - public async Task SetRestriction(string userId, string channelId, bool banUser, bool muteUser, string reason) + public async Task SetRestriction(string userId, string channelId, bool banUser, bool muteUser, string reason) { - CUtilities.CheckCFunctionResult( - await Task.Run( - () => pn_chat_set_restrictions(chatPointer, userId, channelId, banUser, muteUser, reason))); + var result = new ChatOperationResult("Chat.SetRestriction()", this); + var restrictionsChannelId = $"{INTERNAL_MODERATION_PREFIX}_{channelId}"; + var getResult = await Channel.GetChannelData(this, restrictionsChannelId).ConfigureAwait(false); + if (result.RegisterOperation(getResult)) + { + if (result.RegisterOperation(await Channel.UpdateChannelData(this, restrictionsChannelId, + new ChatChannelData()).ConfigureAwait(false))) + { + return result; + } + } + var moderationEventsChannelId = INTERNAL_MODERATION_PREFIX + userId; + //Lift restrictions + if (!banUser && !muteUser) + { + if (result.RegisterOperation(await PubnubInstance.RemoveChannelMembers().Channel(restrictionsChannelId) + .Uuids(new List() { userId }).ExecuteAsync().ConfigureAwait(false))) + { + return result; + } + result.RegisterOperation(await EmitEvent(PubnubChatEventType.Moderation, moderationEventsChannelId, + $"{{\"channelId\": \"{channelId}\", \"restriction\": \"lifted\", \"reason\": \"{reason}\"}}").ConfigureAwait(false)); + return result; + } + //Ban or mute + if (result.RegisterOperation(await PubnubInstance.SetChannelMembers().Channel(restrictionsChannelId).Uuids(new List() + { + new PNChannelMember() + { + Uuid = userId, + Custom = new Dictionary() + { + { "ban", banUser }, + { "mute", muteUser }, + { "reason", reason } + } + } + }).Include(new PNChannelMemberField[] + { + PNChannelMemberField.UUID, + PNChannelMemberField.CUSTOM + }).ExecuteAsync().ConfigureAwait(false))) + { + return result; + } + result.RegisterOperation(await EmitEvent(PubnubChatEventType.Moderation, moderationEventsChannelId, + $"{{\"channelId\": \"{channelId}\", \"restriction\": \"{(banUser ? "banned" : "muted")}\", \"reason\": \"{reason}\"}}").ConfigureAwait(false)); + return result; } - public async Task SetRestriction(string userId, string channelId, Restriction restriction) + /// + /// Sets the restrictions for the user with the provided user ID. + /// + /// Sets the restrictions for the user with the provided user ID in the provided channel. + /// + /// + /// The user ID. + /// The channel ID. + /// The Restriction object to be applied. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var result = await chat.SetRestriction("user_id", "channel_id", new Restriction(){Ban = true, Mute = true, Reason = "Spamming"}); + /// + /// + public async Task SetRestriction(string userId, string channelId, Restriction restriction) { - await SetRestriction(userId, channelId, restriction.Ban, restriction.Mute, restriction.Reason); + return await SetRestriction(userId, channelId, restriction.Ban, restriction.Mute, restriction.Reason).ConfigureAwait(false); } - public void AddListenerToUsersUpdate(List userIds, Action listener) + /// + /// Adds a listener for user update events on multiple users. + /// + /// List of user IDs to listen to. + /// The listener callback to invoke on user updates. + public async void AddListenerToUsersUpdate(List userIds, Action listener) { foreach (var userId in userIds) { - if (TryGetUser(userId, out var user)) + var getUser = await GetUser(userId).ConfigureAwait(false); + if (!getUser.Error) { - user.OnUserUpdated += listener; + getUser.Result.OnUserUpdated += listener; } } } - + /// /// Creates a new user with the provided user ID. /// @@ -1123,21 +774,21 @@ public void AddListenerToUsersUpdate(List userIds, Action listener /// /// /// The user ID. - /// The created user. + /// A ChatOperationResult containing the created User object. /// /// The data for user is empty. /// - /// Throws an exception if any connection problem persists. /// /// /// var chat = // ... - /// var user = chat.CreateUser("user_id"); + /// var result = await chat.CreateUser("user_id"); + /// var user = result.Result; /// /// /// - public async Task CreateUser(string userId) + public async Task> CreateUser(string userId) { - return await CreateUser(userId, new ChatUserData()); + return await CreateUser(userId, new ChatUserData()).ConfigureAwait(false); } /// @@ -1148,35 +799,33 @@ public async Task CreateUser(string userId) /// /// The user ID. /// The additional data for the user. - /// The created user. - /// Throws an exception if any connection problem persists. + /// A ChatOperationResult containing the created User object. /// /// /// var chat = // ... - /// var user = chat.CreateUser("user_id"); + /// var result = await chat.CreateUser("user_id"); + /// var user = result.Result; /// /// /// - public async Task CreateUser(string userId, ChatUserData additionalData) + public async Task> CreateUser(string userId, ChatUserData additionalData) { - if (userWrappers.TryGetValue(userId, out var existingUser)) - { - Debug.WriteLine("Trying to create a user with ID that already exists! Returning existing one."); - return existingUser; - } - - var userPointer = await Task.Run(() => pn_chat_create_user_dirty(chatPointer, userId, - additionalData.Username, - additionalData.ExternalId, - additionalData.ProfileUrl, - additionalData.Email, - additionalData.CustomDataJson, - additionalData.Status, - additionalData.Status)); - CUtilities.CheckCFunctionResult(userPointer); - var user = new User(this, userId, userPointer); - userWrappers.Add(userId, user); - return user; + var result = new ChatOperationResult("Chat.CreateUser()", this); + var existingUser = await GetUser(userId).ConfigureAwait(false); + if (!result.RegisterOperation(existingUser)) + { + result.Result = existingUser.Result; + return result; + } + + var update = await User.UpdateUserData(this, userId, additionalData).ConfigureAwait(false); + if (result.RegisterOperation(update)) + { + return result; + } + var user = new User(this, userId, additionalData); + result.Result = user; + return result; } /// @@ -1187,28 +836,33 @@ public async Task CreateUser(string userId, ChatUserData additionalData) /// /// The user ID. /// The channel ID. - /// True if the user is present, false otherwise. - /// Throws an exception if any connection problem persists. + /// A ChatOperationResult containing true if the user is present, false otherwise. /// /// /// var chat = // ... - /// if (chat.IsPresent("user_id", "channel_id")) { + /// var result = await chat.IsPresent("user_id", "channel_id"); + /// if (result.Result) { /// // User is present /// } /// /// /// /// - public async Task IsPresent(string userId, string channelId) + public async Task> IsPresent(string userId, string channelId) { - if (TryGetChannel(channelId, out var channel)) + var result = new ChatOperationResult("Chat.IsPresent()", this); + var getChannel = await GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) { - return await channel.IsUserPresent(userId); + return result; } - else + var isPresent = await getChannel.Result.IsUserPresent(userId).ConfigureAwait(false); + if (result.RegisterOperation(isPresent)) { - return false; + return result; } + result.Result = isPresent.Result; + return result; } /// @@ -1218,29 +872,33 @@ public async Task IsPresent(string userId, string channelId) /// /// /// The channel ID. - /// The list of the users present in the channel. - /// Throws an exception if any connection problem persists. + /// A ChatOperationResult containing the list of user IDs present in the channel. /// /// /// var chat = // ... - /// var users = chat.WhoIsPresent("channel_id"); - /// foreach (var user in users) { + /// var result = await chat.WhoIsPresent("channel_id"); + /// foreach (var userId in result.Result) { /// // User is present on the channel /// } /// /// /// /// - public async Task> WhoIsPresent(string channelId) + public async Task>> WhoIsPresent(string channelId) { - if (TryGetChannel(channelId, out var channel)) + var result = new ChatOperationResult>("Chat.WhoIsPresent()", this) { Result = new List() }; + var getChannel = await GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) { - return await channel.WhoIsPresent(); + return result; } - else + var whoIs = await getChannel.Result.WhoIsPresent().ConfigureAwait(false); + if (result.RegisterOperation(whoIs)) { - return new List(); + return result; } + result.Result = whoIs.Result; + return result; } /// @@ -1250,123 +908,118 @@ public async Task> WhoIsPresent(string channelId) /// /// /// The user ID. - /// The list of the channels where the user is present. - /// Throws an exception if any connection problem persists. + /// A ChatOperationResult containing the list of channel IDs where the user is present. /// /// /// var chat = // ... - /// var channels = chat.WherePresent("user_id"); - /// foreach (var channel in channels) { - /// // Channel where User is IsPresent + /// var result = await chat.WherePresent("user_id"); + /// foreach (var channelId in result.Result) { + /// // Channel where User is present /// }; /// /// /// /// - public async Task> WherePresent(string userId) + public async Task>> WherePresent(string userId) { - if (TryGetUser(userId, out var user)) + var result = new ChatOperationResult>("Chat.WherePresent()", this) { Result = new List() }; + var getUser = await GetUser(userId).ConfigureAwait(false); + if (result.RegisterOperation(getUser)) { - return await user.WherePresent(); + return result; } - else + var wherePresent = await getUser.Result.WherePresent().ConfigureAwait(false); + if (result.RegisterOperation(wherePresent)) { - return new List(); + return result; } - } - - /// - /// Gets the user with the provided user ID. - /// - /// Tries to get the user with the provided user ID. - /// - /// - /// The user ID. - /// The out user. - /// True if the user was found, false otherwise. - /// Throws an exception if any connection problem persists. - /// - /// - /// var chat = // ... - /// if (chat.TryGetUser("user_id", out var user)) { - /// // User found - /// } - /// - /// - /// - /// - public bool TryGetUser(string userId, out User user) - { - var userPointer = pn_chat_get_user(chatPointer, userId); - return TryGetUser(userId, userPointer, out user); + result.Result = wherePresent.Result; + return result; } /// /// Asynchronously gets the user with the provided user ID. /// /// ID of the User to get. - /// User object if one with given ID is found, null otherwise. - public async Task GetUserAsync(string userId) + /// A ChatOperationResult containing the User object if one with given ID is found, null otherwise. + public async Task> GetUser(string userId) { - return await Task.Run(() => + var result = new ChatOperationResult("Chat.GetUser()", this); + var getData = await User.GetUserData(this, userId).ConfigureAwait(false); + if (result.RegisterOperation(getData)) { - var result = TryGetUser(userId, out var user); - return result ? user : null; - }); - } - - internal bool TryGetUser(IntPtr userPointer, out User user) - { - var id = User.GetUserIdFromPtr(userPointer); - return TryGetUser(id, userPointer, out user); - } - - internal bool TryGetUser(string userId, IntPtr userPointer, out User user) - { - return TryGetWrapper(userWrappers, userId, userPointer, () => new User(this, userId, userPointer), - out user); + return result; + } + var user = new User(this, userId, getData.Result); + result.Result = user; + return result; } - + /// /// Gets the list of users with the provided parameters. /// /// Gets all the users that matches the provided parameters. /// /// - /// The include parameter. - /// The amount of userts to get. - /// The start time token of the users. - /// The end time token of the users. + /// Filter criteria for users. + /// Sort criteria for users. + /// The maximum number of users to get. + /// Pagination object for retrieving specific page results. /// The list of the users that matches the provided parameters. - /// Throws an exception if any connection problem persists. /// /// /// var chat = // ... - /// var users = chat.GetUsers( - /// "admin", - /// 10, - /// "16686902600029072" - /// "16686902600028961", + /// var users = await chat.GetUsers( + /// filter: "status == 'admin'", + /// limit: 10 /// ); - /// foreach (var user in users) { + /// foreach (var user in users.Users) { /// // User found /// }; /// /// /// - public async Task GetUsers(string filter = "", string sort = "", int limit = 0, - Page page = null) + public async Task> GetUsers(string filter = "", string sort = "", int limit = 0, + PNPageObject page = null) { - var buffer = new StringBuilder(8192); - page ??= new Page(); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_chat_get_users(chatPointer, filter, sort, limit, - page.Next, - page.Previous, buffer))); - var internalWrapperJson = buffer.ToString(); - var internalWrapper = JsonConvert.DeserializeObject(internalWrapperJson); - return new UsersResponseWrapper(this, internalWrapper); + var result = new ChatOperationResult("Chat.GetUsers()", this); + var operation = PubnubInstance.GetAllUuidMetadata().IncludeCustom(true).IncludeStatus(true).IncludeType(true); + if (!string.IsNullOrEmpty(filter)) + { + operation = operation.Filter(filter); + } + if (!string.IsNullOrEmpty(sort)) + { + operation = operation.Sort(new List(){sort}); + } + if (limit > 0) + { + operation = operation.Limit(limit); + } + if (page != null) + { + operation = operation.Page(page); + } + var getUuidMetadata = await operation.ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getUuidMetadata)) + { + return result; + } + var response = new UsersResponseWrapper() + { + Users = new List(), + Total = getUuidMetadata.Result.TotalCount, + Page = result.Result.Page + }; + foreach (var resultMetadata in getUuidMetadata.Result.Uuids) + { + var user = new User(this, resultMetadata.Uuid, resultMetadata); + response.Users.Add(user); + } + result.Result = response; + return result; } - + /// /// Updates the user with the provided user ID. /// @@ -1375,37 +1028,20 @@ public async Task GetUsers(string filter = "", string sort /// /// The user ID. /// The updated data for the user. - /// Throws an exception if the user with the provided ID does not exist or any connection problem persists. + /// A ChatOperationResult with information on the Update's success. /// /// /// var chat = // ... - /// chat.UpdateUser("user_id", new ChatUserData { + /// var result = await chat.UpdateUser("user_id", new ChatUserData { /// Username = "new_name" /// // ... /// }); /// /// /// - public async Task UpdateUser(string userId, ChatUserData updatedData) + public async Task UpdateUser(string userId, ChatUserData updatedData) { - var newPointer = await Task.Run(() => pn_chat_update_user_dirty(chatPointer, userId, - updatedData.Username, - updatedData.ExternalId, - updatedData.ProfileUrl, - updatedData.Email, - updatedData.CustomDataJson, - updatedData.Status, - updatedData.Type)); - CUtilities.CheckCFunctionResult(newPointer); - if (userWrappers.TryGetValue(userId, out var existingUserWrapper)) - { - existingUserWrapper.UpdatePointer(newPointer); - } - //TODO: could and should this ever actually happen? - else - { - userWrappers.Add(userId, new User(this, userId, newPointer)); - } + return (await User.UpdateUserData(this, userId, updatedData).ConfigureAwait(false)).ToChatOperationResult("Chat.UpdateUser()", this); } /// @@ -1415,20 +1051,18 @@ public async Task UpdateUser(string userId, ChatUserData updatedData) /// /// /// The user ID. - /// Throws an exception if the user with the provided ID does not exist or any connection problem persists. + /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// var chat = // ... - /// chat.DeleteUser("user_id"); + /// var result = await chat.DeleteUser("user_id"); /// /// - public async Task DeleteUser(string userId) + public async Task DeleteUser(string userId) { - if (userWrappers.ContainsKey(userId)) - { - userWrappers.Remove(userId); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_chat_delete_user(chatPointer, userId))); - } + var result = new ChatOperationResult("Chat.DeleteUser()", this); + result.RegisterOperation(await PubnubInstance.RemoveUuidMetadata().Uuid(userId).ExecuteAsync().ConfigureAwait(false)); + return result; } #endregion @@ -1439,161 +1073,184 @@ public async Task DeleteUser(string userId) /// Gets the memberships of the user with the provided user ID. /// /// Gets all the memberships of the user with the provided user ID. - /// The memberships are limited by the provided limit and the time tokens. + /// The memberships can be filtered, sorted, and paginated. /// /// /// The user ID. - /// The maximum amount of the memberships. - /// The start time token of the memberships. - /// The end time token of the memberships. - /// The list of the memberships of the user. - /// Throws an exception if the user with the provided ID does not exist or any connection problem persists. + /// Filter criteria for memberships. + /// Sort criteria for memberships. + /// The maximum number of memberships to retrieve. + /// Pagination object for retrieving specific page results. + /// A ChatOperationResult containing the list of the memberships of the user. /// /// /// var chat = // ... - /// var memberships = chat.GetUserMemberships( + /// var result = await chat.GetUserMemberships( /// "user_id", - /// 10, - /// "16686902600029072", - /// "16686902600028961" + /// limit: 10 /// ); - /// foreach (var membership in memberships) { + /// foreach (var membership in result.Result.Memberships) { /// // Membership found /// }; /// /// /// - public async Task GetUserMemberships(string userId, string filter = "", + public async Task> GetUserMemberships(string userId, string filter = "", string sort = "", - int limit = 0, Page page = null) + int limit = 0, PNPageObject page = null) { - if (!TryGetUser(userId, out var user)) + var result = new ChatOperationResult("Chat.GetUserMemberships()", this); + var operation = PubnubInstance.GetMemberships().Include( + new[] + { + PNMembershipField.CHANNEL_CUSTOM, + PNMembershipField.CHANNEL_TYPE, + PNMembershipField.CHANNEL_STATUS, + PNMembershipField.CUSTOM, + PNMembershipField.TYPE, + PNMembershipField.STATUS, + }).Uuid(userId); + if (!string.IsNullOrEmpty(filter)) { - return new MembersResponseWrapper(); + operation = operation.Filter(filter); + } + if (!string.IsNullOrEmpty(sort)) + { + operation = operation.Sort(new List() { sort }); + } + if (limit > 0) + { + operation = operation.Limit(limit); + } + if (page != null) + { + operation.Page(page); + } + var getMemberships = await operation.ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getMemberships)) + { + return result; } - var buffer = new StringBuilder(8192); - page ??= new Page(); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_user_get_memberships(user.Pointer, filter, sort, - limit, page.Next, - page.Previous, buffer))); - var internalWrapperJson = buffer.ToString(); - var internalWrapper = JsonConvert.DeserializeObject(internalWrapperJson); - return new MembersResponseWrapper(this, internalWrapper); - } - - public void AddListenerToMembershipsUpdate(List membershipIds, Action listener) - { - foreach (var membershipId in membershipIds) + var memberships = new List(); + foreach (var membershipResult in getMemberships.Result.Memberships) { - if (membershipWrappers.TryGetValue(membershipId, out var membership)) + memberships.Add(new Membership(this, userId, membershipResult.ChannelMetadata.Channel, new ChatMembershipData() { - membership.OnMembershipUpdated += listener; - } + CustomData = membershipResult.Custom, + Status = membershipResult.Status, + Type = membershipResult.Type + })); } + result.Result = new MembersResponseWrapper() + { + Memberships = memberships, + Page = getMemberships.Result.Page, + Total = getMemberships.Result.TotalCount + }; + return result; } /// /// Gets the memberships of the channel with the provided channel ID. /// /// Gets all the memberships of the channel with the provided channel ID. - /// The memberships are limited by the provided limit and the time tokens. + /// The memberships can be filtered, sorted, and paginated. /// /// /// The channel ID. - /// The maximum amount of the memberships. - /// The start time token of the memberships. - /// The end time token of the memberships. - /// The list of the memberships of the channel. - /// Throws an exception if the channel with the provided ID does not exist or any connection problem persists. + /// Filter criteria for memberships. + /// Sort criteria for memberships. + /// The maximum number of memberships to retrieve. + /// Pagination object for retrieving specific page results. + /// A ChatOperationResult containing the list of the memberships of the channel. /// /// /// var chat = // ... - /// var memberships = chat.GetChannelMemberships( - /// "user_id", - /// 10, - /// "16686902600029072", - /// "16686902600028961" + /// var result = await chat.GetChannelMemberships( + /// "channel_id", + /// limit: 10 /// ); - /// foreach (var membership in memberships) { + /// foreach (var membership in result.Result.Memberships) { /// // Membership found /// }; /// /// /// - public async Task GetChannelMemberships(string channelId, string filter = "", + public async Task> GetChannelMemberships(string channelId, string filter = "", string sort = "", - int limit = 0, Page page = null) + int limit = 0, PNPageObject page = null) { - if (!TryGetChannel(channelId, out var channel)) + var result = new ChatOperationResult("Chat.GetChannelMemberships()", this); + var operation = PubnubInstance.GetChannelMembers().Include( + new[] + { + PNChannelMemberField.UUID_CUSTOM, + PNChannelMemberField.UUID_TYPE, + PNChannelMemberField.UUID_STATUS, + PNChannelMemberField.CUSTOM, + PNChannelMemberField.TYPE, + PNChannelMemberField.STATUS, + }).Channel(channelId); + if (!string.IsNullOrEmpty(filter)) { - return new MembersResponseWrapper(); + operation = operation.Filter(filter); + } + if (!string.IsNullOrEmpty(sort)) + { + operation = operation.Sort(new List() { sort }); + } + if (limit > 0) + { + operation = operation.Limit(limit); + } + if (page != null) + { + operation = operation.Page(page); } - page ??= new Page(); - var buffer = new StringBuilder(8192); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_channel_get_members(channel.Pointer, filter, sort, - limit, page.Next, - page.Previous, buffer))); - var internalWrapperJson = buffer.ToString(); - var internalWrapper = JsonConvert.DeserializeObject(internalWrapperJson); - return new MembersResponseWrapper(this, internalWrapper); - } - - private bool TryGetMembership(IntPtr membershipPointer, out Membership membership) - { - var membershipId = Membership.GetMembershipIdFromPtr(membershipPointer); - return TryGetMembership(membershipId, membershipPointer, out membership); - } + var getResult = await operation.ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getResult)) + { + return result; + } - internal bool TryGetMembership(string membershipId, IntPtr membershipPointer, out Membership membership) - { - return TryGetWrapper(membershipWrappers, membershipId, membershipPointer, - () => new Membership(this, membershipPointer, membershipId), out membership); + var memberships = new List(); + foreach (var channelMemberResult in getResult.Result.ChannelMembers) + { + memberships.Add(new Membership(this, channelMemberResult.UuidMetadata.Uuid, channelId, new ChatMembershipData() + { + CustomData = channelMemberResult.Custom, + Status = channelMemberResult.Status, + Type = channelMemberResult.Type + })); + } + result.Result = new MembersResponseWrapper() + { + Memberships = memberships, + Page = getResult.Result.Page, + Total = getResult.Result.TotalCount + }; + return result; } #endregion #region Messages - - public async Task GetMessageReportsHistory(string channelId, string startTimeToken, - string endTimeToken, int count) - { - return await GetEventsHistory($"PUBNUB_INTERNAL_MODERATION_{channelId}", startTimeToken, endTimeToken, - count); - } - + /// - /// Gets the Message object for the given timetoken. - /// - /// Gets the Message object from the channel for the given timetoken. - /// The timetoken is used to identify the message. - /// + /// Gets the message reports history for a specific channel. /// - /// The channel ID. - /// The timetoken of the message. - /// The out parameter that contains the Message object. - /// true if the message is found; otherwise, false. - /// - /// - /// var chat = // ... - /// if (chat.TryGetMessage("channel_id", "timetoken", out var message)) { - /// // Message found - /// }; - /// - /// - /// - /// - public bool TryGetMessage(string channelId, string messageTimeToken, out Message message) + /// The ID of the channel to get reports for. + /// The start time token for the history range. + /// The end time token for the history range. + /// The maximum number of reports to retrieve. + /// A ChatOperationResult containing the events history wrapper with report events. + public async Task> GetMessageReportsHistory(string channelId, string startTimeToken, + string endTimeToken, int count) { - if (!TryGetChannel(channelId, out var channel)) - { - message = null; - return false; - } - - var messagePointer = pn_channel_get_message(channel.Pointer, messageTimeToken); - return TryGetMessage(messageTimeToken, messagePointer, out message); + return await GetEventsHistory($"PUBNUB_INTERNAL_MODERATION_{channelId}", startTimeToken, endTimeToken, + count).ConfigureAwait(false); } /// @@ -1601,204 +1258,289 @@ public bool TryGetMessage(string channelId, string messageTimeToken, out Message /// /// ID of the channel on which the message was sent. /// TimeToken of the searched-for message. - /// Message object if one was found, null otherwise. - public async Task GetMessageAsync(string channelId, string messageTimeToken) + /// A ChatOperationResult containing the Message object if one was found, null otherwise. + public async Task> GetMessage(string channelId, string messageTimeToken) { - return await Task.Run(() => + var result = new ChatOperationResult("Chat.GetMessage()", this); + var startTimeToken = (long.Parse(messageTimeToken) + 1).ToString(); + var getHistory = await GetChannelMessageHistory(channelId, startTimeToken, messageTimeToken, 1).ConfigureAwait(false); + if (result.RegisterOperation(getHistory)) + { + return result; + } + if (!getHistory.Result.Any()) { - var result = TryGetMessage(channelId, messageTimeToken, out var message); - return result ? message : null; - }); + result.Error = true; + result.Exception = new PNException($"Didn't find any message with timetoken {messageTimeToken} on channel {channelId}"); + return result; + } + result.Result = getHistory.Result[0]; + return result; } - public async Task MarkAllMessagesAsRead(string filter = "", string sort = "", + /// + /// Marks all messages as read for the current user across all their channels. + /// + /// Optional filter to apply when getting user memberships. + /// Optional sort criteria for memberships. + /// Maximum number of memberships to process (0-100). + /// Optional pagination object. + /// A ChatOperationResult containing the wrapper with updated memberships and status information. + public async Task> MarkAllMessagesAsRead(string filter = "", string sort = "", int limit = 0, - Page page = null) - { - page ??= new Page(); - var buffer = new StringBuilder(2048); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_chat_mark_all_messages_as_read(chatPointer, filter, - sort, limit, - page.Next, page.Previous, buffer))); - var internalWrapperJson = buffer.ToString(); - var internalWrapper = JsonConvert.DeserializeObject(internalWrapperJson); - return new MarkMessagesAsReadWrapper(this, internalWrapper); - } - - private bool TryGetMessage(string timeToken, IntPtr messagePointer, out Message message) - { - return TryGetWrapper(messageWrappers, timeToken, messagePointer, - () => new Message(this, messagePointer, timeToken), out message); - } - - internal bool TryGetAnyMessage(string timeToken, out Message message) - { - return messageWrappers.TryGetValue(timeToken, out message); - } - - internal bool TryGetThreadMessage(string timeToken, IntPtr threadMessagePointer, - out ThreadMessage threadMessage) + PNPageObject page = null) { - var found = TryGetWrapper(messageWrappers, timeToken, threadMessagePointer, - () => new ThreadMessage(this, threadMessagePointer, timeToken), out var foundMessage); - if (!found || foundMessage is not ThreadMessage foundThreadMessage) + var result = new ChatOperationResult("Chat.MarkAllMessagesAsRead()", this); + if (limit < 0 || limit > 100) { - threadMessage = null; - return false; + result.Error = true; + result.Exception = new PNException("For marking messages as read limit has to be between 0 and 100"); + return result; } - else + var currentUserId = PubnubInstance.GetCurrentUserId(); + var getCurrentUser = await GetCurrentUser().ConfigureAwait(false); + if (result.RegisterOperation(getCurrentUser)) { - threadMessage = foundThreadMessage; - return true; + return result; } + var getCurrentMemberships = await getCurrentUser.Result.GetMemberships(filter, sort, limit, page).ConfigureAwait(false); + if (result.RegisterOperation(getCurrentMemberships)) + { + return result; + } + if (getCurrentMemberships.Result.Memberships == null || !getCurrentMemberships.Result.Memberships.Any()) + { + result.Result = new MarkMessagesAsReadWrapper() + { + Memberships = new List() + }; + return result; + } + var timeToken = ChatUtils.TimeTokenNow(); + var memberships = getCurrentMemberships.Result.Memberships; + foreach (var membership in memberships) + { + membership.MembershipData.CustomData ??= new(); + membership.MembershipData.CustomData["lastReadMessageTimetoken"] = timeToken; + } + if (result.RegisterOperation(await Membership.UpdateMembershipsData(this, currentUserId, memberships).ConfigureAwait(false))) + { + return result; + } + foreach (var membership in memberships) + { + await EmitEvent(PubnubChatEventType.Receipt, membership.ChannelId, + $"{{\"messageTimetoken\": \"{timeToken}\"}}").ConfigureAwait(false); + } + result.Result = new MarkMessagesAsReadWrapper() + { + Memberships = memberships, + Page = getCurrentMemberships.Result.Page, + Status = getCurrentMemberships.Result.Status, + Total = getCurrentMemberships.Result.Total + }; + return result; } - - internal bool TryGetMessage(IntPtr messagePointer, out Message message) - { - var messageId = Message.GetMessageIdFromPtr(messagePointer); - return TryGetMessage(messageId, messagePointer, out message); - } - - public async Task> GetUnreadMessagesCounts(string filter = "", string sort = "", + + /// + /// Gets unread message counts for the current user's channels. + /// + /// Optional filter to apply when getting user memberships. + /// Optional sort criteria for memberships. + /// Maximum number of memberships to process (0-100). + /// Optional pagination object. + /// A ChatOperationResult containing a list of unread message wrappers with count information per channel. + public async Task>> GetUnreadMessagesCounts(string filter = "", string sort = "", int limit = 0, - Page page = null) + PNPageObject page = null) { - var buffer = new StringBuilder(4096); - page ??= new Page(); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_chat_get_unread_messages_counts(chatPointer, filter, - sort, limit, - page.Next, page.Previous, buffer))); - var wrappersListJson = buffer.ToString(); - var internalWrappersList = - JsonConvert.DeserializeObject>(wrappersListJson); - var returnWrappers = new List(); - if (internalWrappersList == null) + var result = new ChatOperationResult>("Chat.GetUnreadMessagesCounts()", this); + if (limit < 0 || limit > 100) { - return returnWrappers; + result.Error = true; + result.Exception = new PNException("For getting message counts limit has to be between 0 and 100"); + return result; } - - foreach (var internalWrapper in internalWrappersList) + var getCurrentUser = await GetCurrentUser().ConfigureAwait(false); + if (result.RegisterOperation(getCurrentUser)) { - returnWrappers.Add(new UnreadMessageWrapper(this, internalWrapper)); + return result; } - - return returnWrappers; - } - - public async Task CreateThreadChannel(Message message) - { - if (TryGetThreadChannel(message, out var existingThread)) + var getCurrentMemberships = await getCurrentUser.Result.GetMemberships(filter, sort, limit, page).ConfigureAwait(false); + if (result.RegisterOperation(getCurrentMemberships)) { - return existingThread; + return result; } - - var threadChannelPointer = await Task.Run(() => pn_message_create_thread(message.Pointer)); - CUtilities.CheckCFunctionResult(threadChannelPointer); - var newThread = new ThreadChannel(this, message, threadChannelPointer); - channelWrappers.Add(newThread.Id, newThread); - return newThread; - } - - public async Task RemoveThreadChannel(Message message) - { - if (!TryGetThreadChannel(message, out var existingThread)) + if (getCurrentMemberships.Result.Memberships == null || !getCurrentMemberships.Result.Memberships.Any()) { - return; + result.Result = new List(); + return result; } - - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_message_remove_thread(message.Pointer))); - channelWrappers.Remove(existingThread.Id); + var memberships = getCurrentMemberships.Result.Memberships; + var channelIds = new List(); + var timeTokens = new List(); + foreach (var membership in memberships) + { + channelIds.Add(membership.ChannelId); + var lastRead = string.IsNullOrEmpty(membership.LastReadMessageTimeToken) ? Membership.EMPTY_TIMETOKEN : long.Parse(membership.LastReadMessageTimeToken); + timeTokens.Add(lastRead); + } + var getCounts = await PubnubInstance.MessageCounts().Channels(channelIds.ToArray()).ChannelsTimetoken(timeTokens.ToArray()) + .ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getCounts)) + { + return result; + } + var wrapperList = new List(); + foreach (var channelMessagesCount in getCounts.Result.Channels) + { + wrapperList.Add(new UnreadMessageWrapper() + { + ChannelId = channelMessagesCount.Key, + Count = Convert.ToInt32(channelMessagesCount.Value), + Membership = memberships.First(x => x.ChannelId == channelMessagesCount.Key) + }); + } + result.Result = wrapperList; + return result; } /// - /// Tries to retrieve a ThreadChannel object from a Message object if there is one. + /// Creates a thread channel for a specific message. /// - /// Message on which the ThreadChannel is supposed to be. - /// Retrieved ThreadChannel or null if it wasn't found/ - /// True if a ThreadChannel was found, false otherwise. - /// - public bool TryGetThreadChannel(Message message, out ThreadChannel threadChannel) - { - var threadId = ThreadChannel.MessageToThreadChannelId(message); - - //Getting most up-to-date pointer - var threadPointer = pn_message_get_thread(message.Pointer); - - if (threadPointer == IntPtr.Zero) + /// The time token of the message to create a thread for. + /// The ID of the channel where the message was sent. + /// A ChatOperationResult containing the created ThreadChannel. + public async Task> CreateThreadChannel(string messageTimeToken, string messageChannelId) + { + var result = new ChatOperationResult("Chat.CreateThreadChannel()", this); + var getMessage = await GetMessage(messageChannelId, messageTimeToken).ConfigureAwait(false); + if (result.RegisterOperation(getMessage)) { - channelWrappers.Remove(threadId); - threadChannel = null; - return false; + return result; } - - //Existing thread pointer but not cached as a wrapper - if (!channelWrappers.TryGetValue(threadId, out var existingChannel)) + var createThread = getMessage.Result.CreateThread(); + if (result.RegisterOperation(createThread)) { - CUtilities.CheckCFunctionResult(threadPointer); - threadChannel = new ThreadChannel(this, message, threadPointer); - channelWrappers.Add(threadChannel.Id, threadChannel); - return true; + return result; } + result.Result = createThread.Result; + return result; + } - //Existing wrapper - if (existingChannel is ThreadChannel existingThreadChannel) - { - threadChannel = existingThreadChannel; - threadChannel.UpdatePointer(threadPointer); - return true; - } - else + /// + /// Removes a thread channel associated with a specific message. + /// + /// The time token of the message whose thread to remove. + /// The ID of the channel where the message was sent. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task RemoveThreadChannel(string messageTimeToken, string messageChannelId) + { + var result = new ChatOperationResult("Chat.RemoveThreadChannel()", this); + var getMessage = await GetMessage(messageChannelId, messageTimeToken).ConfigureAwait(false); + if (result.RegisterOperation(getMessage)) { - throw new Exception("Chat wrapper error: cached ThreadChannel was of the wrong type!"); + return result; } + result.RegisterOperation(await getMessage.Result.RemoveThread().ConfigureAwait(false)); + return result; } /// /// Asynchronously tries to retrieve a ThreadChannel object from a Message object if there is one. /// /// Message on which the ThreadChannel is supposed to be. - /// The ThreadChannel object if one was found, null otherwise. - public async Task GetThreadChannelAsync(Message message) + /// A ChatOperationResult containing the ThreadChannel object if one was found, null otherwise. + public async Task> GetThreadChannel(Message message) { - return await Task.Run(() => + var result = new ChatOperationResult("Chat.GetThreadChannel()", this); + var getChannel = await GetChannel(message.GetThreadId()).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) { - var result = TryGetThreadChannel(message, out var threadChannel); - return result ? threadChannel : null; - }); + return result; + } + if (getChannel.Result is not ThreadChannel threadChannel) + { + result.Error = true; + result.Exception = new PNException("Retrieved channel wasn't a thread channel"); + return result; + } + result.Result = threadChannel; + return result; } - public async Task ForwardMessage(Message message, Channel channel) - { - CUtilities.CheckCFunctionResult(await Task.Run(() => - pn_chat_forward_message(chatPointer, message.Pointer, channel.Pointer))); + /// + /// Forwards a message to a different channel. + /// + /// The time token of the message to forward. + /// The ID of the channel to forward the message to. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task ForwardMessage(string messageTimeToken, string channelId) + { + var result = new ChatOperationResult("Chat.ForwardMessage()", this); + var getMessage = await GetMessage(channelId, messageTimeToken).ConfigureAwait(false); + if (result.RegisterOperation(getMessage)) + { + return result; + } + result.RegisterOperation(await getMessage.Result.Forward(channelId).ConfigureAwait(false)); + return result; } - public void AddListenerToMessagesUpdate(string channelId, List messageTimeTokens, + /// + /// Adds a listener for message update events on specific messages in a channel. + /// + /// The ID of the channel containing the messages. + /// List of message time tokens to listen to for updates. + /// The listener callback to invoke on message updates. + public async void AddListenerToMessagesUpdate(string channelId, List messageTimeTokens, Action listener) { foreach (var messageTimeToken in messageTimeTokens) { - if (TryGetMessage(channelId, messageTimeToken, out var message)) + var getMessage = await GetMessage(channelId, messageTimeToken).ConfigureAwait(false); + if (!getMessage.Error) { - message.OnMessageUpdated += listener; + getMessage.Result.OnMessageUpdated += listener; } } } - public async Task PinMessageToChannel(string channelId, Message message) - { - if (TryGetChannel(channelId, out var channel)) + /// + /// Pins a message to a channel. + /// + /// The ID of the channel to pin the message to. + /// The message to pin. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task PinMessageToChannel(string channelId, Message message) + { + var result = new ChatOperationResult("Chat.PinMessageToChannel()", this); + var getChannel = await GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) { - await channel.PinMessage(message); + return result; } + var pin = await getChannel.Result.PinMessage(message).ConfigureAwait(false); + result.RegisterOperation(pin); + return result; } - public async Task UnpinMessageFromChannel(string channelId) + /// + /// Unpins the currently pinned message from a channel. + /// + /// The ID of the channel to unpin the message from. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task UnpinMessageFromChannel(string channelId) { - if (TryGetChannel(channelId, out var channel)) + var result = new ChatOperationResult("Chat.UnPinMessageFromChannel()", this); + var getChannel = await GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) { - await channel.UnpinMessage(); + return result; } + var unpin = await getChannel.Result.UnpinMessage().ConfigureAwait(false); + result.RegisterOperation(unpin); + return result; } /// @@ -1812,146 +1554,133 @@ public async Task UnpinMessageFromChannel(string channelId) /// The start time token of the messages. /// The end time token of the messages. /// The maximum amount of the messages. - /// The list of the messages that were sent in the channel. - /// Throws an exception if the channel with the provided ID does not exist or any connection problem persists. + /// A ChatOperationResult containing the list of messages that were sent in the channel. /// /// /// var chat = // ... - /// var messages = chat.GetChannelMessageHistory("channel_id", "start_time_token", "end_time_token", 10); - /// foreach (var message in messages) { + /// var result = await chat.GetChannelMessageHistory("channel_id", "start_time_token", "end_time_token", 10); + /// foreach (var message in result.Result) { /// // Message found /// }; /// /// /// - public async Task> GetChannelMessageHistory(string channelId, string startTimeToken, + public async Task>> GetChannelMessageHistory(string channelId, string startTimeToken, string endTimeToken, int count) { - var messages = new List(); - if (!TryGetChannel(channelId, out var channel)) + var result = new ChatOperationResult>("Chat.GetChannelMessageHistory()", this) { - Debug.WriteLine("Didn't find the channel for history fetch!"); - return messages; - } - - var buffer = new StringBuilder(32768); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_channel_get_history(channel.Pointer, startTimeToken, - endTimeToken, count, - buffer))); - var jsonPointers = buffer.ToString(); - var messagePointers = JsonConvert.DeserializeObject(jsonPointers); - var returnMessages = new List(); - if (messagePointers == null) + Result = new List() + }; + var getHistory = await PubnubInstance.FetchHistory().Channels(new[] { channelId }) + .Start(long.Parse(startTimeToken)).End(long.Parse(endTimeToken)).MaximumPerChannel(count).IncludeMessageActions(true) + .IncludeMeta(true).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getHistory) || getHistory.Result.Messages == null || !getHistory.Result.Messages.ContainsKey(channelId)) { - return returnMessages; + return result; } - foreach (var messagePointer in messagePointers) + //TODO: should be in "MessageHistoryWrapper" object? + var isMore = getHistory.Result.More != null; + foreach (var historyItem in getHistory.Result.Messages[channelId]) { - var id = Message.GetMessageIdFromPtr(messagePointer); - if (TryGetMessage(id, messagePointer, out var message)) + if (ChatParsers.TryParseMessageFromHistory(this, channelId, historyItem, out var message)) { - returnMessages.Add(message); + result.Result.Add(message); } } - - return returnMessages; + return result; } #endregion #region Events - public async Task GetEventsHistory(string channelId, string startTimeToken, + internal void BroadcastAnyEvent(ChatEvent chatEvent) + { + OnAnyEvent?.Invoke(chatEvent); + } + + /// + /// Gets the events history for a specific channel within a time range. + /// + /// The ID of the channel to get events for. + /// The start time token for the history range. + /// The end time token for the history range. + /// The maximum number of events to retrieve. + /// A ChatOperationResult containing the events history wrapper with chat events. + public async Task> GetEventsHistory(string channelId, string startTimeToken, string endTimeToken, int count) { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_chat_get_events_history(chatPointer, channelId, - startTimeToken, - endTimeToken, count, buffer))); - var wrapperJson = buffer.ToString(); - if (!CUtilities.IsValidJson(wrapperJson)) + var result = new ChatOperationResult("Chat.GetEventsHistory()", this) { - return new EventsHistoryWrapper(); - ; - } - - return JsonConvert.DeserializeObject(wrapperJson); - } - - public async Task EmitEvent(PubnubChatEventType type, string channelId, string jsonPayload) - { - CUtilities.CheckCFunctionResult(await Task.Run(() => - pn_chat_emit_event(chatPointer, (byte)type, channelId, jsonPayload))); - } - - internal IntPtr ListenForEvents(string channelId, PubnubChatEventType type) - { - if (string.IsNullOrEmpty(channelId)) + Result = new EventsHistoryWrapper() + { + Events = new List() + } + }; + var getHistory = await PubnubInstance.FetchHistory().Channels(new[] { channelId }) + .Start(long.Parse(startTimeToken)).End(long.Parse(endTimeToken)).MaximumPerChannel(count) + .ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getHistory) || !getHistory.Result.Messages.ContainsKey(channelId)) { - throw new ArgumentException("Channel ID cannot be null or empty."); + return result; } - var callbackHandler = pn_chat_listen_for_events(chatPointer, channelId, (byte)type); - CUtilities.CheckCFunctionResult(callbackHandler); - return callbackHandler; - } - - #endregion - - private bool TryGetWrapper(Dictionary wrappers, string id, IntPtr pointer, Func createWrapper, - out T wrapper) where T : ChatEntity - { - if (wrappers.TryGetValue(id, out wrapper)) + var isMore = getHistory.Result.More != null; + var events = new List(); + foreach (var message in getHistory.Result.Messages[channelId]) { - //We had it before but it's no longer valid - if (pointer == IntPtr.Zero) - { - Debug.WriteLine(CUtilities.GetErrorMessage()); - wrappers.Remove(id); - return false; - } - - //Pointer is valid but something nulled the wrapper - if (wrapper == null) + if (ChatParsers.TryParseEventFromHistory(this, channelId, message, out var chatEvent)) { - wrappers[id] = createWrapper(); - wrapper = wrappers[id]; - return true; - } - //Updating existing wrapper with updated pointer - else - { - wrapper.UpdatePointer(pointer); - return true; + events.Add(chatEvent); } } - //Adding new user to wrappers cache - else if (pointer != IntPtr.Zero) + result.Result = new EventsHistoryWrapper() { - wrapper = createWrapper(); - wrappers.Add(id, wrapper); - return true; - } - else + Events = events, + IsMore = isMore + }; + return result; + } + + /// + /// Emits a chat event on the specified channel. + /// + /// The type of event to emit. + /// The channel ID where to emit the event. + /// The JSON payload of the event. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task EmitEvent(PubnubChatEventType type, string channelId, string jsonPayload) + { + var result = new ChatOperationResult("Chat.EmitEvent()", this); + jsonPayload = jsonPayload.Remove(0, 1); + jsonPayload = jsonPayload.Remove(jsonPayload.Length - 1); + var fullPayload = $"{{{jsonPayload}, \"type\": \"{ChatEnumConverters.ChatEventTypeToString(type)}\"}}"; + var emitOperation = PubnubInstance.Publish().Channel(channelId).Message(fullPayload); + if (type is PubnubChatEventType.Receipt or PubnubChatEventType.Typing) { - Debug.WriteLine(CUtilities.GetErrorMessage()); - return false; + emitOperation.ShouldStore(false); } + result.RegisterOperation(await emitOperation.ExecuteAsync().ConfigureAwait(false)); + return result; } + #endregion + + /// + /// Destroys the chat instance and cleans up resources. + /// + /// Stops user activity tracking, destroys the PubNub instance, and disposes the rate limiter. + /// + /// public void Destroy() { - //TODO: a temporary solution, maybe nulling the ptr later will be better - if (fetchUpdates == false) - { - return; - } - - fetchUpdates = false; - pn_chat_delete(chatPointer); + storeActivity = false; + PubnubInstance.Destroy(); + RateLimiter.Dispose(); } ~Chat() diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs index ef4137e..15211e3 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs @@ -1,70 +1,49 @@ using System; -using System.Runtime.InteropServices; -using System.Text; +using System.Collections.Generic; using System.Threading.Tasks; -using PubnubChatApi.Enums; -using PubnubChatApi.Utilities; +using PubnubApi; -namespace PubNubChatAPI.Entities +namespace PubnubChatApi { public class ChatAccessManager { - #region DLL Imports - [DllImport("pubnub-chat")] - private static extern int pn_pam_can_i(IntPtr chat, byte permission, byte resource_type, string resource_name); - - [DllImport("pubnub-chat")] - private static extern int pn_pam_parse_token(IntPtr chat, string token, StringBuilder result); + private Chat chat; - [DllImport("pubnub-chat")] - private static extern int pn_pam_set_auth_token(IntPtr chat, string token); - - [DllImport("pubnub-chat")] - private static extern int pn_pam_set_pubnub_origin(IntPtr chat, string origin); - - #endregion - - private IntPtr chatPointer; - - internal ChatAccessManager(IntPtr chatPointer) + internal ChatAccessManager(Chat chat) { - this.chatPointer = chatPointer; + this.chat = chat; } public async Task CanI(PubnubAccessPermission permission, PubnubAccessResourceType resourceType, string resourceName) { - var result = await Task.Run(() => pn_pam_can_i(chatPointer, (byte)permission, (byte)resourceType, resourceName)); - CUtilities.CheckCFunctionResult(result); - return result == 1; - } - - /// - /// Sets a new token for this Chat instance. - /// - public void SetAuthToken(string token) - { - CUtilities.CheckCFunctionResult(pn_pam_set_auth_token(chatPointer, token)); - } - - /// - /// Decodes an existing token. - /// - /// A JSON string object containing permissions embedded in that token. - public string ParseToken(string token) - { - var buffer = new StringBuilder(512); - CUtilities.CheckCFunctionResult(pn_pam_parse_token(chatPointer, token, buffer)); - return buffer.ToString(); - } - - /// - /// Sets a new custom origin value. - /// - /// - public void SetPubnubOrigin(string origin) - { - CUtilities.CheckCFunctionResult(pn_pam_set_pubnub_origin(chatPointer, origin)); + var parsed = chat.PubnubInstance.ParseToken(chat.PubnubInstance.PNConfig.AuthKey); + Dictionary mapping = resourceType switch + { + PubnubAccessResourceType.Uuids => parsed.Resources.Uuids, + PubnubAccessResourceType.Channels => parsed.Resources.Channels, + _ => throw new ArgumentOutOfRangeException(nameof(resourceType), resourceType, null) + }; + var authValues = mapping[resourceName]; + switch (permission) + { + case PubnubAccessPermission.Read: + return authValues.Read; + case PubnubAccessPermission.Write: + return authValues.Write; + case PubnubAccessPermission.Manage: + return authValues.Manage; + case PubnubAccessPermission.Delete: + return authValues.Delete; + case PubnubAccessPermission.Get: + return authValues.Get; + case PubnubAccessPermission.Join: + return authValues.Join; + case PubnubAccessPermission.Update: + return authValues.Update; + default: + throw new ArgumentOutOfRangeException(nameof(permission), permission, null); + } } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs index dada71b..e6c71ba 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs @@ -1,28 +1,12 @@ -using System; using System.Collections.Generic; -using PubNubChatAPI.Entities; -using PubnubChatApi.Utilities; +using PubnubApi; -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { public struct ChannelsResponseWrapper { public List Channels; - public Page Page; - public int Total; - - internal ChannelsResponseWrapper(Chat chat, InternalChannelsResponseWrapper internalWrapper) - { - Page = internalWrapper.Page; - Total = internalWrapper.Total; - Channels = PointerParsers.ParseJsonChannelPointers(chat, internalWrapper.Channels); - } - } - - internal struct InternalChannelsResponseWrapper - { - public IntPtr[] Channels; - public Page Page; + public PNPageObject Page; public int Total; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs index fd26833..b501a56 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; +using PubnubApi; -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { public class ChannelsRestrictionsWrapper { public List Restrictions = new (); - public Page Page = new (); + public PNPageObject Page = new (); public int Total; - public string Status = string.Empty; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs index 275ee2c..48babc9 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs @@ -1,4 +1,7 @@ -namespace PubnubChatApi.Entities.Data +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi { /// /// Data class for the chat channel. @@ -11,11 +14,24 @@ namespace PubnubChatApi.Entities.Data /// public class ChatChannelData { - public string ChannelName { get; set; } = string.Empty; - public string ChannelDescription { get; set; } = string.Empty; - public string ChannelCustomDataJson { get; set; } = string.Empty; - public string ChannelUpdated { get; set; } = string.Empty; - public string ChannelStatus { get; set; } = string.Empty; - public string ChannelType { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public Dictionary CustomData { get; set; } = new (); + public string Updated { get; internal set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + + public static implicit operator ChatChannelData(PNChannelMetadataResult metadataResult) + { + return new ChatChannelData() + { + Name = metadataResult.Name, + Description = metadataResult.Description, + CustomData = metadataResult.Custom, + Status = metadataResult.Status, + Updated = metadataResult.Updated, + Type = metadataResult.Type + }; + } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs index a0f47f4..4f2b629 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs @@ -1,4 +1,7 @@ -namespace PubnubChatApi.Entities.Data +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi { /// /// Data class for a chat membership. @@ -11,8 +14,18 @@ namespace PubnubChatApi.Entities.Data /// public class ChatMembershipData { - public string CustomDataJson { get; set; } = string.Empty; - public string Status { get; set; } = string.Empty; - public string Type { get; set; } = string.Empty; + public Dictionary CustomData { get; set; } = new(); + public string Status { get; set; } + public string Type { get; set; } + + public static implicit operator ChatMembershipData(PNChannelMembersItemResult membersItem) + { + return new ChatMembershipData() + { + CustomData = membersItem.Custom, + Status = membersItem.Status, + Type = membersItem.Type + }; + } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatOperationResult.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatOperationResult.cs new file mode 100644 index 0000000..cd4a001 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatOperationResult.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi +{ + public class ChatOperationResult + { + public bool Error { get; internal set; } + public List InternalStatuses { get; internal set; } = new(); + public Exception Exception { get; internal set; } + + internal string OperationName { get; } + protected Chat chat; + + internal ChatOperationResult(string operationName, Chat chat) + { + OperationName = operationName; + this.chat = chat; + } + + /// + /// Registers a single PNResult to this overall Chat Operation Result. + /// Returns pubnubResult.Status.Error + /// + internal bool RegisterOperation(PNResult pubnubResult) + { + InternalStatuses.Add(pubnubResult.Status); + chat.Logger.Debug($"Chat operation \"{OperationName}\" registered PN Status: {chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(pubnubResult.Status)}"); + Error = pubnubResult.Status.Error; + if (Error) + { + chat.Logger.Debug($"Chat operation \"{OperationName}\" registered PN Status with error: {pubnubResult.Status.ErrorData.Information}"); + Exception = pubnubResult.Status.ErrorData.Throwable; + } + return Error; + } + + /// + /// Registers another ChatOperationResult to this ChatOperationResult. + /// Returns otherChatResult.Error + /// + internal bool RegisterOperation(ChatOperationResult otherChatResult) + { + foreach (var status in otherChatResult.InternalStatuses) + { + chat.Logger.Debug($"Chat operation \"{OperationName}\" registered PN Status from operation \"{otherChatResult.OperationName}\": {chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(status)}"); + InternalStatuses.Add(status); + + } + if (otherChatResult.Error) + { + chat.Logger.Debug($"Chat operation \"{OperationName}\" registered PN Status from operation \"{otherChatResult.OperationName}\" with error: {otherChatResult.Exception.Message}"); + } + Exception = otherChatResult.Exception; + Error = otherChatResult.Error; + return Error; + } + } + + public class ChatOperationResult : ChatOperationResult + { + internal ChatOperationResult(string operationName, Chat chat) : base(operationName, chat) + { + } + + public T Result { get; internal set; } + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs index 6d88df1..774952e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs @@ -1,4 +1,7 @@ -namespace PubnubChatApi.Entities.Data +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi { /// /// Data class for the chat user. @@ -15,8 +18,23 @@ public class ChatUserData public string ExternalId { get; set; } = string.Empty; public string ProfileUrl { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; - public string CustomDataJson { get; set; } = string.Empty; + public Dictionary CustomData { get; set; } = new (); public string Status { get; set; } = string.Empty; public string Type { get; set; } = string.Empty; + + public static implicit operator ChatUserData(PNUuidMetadataResult metadataResult) + { + return new ChatUserData() + { + ExternalId = metadataResult.ExternalId, + Email = metadataResult.Email, + ProfileUrl = metadataResult.ProfileUrl, + Username = metadataResult.Name, + Status = metadataResult.Status, + Type = metadataResult.Type, + CustomData = metadataResult.Custom + }; + } + } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs index 10a65e0..8ca515b 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using PubNubChatAPI.Entities; -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { - public struct CreatedChannelWrapper + public class CreatedChannelWrapper { public Channel CreatedChannel; public Membership HostMembership; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/EventsHistoryWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/EventsHistoryWrapper.cs index 9bbfe85..3af98d2 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/EventsHistoryWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/EventsHistoryWrapper.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; -using PubnubChatApi.Entities.Events; -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { public struct EventsHistoryWrapper { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs index 24eee1c..2b3bcb7 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs @@ -1,31 +1,13 @@ -using System; using System.Collections.Generic; -using PubNubChatAPI.Entities; -using PubnubChatApi.Utilities; +using PubnubApi; -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { public struct MarkMessagesAsReadWrapper { - public Page Page; + public PNPageObject Page; public int Total; - public int Status; + public string Status; public List Memberships; - - internal MarkMessagesAsReadWrapper(Chat chat, InternalMarkMessagesAsReadWrapper internalWrapper) - { - Page = internalWrapper.Page; - Total = internalWrapper.Total; - Status = internalWrapper.Status; - Memberships = PointerParsers.ParseJsonMembershipPointers(chat, internalWrapper.Memberships); - } - } - - internal struct InternalMarkMessagesAsReadWrapper - { - public Page Page; - public int Total; - public int Status; - public IntPtr[] Memberships; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs index 63c185f..54357fb 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs @@ -1,30 +1,12 @@ -using System; using System.Collections.Generic; -using PubNubChatAPI.Entities; -using PubnubChatApi.Utilities; +using PubnubApi; -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { - public struct MembersResponseWrapper + public class MembersResponseWrapper { - public List Memberships; - public Page Page; - public int Total; - public string Status; - - internal MembersResponseWrapper(Chat chat, InternalMembersResponseWrapper internalWrapper) - { - Page = internalWrapper.Page; - Total = internalWrapper.Total; - Status = internalWrapper.Status; - Memberships = PointerParsers.ParseJsonMembershipPointers(chat, internalWrapper.Memberships); - } - } - - internal struct InternalMembersResponseWrapper - { - public IntPtr[] Memberships; - public Page Page; + public List Memberships = new (); + public PNPageObject Page = new (); public int Total; public string Status; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MentionedUser.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MentionedUser.cs new file mode 100644 index 0000000..e2446b0 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MentionedUser.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace PubnubChatApi +{ + public struct MentionedUser + { + [JsonProperty("id")] + public string Id; + [JsonProperty("name")] + public string Name; + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MessageAction.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MessageAction.cs index edb88c3..26e9357 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MessageAction.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MessageAction.cs @@ -1,6 +1,4 @@ -using PubnubChatApi.Enums; - -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { public struct MessageAction { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/Page.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/Page.cs deleted file mode 100644 index f19b330..0000000 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/Page.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace PubnubChatApi.Entities.Data -{ - public class Page - { - public string Next = string.Empty; - public string Previous = string.Empty; - } -} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs index 20a07d7..1bb27e1 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs @@ -1,4 +1,6 @@ -namespace PubnubChatApi.Entities.Data +using PubnubApi; + +namespace PubnubChatApi { public class PubnubChatConfig { @@ -11,30 +13,21 @@ public class RateLimitPerChannel public int UnknownConversation; } - public string PublishKey { get; } - public string SubscribeKey { get; } - public string UserId { get; } - public string AuthKey { get; } public int TypingTimeout { get; } public int TypingTimeoutDifference { get; } public int RateLimitFactor { get; } public RateLimitPerChannel RateLimitsPerChannel { get; } public bool StoreUserActivityTimestamp { get; } public int StoreUserActivityInterval { get; } - - public PubnubChatConfig(string publishKey, string subscribeKey, string userId, string authKey = "", - int typingTimeout = 5000, int typingTimeoutDifference = 1000, int rateLimitFactor = 2, + + public PubnubChatConfig(int typingTimeout = 5000, int typingTimeoutDifference = 1000, int rateLimitFactor = 2, RateLimitPerChannel rateLimitPerChannel = null, bool storeUserActivityTimestamp = false, int storeUserActivityInterval = 60000) { - RateLimitsPerChannel = rateLimitPerChannel; + RateLimitsPerChannel = rateLimitPerChannel ?? new RateLimitPerChannel(); RateLimitFactor = rateLimitFactor; StoreUserActivityTimestamp = storeUserActivityTimestamp; StoreUserActivityInterval = storeUserActivityInterval; - PublishKey = publishKey; - SubscribeKey = subscribeKey; - UserId = userId; - AuthKey = authKey; TypingTimeout = typingTimeout; TypingTimeoutDifference = typingTimeoutDifference; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ReferencedChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ReferencedChannel.cs new file mode 100644 index 0000000..0834889 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ReferencedChannel.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace PubnubChatApi +{ + public struct ReferencedChannel + { + [JsonProperty("id")] + public string Id; + [JsonProperty("name")] + public string Name; + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/Restriction.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/Restriction.cs index 3f78b9b..ebacc6b 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/Restriction.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/Restriction.cs @@ -1,4 +1,4 @@ -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { /// /// Data struct for restriction. diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs index 0a466bc..abf4951 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs @@ -1,14 +1,13 @@ using System.Collections.Generic; -using PubNubChatAPI.Entities; -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { public class SendTextParams { public bool StoreInHistory = true; public bool SendByPost = false; - public string Meta = string.Empty; - public Dictionary MentionedUsers = new(); + public Dictionary Meta = new(); + public Dictionary MentionedUsers = new(); public Message QuotedMessage = null; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/TextLink.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/TextLink.cs index 54cf3f7..8d819c1 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/TextLink.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/TextLink.cs @@ -1,9 +1,14 @@ -namespace PubnubChatApi.Entities.Data +using Newtonsoft.Json; + +namespace PubnubChatApi { public class TextLink { + [JsonProperty("start_index")] public int StartIndex; - public int EndIndex; + [JsonProperty("end_index")] + public int EndIndex; + [JsonProperty("link")] public string Link = string.Empty; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs index 861a65a..599d081 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs @@ -1,27 +1,9 @@ -using System; -using PubNubChatAPI.Entities; -using PubnubChatApi.Utilities; - -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { public struct UnreadMessageWrapper { - public Channel Channel; + public string ChannelId; public Membership Membership; public int Count; - - internal UnreadMessageWrapper(Chat chat, InternalUnreadMessageWrapper internalWrapper) - { - Count = internalWrapper.Count; - Channel = PointerParsers.ParseJsonChannelPointers(chat, new []{internalWrapper.Channel})[0]; - Membership = PointerParsers.ParseJsonMembershipPointers(chat, new []{internalWrapper.Membership})[0]; - } - } - - internal struct InternalUnreadMessageWrapper - { - public IntPtr Channel; - public IntPtr Membership; - public int Count; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionData.cs index 5c63d04..4df4b2f 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionData.cs @@ -1,38 +1,11 @@ -using System; -using PubNubChatAPI.Entities; -using PubnubChatApi.Entities.Events; - -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { public class UserMentionData { public string ChannelId; - public string UserId; - public ChatEvent Event; - public Message Message; - public string ParentChannelId; - public string ThreadChannelId; - - internal UserMentionData(Chat chat, InternalUserMentionData internalWrapper) - { - ChannelId = internalWrapper.ChannelId; - UserId = internalWrapper.UserId; - Event = internalWrapper.Event; - chat.TryGetMessage(internalWrapper.Message, out Message); - ParentChannelId = internalWrapper.ParentChannelId; - ThreadChannelId = internalWrapper.ThreadChannelId; - } - } - - internal class InternalUserMentionData - { - public string ChannelId; public string UserId; public ChatEvent Event; - public IntPtr Message; - - public string ParentChannelId; - public string ThreadChannelId; + public Message Message; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs index be2ebec..5568745 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs @@ -1,26 +1,10 @@ using System.Collections.Generic; -using PubNubChatAPI.Entities; -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { public class UserMentionsWrapper { public List Mentions = new(); public bool IsMore; - - internal UserMentionsWrapper(Chat chat, InternalUserMentionsWrapper internalWrapper) - { - IsMore = internalWrapper.IsMore; - foreach (var internalMention in internalWrapper.UserMentionData) - { - Mentions.Add(new UserMentionData(chat, internalMention)); - } - } - } - - internal class InternalUserMentionsWrapper - { - public List UserMentionData = new(); - public bool IsMore; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs index 5552320..88b1525 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs @@ -1,28 +1,12 @@ -using System; using System.Collections.Generic; -using PubNubChatAPI.Entities; -using PubnubChatApi.Utilities; +using PubnubApi; -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { public struct UsersResponseWrapper { public List Users; - public Page Page; - public int Total; - - internal UsersResponseWrapper(Chat chat, InternalUsersResponseWrapper internalWrapper) - { - Page = internalWrapper.Page; - Total = internalWrapper.Total; - Users = PointerParsers.ParseJsonUserPointers(chat, internalWrapper.Users); - } - } - - internal struct InternalUsersResponseWrapper - { - public IntPtr[] Users; - public Page Page; + public PNPageObject Page; public int Total; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UsersRestrictionsWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UsersRestrictionsWrapper.cs index 421946d..cc54980 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UsersRestrictionsWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UsersRestrictionsWrapper.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; +using PubnubApi; -namespace PubnubChatApi.Entities.Data +namespace PubnubChatApi { public class UsersRestrictionsWrapper { public List Restrictions = new (); - public Page Page = new (); + public PNPageObject Page = new (); public int Total; - public string Status = string.Empty; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Events/ChatEvent.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Events/ChatEvent.cs index e214a05..8d85bfa 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Events/ChatEvent.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Events/ChatEvent.cs @@ -1,6 +1,4 @@ -using PubnubChatApi.Enums; - -namespace PubnubChatApi.Entities.Events +namespace PubnubChatApi { public struct ChatEvent { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs index 48184cb..2f9e1d6 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs @@ -1,13 +1,10 @@ using System; -using System.Runtime.InteropServices; -using System.Text; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; -using Newtonsoft.Json; -using PubnubChatApi.Entities.Data; -using PubnubChatApi.Enums; -using PubnubChatApi.Utilities; +using PubnubApi; -namespace PubNubChatAPI.Entities +namespace PubnubChatApi { /// /// Represents a membership of a user in a channel. @@ -24,99 +21,25 @@ namespace PubNubChatAPI.Entities /// public class Membership : UniqueChatEntity { - #region DLL Imports - - [DllImport("pubnub-chat")] - private static extern void pn_membership_delete(IntPtr membership); - - [DllImport("pubnub-chat")] - private static extern void pn_membership_get_user_id( - IntPtr membership, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_membership_get_channel_id( - IntPtr membership, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_membership_update_dirty( - IntPtr membership, - string custom_data_json, - string type, - string status); - - [DllImport("pubnub-chat")] - private static extern int pn_membership_last_read_message_timetoken(IntPtr membership, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_membership_set_last_read_message_timetoken(IntPtr membership, string timetoken); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_membership_set_last_read_message(IntPtr membership, IntPtr message); - - [DllImport("pubnub-chat")] - private static extern int pn_membership_get_unread_messages_count(IntPtr membership); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_membership_update_with_base(IntPtr membership, - IntPtr base_membership); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_membership_stream_updates(IntPtr membership); - - [DllImport("pubnub-chat")] - private static extern void pn_membership_get_membership_data( - IntPtr membership, - StringBuilder result); - - #endregion - + //Message counts requires a valid timetoken, so this one will be like "0", from beginning of the channel + internal const long EMPTY_TIMETOKEN = 17000000000000000; + /// /// The user ID of the user that this membership belongs to. /// - public string UserId - { - get - { - var buffer = new StringBuilder(512); - pn_membership_get_user_id(pointer, buffer); - return buffer.ToString(); - } - } + public string UserId { get; } /// /// The channel ID of the channel that this membership belongs to. /// - public string ChannelId - { - get - { - var buffer = new StringBuilder(512); - pn_membership_get_channel_id(pointer, buffer); - return buffer.ToString(); - } - } - + public string ChannelId { get; } + /// - /// Returns a class with additional Membership data. + /// The string time token of last read message on the membership channel. /// - public ChatMembershipData MembershipData - { - get - { - var buffer = new StringBuilder(512); - pn_membership_get_membership_data(pointer, buffer); - var jsonString = buffer.ToString(); - var data = new ChatMembershipData(); - if (CUtilities.IsValidJson(jsonString)) - { - data = JsonConvert.DeserializeObject(jsonString); - } + public string LastReadMessageTimeToken => MembershipData.CustomData != null && MembershipData.CustomData.TryGetValue("lastReadMessageTimetoken", out var timeToken) ? timeToken.ToString() : ""; - return data; - } - } + public ChatMembershipData MembershipData { get; private set; } /// /// Event that is triggered when the membership is updated. @@ -136,40 +59,30 @@ public ChatMembershipData MembershipData /// public event Action OnMembershipUpdated; - private Chat chat; + protected override string UpdateChannelId => ChannelId; - internal Membership(Chat chat, IntPtr membershipPointer, string membershipId) : base(membershipPointer, - membershipId) + internal Membership(Chat chat, string userId, string channelId, ChatMembershipData membershipData) : base(chat, userId+channelId) { - this.chat = chat; + UserId = userId; + ChannelId = channelId; + UpdateLocalData(membershipData); } - protected override IntPtr StreamUpdates() + internal void UpdateLocalData(ChatMembershipData newData) { - return pn_membership_stream_updates(pointer); + MembershipData = newData; } - internal static string GetMembershipIdFromPtr(IntPtr membershipPointer) + protected override SubscribeCallback CreateUpdateListener() { - var userIdBuffer = new StringBuilder(512); - pn_membership_get_user_id(membershipPointer, userIdBuffer); - var userId = userIdBuffer.ToString(); - var channelIdBuffer = new StringBuilder(512); - pn_membership_get_channel_id(membershipPointer, channelIdBuffer); - var channelId = channelIdBuffer.ToString(); - return userId + channelId; - } - - internal void BroadcastMembershipUpdate() - { - OnMembershipUpdated?.Invoke(this); - } - - internal override void UpdateWithPartialPtr(IntPtr partialPointer) - { - var newFullPointer = pn_membership_update_with_base(partialPointer, pointer); - CUtilities.CheckCFunctionResult(newFullPointer); - UpdatePointer(newFullPointer); + return chat.ListenerFactory.ProduceListener(objectEventCallback: delegate(Pubnub pn, PNObjectEventResult e) + { + if (ChatParsers.TryParseMembershipUpdate(chat, this, e, out var updatedData)) + { + UpdateLocalData(updatedData); + OnMembershipUpdated?.Invoke(this); + } + }); } /// @@ -180,46 +93,144 @@ internal override void UpdateWithPartialPtr(IntPtr partialPointer) /// /// /// The ChatMembershipData object to update the membership with. + /// A ChatOperationResult indicating the success or failure of the operation. /// - public async Task Update(ChatMembershipData membershipData) + public async Task Update(ChatMembershipData membershipData) { - var newPointer = await Task.Run(() => pn_membership_update_dirty(pointer, membershipData.CustomDataJson, - membershipData.Type, membershipData.Status)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + var result = (await UpdateMembershipData(membershipData).ConfigureAwait(false)).ToChatOperationResult("Membership.Update()", chat); + if (!result.Error) + { + UpdateLocalData(membershipData); + } + return result; } - public string GetLastReadMessageTimeToken() + internal async Task> UpdateMembershipData(ChatMembershipData membershipData) { - var buffer = new StringBuilder(128); - CUtilities.CheckCFunctionResult(pn_membership_last_read_message_timetoken(pointer, buffer)); - return buffer.ToString(); + return await chat.PubnubInstance.ManageMemberships().Uuid(UserId).Set(new List() + { + new() + { + Channel = ChannelId, + Custom = membershipData.CustomData, + Status = membershipData.Status, + Type = membershipData.Type + } + }).Include(new[] + { + PNMembershipField.TYPE, + PNMembershipField.CUSTOM, + PNMembershipField.STATUS, + PNMembershipField.CHANNEL, + PNMembershipField.CHANNEL_CUSTOM, + PNMembershipField.CHANNEL_TYPE, + PNMembershipField.CHANNEL_STATUS, + }).ExecuteAsync().ConfigureAwait(false); } - - public async Task SetLastReadMessage(Message message) + + internal static async Task> UpdateMembershipsData(Chat chat, string userId, List memberships) { - var newPointer = await Task.Run(() => pn_membership_set_last_read_message(pointer, message.Pointer)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + var pnMemberships = memberships.Select(membership => new PNMembership() + { + Channel = membership.ChannelId, + Custom = membership.MembershipData.CustomData, + Status = membership.MembershipData.Status, + Type = membership.MembershipData.Type + }).ToList(); + return await chat.PubnubInstance.SetMemberships().Uuid(userId).Channels(pnMemberships).Include(new[] + { + PNMembershipField.TYPE, + PNMembershipField.CUSTOM, + PNMembershipField.STATUS, + PNMembershipField.CHANNEL, + PNMembershipField.CHANNEL_CUSTOM, + PNMembershipField.CHANNEL_TYPE, + PNMembershipField.CHANNEL_STATUS + }).ExecuteAsync().ConfigureAwait(false); } - public async Task SetLastReadMessageTimeToken(string timeToken) + /// + /// Sets the last read message for this membership. + /// + /// Updates the membership to mark the specified message as the last one read by the user. + /// This is used for tracking read receipts and unread message counts. + /// + /// + /// The message to mark as last read. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + public async Task SetLastReadMessage(Message message) { - var newPointer = await Task.Run(() => pn_membership_set_last_read_message_timetoken(pointer, timeToken)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + return await SetLastReadMessageTimeToken(message.TimeToken).ConfigureAwait(false); } - - public async Task GetUnreadMessagesCount() + + /// + /// Sets the last read message time token for this membership. + /// + /// Updates the membership to mark the message with the specified time token as the last one read by the user. + /// This is used for tracking read receipts and unread message counts. + /// + /// + /// The time token of the message to mark as last read. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + public async Task SetLastReadMessageTimeToken(string timeToken) + { + var result = new ChatOperationResult("Membership.SetLastReadMessageTimeToken()", chat); + MembershipData.CustomData ??= new Dictionary(); + MembershipData.CustomData["lastReadMessageTimetoken"] = timeToken; + var update = await UpdateMembershipData(MembershipData).ConfigureAwait(false); + if (result.RegisterOperation(update)) + { + return result; + } + result.RegisterOperation(await chat.EmitEvent(PubnubChatEventType.Receipt, ChannelId, + $"{{\"messageTimetoken\": \"{timeToken}\"}}").ConfigureAwait(false)); + return result; + } + + /// + /// Gets the count of unread messages for this membership. + /// + /// Calculates the number of messages that have been sent to the channel since the last read message time token. + /// + /// + /// A ChatOperationResult with the number of unread messages + /// + /// + public async Task> GetUnreadMessagesCount() { - var result = await Task.Run(() => pn_membership_get_unread_messages_count(pointer)); - CUtilities.CheckCFunctionResult(result); + var result = new ChatOperationResult("Membership.GetUnreadMessagesCount()", chat); + if (!long.TryParse(LastReadMessageTimeToken, out var lastRead)) + { + result.Error = true; + result.Exception = new PNException("LastReadMessageTimeToken is not a valid time token!"); + return result; + } + lastRead = lastRead == 0 ? EMPTY_TIMETOKEN : lastRead; + var countsResponse = await chat.PubnubInstance.MessageCounts().Channels(new[] { ChannelId }) + .ChannelsTimetoken(new[] { lastRead }).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(countsResponse)) + { + return result; + } + result.Result = countsResponse.Result.Channels[ChannelId]; return result; } - protected override void DisposePointer() + /// + /// Refreshes the membership data from the server. + /// + /// Fetches the latest membership information from the server and updates the local data. + /// This is useful when you want to ensure you have the most up-to-date membership information. + /// + /// + /// A ChatOperationResult indicating the success or failure of the refresh operation. + public override async Task Refresh() { - pn_membership_delete(pointer); + return await chat.GetChannelMemberships(ChannelId, filter:$"uuid.id == \"{UserId}\"").ConfigureAwait(false); } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs index 3fb6ce2..9a33367 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -1,15 +1,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Text; +using System.Linq; using System.Threading.Tasks; -using Newtonsoft.Json; -using PubnubChatApi.Entities.Data; -using PubnubChatApi.Enums; -using PubnubChatApi.Utilities; +using PubnubApi; -namespace PubNubChatAPI.Entities +namespace PubnubChatApi { /// /// Represents a message in a chat channel. @@ -22,113 +17,24 @@ namespace PubNubChatAPI.Entities /// public class Message : UniqueChatEntity { - #region DLL Imports - - [DllImport("pubnub-chat")] - private static extern void pn_message_delete(IntPtr message); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_message_edit_text(IntPtr message, string text); - - [DllImport("pubnub-chat")] - private static extern int pn_message_text(IntPtr message, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_message_delete_message(IntPtr message); - - [DllImport("pubnub-chat")] - private static extern int pn_message_delete_message_hard(IntPtr message); - - [DllImport("pubnub-chat")] - private static extern int pn_message_deleted(IntPtr message); - - [DllImport("pubnub-chat")] - private static extern int pn_message_get_timetoken(IntPtr message, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_message_get_data_type(IntPtr message); - - [DllImport("pubnub-chat")] - private static extern void pn_message_get_data_text(IntPtr message, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_message_get_data_channel_id(IntPtr message, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_message_get_data_user_id(IntPtr message, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_message_get_data_meta(IntPtr message, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_message_get_data_message_actions(IntPtr message, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_message_pin(IntPtr message); - - [DllImport("pubnub-chat")] - private static extern int pn_message_get_reactions(IntPtr message, StringBuilder reactions_json); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_message_toggle_reaction(IntPtr message, string reaction); - - [DllImport("pubnub-chat")] - private static extern int pn_message_has_user_reaction(IntPtr message, string reaction); - - [DllImport("pubnub-chat")] - private static extern int pn_message_report(IntPtr message, string reason); - - [DllImport("pubnub-chat")] - private static extern int pn_message_has_thread(IntPtr message); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_message_update_with_base_message(IntPtr message, IntPtr base_message); - - [DllImport("pubnub-chat")] - private static extern int pn_message_mentioned_users(IntPtr message, IntPtr chat, StringBuilder result); - [DllImport("pubnub-chat")] - private static extern int pn_message_referenced_channels(IntPtr message, IntPtr chat, StringBuilder result); - [DllImport("pubnub-chat")] - private static extern IntPtr pn_message_quoted_message(IntPtr message); - [DllImport("pubnub-chat")] - private static extern int pn_message_text_links(IntPtr message, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_message_restore(IntPtr message); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_message_stream_updates(IntPtr message); - - #endregion - /// /// The text content of the message. /// /// This is the main content of the message. It can be any text that the user wants to send. /// /// - public virtual string MessageText - { + public string MessageText { get { - var buffer = new StringBuilder(32768); - CUtilities.CheckCFunctionResult(pn_message_text(pointer, buffer)); - return buffer.ToString(); + var edits = MessageActions.Where(x => x.Type == PubnubMessageActionType.Edited).ToList(); + return edits.Any() ? edits[0].Value : OriginalMessageText; } } /// /// The original, un-edited text of the message. /// - public virtual string OriginalMessageText - { - get - { - var buffer = new StringBuilder(32768); - pn_message_get_data_text(pointer, buffer); - return buffer.ToString(); - } - } + public string OriginalMessageText { get; internal set; } /// /// The time token of the message. @@ -137,15 +43,7 @@ public virtual string OriginalMessageText /// It is used to identify the message in the chat. /// /// - public virtual string TimeToken - { - get - { - var buffer = new StringBuilder(512); - pn_message_get_timetoken(pointer, buffer); - return buffer.ToString(); - } - } + public string TimeToken { get; internal set; } /// /// The channel ID of the channel that the message belongs to. @@ -153,15 +51,7 @@ public virtual string TimeToken /// This is the ID of the channel that the message was sent to. /// /// - public virtual string ChannelId - { - get - { - var buffer = new StringBuilder(512); - pn_message_get_data_channel_id(pointer, buffer); - return buffer.ToString(); - } - } + public string ChannelId { get; internal set; } /// /// The user ID of the user that sent the message. @@ -170,15 +60,7 @@ public virtual string ChannelId /// Do not confuse this with the username of the user. /// /// - public virtual string UserId - { - get - { - var buffer = new StringBuilder(512); - pn_message_get_data_user_id(pointer, buffer); - return buffer.ToString(); - } - } + public string UserId { get; internal set; } /// /// The metadata of the message. @@ -187,15 +69,7 @@ public virtual string UserId /// It can be used to store additional information about the message. /// /// - public virtual string Meta - { - get - { - var buffer = new StringBuilder(4096); - pn_message_get_data_meta(pointer, buffer); - return buffer.ToString(); - } - } + public Dictionary Meta { get; internal set; } = new (); /// /// Whether the message has been deleted. @@ -205,106 +79,127 @@ public virtual string Meta /// It means that all the deletions are soft deletions. /// /// - public virtual bool IsDeleted - { - get - { - var result = pn_message_deleted(pointer); - CUtilities.CheckCFunctionResult(result); - return result == 1; - } - } - - public virtual List MentionedUsers - { + public bool IsDeleted => MessageActions.Any(x => x.Type == PubnubMessageActionType.Deleted); + + /// + /// Gets the list of users mentioned in this message. + /// + /// Extracts user mentions from the message metadata and returns them as a list of MentionedUser objects. + /// + /// + /// A list of users that were mentioned in this message. + public List MentionedUsers { get { - var buffer = new StringBuilder(1024); - CUtilities.CheckCFunctionResult(pn_message_mentioned_users(pointer, chat.Pointer, buffer)); - var usersJson = buffer.ToString(); - if (!CUtilities.IsValidJson(usersJson)) + var mentioned = new List(); + if (!Meta.TryGetValue("mentionedUsers", out var rawMentionedUsers)) { - return new List(); + return mentioned; } - var jsonDict = JsonConvert.DeserializeObject>(usersJson); - if (jsonDict == null || !jsonDict.TryGetValue("value", out var pointers) || pointers == null) + if (rawMentionedUsers is Dictionary mentionedDict) { - return new List(); + foreach (var kvp in mentionedDict) + { + if (kvp.Value is Dictionary mentionedUser) + { + mentioned.Add(new MentionedUser() + { + Id = (string)mentionedUser["id"], + Name = (string)mentionedUser["name"] + }); + } + } } - return PointerParsers.ParseJsonUserPointers(chat, pointers); + return mentioned; } } - public virtual List ReferencedChannels - { + /// + /// Gets the list of channels referenced in this message. + /// + /// Extracts channel references from the message metadata and returns them as a list of ReferencedChannel objects. + /// + /// + /// A list of channels that were referenced in this message. + public List ReferencedChannels { get { - var buffer = new StringBuilder(1024); - CUtilities.CheckCFunctionResult(pn_message_referenced_channels(pointer, chat.Pointer, buffer)); - var channelsJson = buffer.ToString(); - if (!CUtilities.IsValidJson(channelsJson)) + var referenced = new List(); + if (!Meta.TryGetValue("referencedChannels", out var rawReferenced)) { - return new List(); + return referenced; } - var jsonDict = JsonConvert.DeserializeObject>(channelsJson); - if (jsonDict == null || !jsonDict.TryGetValue("value", out var pointers) || pointers == null) + if (rawReferenced is Dictionary referencedDict) { - return new List(); + foreach (var kvp in referencedDict) + { + if (kvp.Value is Dictionary referencedChannel) + { + referenced.Add(new ReferencedChannel() + { + Id = (string)referencedChannel["id"], + Name = (string)referencedChannel["name"] + }); + } + } } - return PointerParsers.ParseJsonChannelPointers(chat, pointers); + return referenced; } } - public virtual List TextLinks - { + /// + /// Gets the list of text links found in this message. + /// + /// Extracts text links from the message metadata and returns them as a list of TextLink objects + /// containing start index, end index, and link URL information. + /// + /// + /// A list of text links found in this message. + public List TextLinks { get { - var buffer = new StringBuilder(2048); - CUtilities.CheckCFunctionResult(pn_message_text_links(pointer, buffer)); - var jsonString = buffer.ToString(); - if (!CUtilities.IsValidJson(jsonString)) + var links = new List(); + if (!Meta.TryGetValue("textLinks", out var rawLinks)) { - return new List(); + return links; } - var textLinks = JsonConvert.DeserializeObject>>(jsonString); - if (textLinks == null || !textLinks.TryGetValue("value", out var links) || links == null) + if (rawLinks is Dictionary linksDick) { - return new List(); + foreach (var kvp in linksDick) + { + if (kvp.Value is Dictionary link) + { + links.Add(new TextLink() + { + StartIndex = Convert.ToInt32(link["start_index"]), + EndIndex = Convert.ToInt32(link["end_index"]), + Link = (string)link["link"] + }); + } + } } return links; } } - protected List DeserializeMessageActions(string json) - { - var reactions = new List(); - if (CUtilities.IsValidJson(json)) - { - reactions = JsonConvert.DeserializeObject>(json); - reactions ??= new List(); - } - return reactions; - } - - public virtual List MessageActions - { - get - { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(pn_message_get_data_message_actions(pointer, buffer)); - return DeserializeMessageActions(buffer.ToString()); - } - } - - public virtual List Reactions - { - get - { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(pn_message_get_reactions(pointer, buffer)); - return DeserializeMessageActions(buffer.ToString()); - } - } + /// + /// Gets or sets the list of message actions applied to this message. + /// + /// Message actions include reactions, edits, deletions, and other modifications to the message. + /// + /// + /// A list of all message actions applied to this message. + public List MessageActions { get; internal set; } = new(); + + /// + /// Gets the list of reactions added to this message. + /// + /// Filters the message actions to return only reactions (like emojis or other reaction types). + /// + /// + /// A list of reaction message actions for this message. + public List Reactions => + MessageActions.Where(x => x.Type == PubnubMessageActionType.Reaction).ToList(); /// /// The data type of the message. @@ -314,9 +209,8 @@ public virtual List Reactions /// /// /// - public virtual PubnubChatMessageType Type => (PubnubChatMessageType)pn_message_get_data_type(pointer); + public PubnubChatMessageType Type { get; internal set; } - protected Chat chat; /// /// Event that is triggered when the message is updated. @@ -338,40 +232,28 @@ public virtual List Reactions /// public event Action OnMessageUpdated; - internal Message(Chat chat, IntPtr messagePointer, string timeToken) : base(messagePointer, timeToken) - { - this.chat = chat; - } - - protected override IntPtr StreamUpdates() - { - return pn_message_stream_updates(pointer); - } - - internal static string GetMessageIdFromPtr(IntPtr messagePointer) - { - var buffer = new StringBuilder(512); - pn_message_get_timetoken(messagePointer, buffer); - return buffer.ToString(); - } + protected override string UpdateChannelId => ChannelId; - internal static string GetChannelIdFromMessagePtr(IntPtr messagePointer) - { - var buffer = new StringBuilder(512); - pn_message_get_data_channel_id(messagePointer, buffer); - return buffer.ToString(); - } - - internal override void UpdateWithPartialPtr(IntPtr partialPointer) + internal Message(Chat chat, string timeToken,string originalMessageText, string channelId, string userId, PubnubChatMessageType type, Dictionary meta, List messageActions) : base(chat, timeToken) { - var newFullPointer = pn_message_update_with_base_message(partialPointer, pointer); - CUtilities.CheckCFunctionResult(newFullPointer); - UpdatePointer(newFullPointer); + TimeToken = timeToken; + OriginalMessageText = originalMessageText; + ChannelId = channelId; + UserId = userId; + Type = type; + Meta = meta; + MessageActions = messageActions; } - internal virtual void BroadcastMessageUpdate() + protected override SubscribeCallback CreateUpdateListener() { - OnMessageUpdated?.Invoke(this); + return chat.ListenerFactory.ProduceListener(messageActionCallback: delegate(Pubnub pn, PNMessageActionEventResult e) + { + if (ChatParsers.TryParseMessageUpdate(chat, this, e)) + { + OnMessageUpdated?.Invoke(this); + } + }); } /// @@ -382,145 +264,491 @@ internal virtual void BroadcastMessageUpdate() /// /// /// The new text of the message. + /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// var message = // ...; - /// message.EditMessageText("New text"); + /// var result = await message.EditMessageText("New text"); /// /// /// - public virtual async Task EditMessageText(string newText) + public async Task EditMessageText(string newText) { - var newPointer = await Task.Run(() => pn_message_edit_text(pointer, newText)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + var result = new ChatOperationResult("Message.EditMessageText()", chat); + if (string.IsNullOrEmpty(newText)) + { + result.Error = true; + result.Exception = new PNException("Failed to edit text, new text is empty or null"); + return result; + } + result.RegisterOperation(await chat.PubnubInstance.AddMessageAction() + .Action(new PNMessageAction() { Type = "edited", Value = newText }) + .Channel(ChannelId) + .MessageTimetoken(long.Parse(TimeToken)).Channel(ChannelId).ExecuteAsync().ConfigureAwait(false)); + return result; } - public virtual bool TryGetQuotedMessage(out Message quotedMessage) + /// + /// Gets the quoted message if this message quotes another message. + /// + /// A ChatOperationResult containing the quoted Message object if one exists, null otherwise. + public async Task> GetQuotedMessage() { - var quotedMessagePointer = pn_message_quoted_message(pointer); - if (quotedMessagePointer == IntPtr.Zero) + var result = new ChatOperationResult("Message.GetQuotedMessage()", chat); + if (!Meta.TryGetValue("quotedMessage", out var quotedMessage)) + { + result.Error = true; + result.Exception = new PNException("No quoted message was found."); + return result; + } + if (quotedMessage is not Dictionary quotedMessageDict || + !quotedMessageDict.TryGetValue("timetoken", out var timetoken) || + !quotedMessageDict.TryGetValue("channelId", out var channelId)) + { + result.Error = true; + result.Exception = new PNException("Quoted message data has incorrect format."); + return result; + } + var getMessage = await chat.GetMessage(channelId.ToString(), timetoken.ToString()).ConfigureAwait(false); + if (result.RegisterOperation(getMessage)) { - Debug.WriteLine(CUtilities.GetErrorMessage()); - quotedMessage = null; - return false; + return result; } - return chat.TryGetMessage(quotedMessagePointer, out quotedMessage); + result.Result = getMessage.Result; + return result; } + /// + /// Checks if this message has a thread associated with it. + /// + /// Determines whether a thread channel has been created for this message by checking for thread-related message actions. + /// + /// + /// True if this message has a thread, false otherwise. + /// + /// public bool HasThread() { - var result = pn_message_has_thread(pointer); - CUtilities.CheckCFunctionResult(result); - return result == 1; + return MessageActions.Any(x => x.Type == PubnubMessageActionType.ThreadRootId); } - public async Task CreateThread() + internal string GetThreadId() { - return await chat.CreateThreadChannel(this); + return $"{Chat.MESSAGE_THREAD_ID_PREFIX}_{ChannelId}_{TimeToken}"; } /// - /// Tries to get the ThreadChannel started on this Message. + /// Creates a thread channel for this message. + /// + /// Creates a new thread channel that is associated with this message. Users can send messages + /// to the thread to have conversations related to this specific message. + /// /// - /// The retrieved ThreadChannel object, null if one wasn't found. - /// True if a ThreadChannel object has been found, false otherwise. - /// - public bool TryGetThread(out ThreadChannel threadChannel) + /// A ChatOperationResult containing the created ThreadChannel object. + /// + /// + /// var message = // ...; + /// var result = message.CreateThread(); + /// if (!result.Error) { + /// var threadChannel = result.Result; + /// // Use the thread channel + /// } + /// + /// + /// + /// + /// + public ChatOperationResult CreateThread() { - return chat.TryGetThreadChannel(this, out threadChannel); + var result = new ChatOperationResult("Message.CreateThread()", chat); + if (ChannelId.Contains(Chat.MESSAGE_THREAD_ID_PREFIX)) + { + result.Error = true; + result.Exception = new PNException("Only one level of thread nesting is allowed."); + return result; + } + if (IsDeleted) + { + result.Error = true; + result.Exception = new PNException("You cannot create threads on deleted messages."); + return result; + } + if (HasThread()) + { + result.Error = true; + result.Exception = new PNException("Thread for this message already exist."); + return result; + } + var threadId = GetThreadId(); + var description = $"Thread on message with timetoken {TimeToken} on channel {ChannelId}"; + var data = new ChatChannelData() + { + Description = description + }; + result.Result = new ThreadChannel(chat, threadId, ChannelId, TimeToken, data); + return result; } /// /// Asynchronously tries to get the ThreadChannel started on this Message. /// - /// The retrieved ThreadChannel object, null if one wasn't found. - public async Task GetThreadAsync() + /// A ChatOperationResult containing the retrieved ThreadChannel object, null if one wasn't found. + public async Task> GetThread() { - return await chat.GetThreadChannelAsync(this); + return await chat.GetThreadChannel(this).ConfigureAwait(false); } - public async Task RemoveThread() + /// + /// Removes the thread channel associated with this message. + /// + /// Deletes the thread channel that was created for this message, including all messages in the thread. + /// This action cannot be undone. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// if (message.HasThread()) { + /// var result = await message.RemoveThread(); + /// if (!result.Error) { + /// // Thread has been removed + /// } + /// } + /// + /// + /// + /// + /// + public async Task RemoveThread() { - await chat.RemoveThreadChannel(this); + var result = new ChatOperationResult("Message.RemoveThread()", chat); + if (!HasThread()) + { + result.Error = true; + result.Exception = new PNException("There is no thread to be deleted"); + return result; + } + var threadMessageAction = MessageActions.First(x => x.Type == PubnubMessageActionType.ThreadRootId); + var getThread = await GetThread().ConfigureAwait(false); + if (result.RegisterOperation(getThread)) + { + return result; + } + if (result.RegisterOperation(await chat.PubnubInstance.RemoveMessageAction().Channel(ChannelId) + .MessageTimetoken(long.Parse(TimeToken)).ActionTimetoken(long.Parse(threadMessageAction.TimeToken)) + .ExecuteAsync().ConfigureAwait(false))) + { + return result; + } + MessageActions = MessageActions.Where(x => x.Type != PubnubMessageActionType.ThreadRootId).ToList(); + result.RegisterOperation(await getThread.Result.Delete().ConfigureAwait(false)); + return result; } - public async Task Pin() + /// + /// Pins this message to its channel. + /// + /// Marks this message as the pinned message for the channel it belongs to. + /// Only one message can be pinned per channel. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// var result = await message.Pin(); + /// if (!result.Error) { + /// // Message has been pinned to the channel + /// } + /// + /// + /// + /// + public async Task Pin() { - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_message_pin(pointer))); + var result = new ChatOperationResult("Message.Pin()", chat); + var getChannel = await chat.GetChannel(ChannelId).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) + { + return result; + } + result.RegisterOperation(await getChannel.Result.PinMessage(this).ConfigureAwait(false)); + return result; } - public virtual async Task Report(string reason) + /// + /// Reports this message for inappropriate content or behavior. + /// + /// Submits a report about this message to the moderation system with the specified reason. + /// This helps maintain community guidelines and content standards. + /// + /// + /// The reason for reporting this message. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// var result = await message.Report("Spam content"); + /// if (!result.Error) { + /// // Message has been reported + /// } + /// + /// + public async Task Report(string reason) { - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_message_report(pointer, reason))); + var jsonDict = new Dictionary() + { + {"text",MessageText}, + {"reason",reason}, + {"reportedMessageChannelId",ChannelId}, + {"reportedMessageTimetoken",TimeToken}, + {"reportedUserId",UserId} + }; + return await chat.EmitEvent(PubnubChatEventType.Report, $"{Chat.INTERNAL_MODERATION_PREFIX}_{ChannelId}", + chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)).ConfigureAwait(false); } - public virtual async Task Forward(string channelId) + /// + /// Forwards this message to another channel. + /// + /// Sends a copy of this message to the specified channel, preserving the original text and metadata. + /// + /// + /// The ID of the channel to forward the message to. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// var result = await message.Forward("target-channel-id"); + /// if (!result.Error) { + /// // Message has been forwarded + /// } + /// + /// + /// + public async Task Forward(string channelId) { - if (chat.TryGetChannel(channelId, out var channel)) + var result = new ChatOperationResult("Message.Forward()", chat); + var getChannel = await chat.GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) { - await chat.ForwardMessage(this, channel); + return result; } + result.RegisterOperation(await getChannel.Result.ForwardMessage(this).ConfigureAwait(false)); + return result; } - public virtual bool HasUserReaction(string reactionValue) + /// + /// Checks if this message has a specific reaction. + /// + /// Determines whether the specified reaction value (like an emoji) has been added to this message. + /// + /// + /// The reaction value to check for (e.g., "👍", "❤️"). + /// True if the message has the specified reaction, false otherwise. + /// + /// + /// var message = // ...; + /// if (message.HasUserReaction("👍")) { + /// // Message has a thumbs up reaction + /// } + /// + /// + /// + /// + public bool HasUserReaction(string reactionValue) { - var result = pn_message_has_user_reaction(pointer, reactionValue); - CUtilities.CheckCFunctionResult(result); - return result == 1; + return Reactions.Any(x => x.Value == reactionValue); } - public virtual async Task ToggleReaction(string reactionValue) + /// + /// Toggles a reaction on this message. + /// + /// Adds the specified reaction if it doesn't exist, or removes it if it already exists. + /// This allows users to react to messages with emojis or other reaction types. + /// + /// + /// The reaction value to toggle (e.g., "👍", "❤️"). + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// var result = await message.ToggleReaction("👍"); + /// if (!result.Error) { + /// // Reaction has been toggled + /// } + /// + /// + /// + /// + public async Task ToggleReaction(string reactionValue) { - var newPointer = await Task.Run(() => pn_message_toggle_reaction(pointer, reactionValue)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + var result = new ChatOperationResult("Message.ToggleReaction()", chat); + var currentUserId = chat.PubnubInstance.GetCurrentUserId(); + for (var i = 0; i < MessageActions.Count; i++) + { + var reaction = MessageActions[i]; + if (reaction.Type == PubnubMessageActionType.Reaction && reaction.UserId == currentUserId && reaction.Value == reactionValue) + { + //Removing old one + var remove = await chat.PubnubInstance.RemoveMessageAction().MessageTimetoken(long.Parse(TimeToken)) + .ActionTimetoken(long.Parse(reaction.TimeToken)).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(remove)) + { + return result; + } + MessageActions.RemoveAt(i); + break; + } + } + var add = await chat.PubnubInstance.AddMessageAction().Action(new PNMessageAction() + { + Type = "reaction", Value = reactionValue + }).MessageTimetoken(long.Parse(TimeToken)).Channel(ChannelId).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(add)) + { + return result; + } + MessageActions.Add(new MessageAction() + { + UserId = currentUserId, + TimeToken = add.Result.MessageTimetoken.ToString(), + Type = PubnubMessageActionType.Reaction, + Value = reactionValue + }); + return result; } - public virtual async Task Restore() + /// + /// Restores a previously deleted message. + /// + /// Undoes the soft deletion of this message, making it visible again to all users. + /// This only works for messages that were soft deleted. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// if (message.IsDeleted) { + /// var result = await message.Restore(); + /// if (!result.Error) { + /// // Message has been restored + /// } + /// } + /// + /// + /// + /// + public async Task Restore() { - var newPointer = await Task.Run(() => pn_message_restore(pointer)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + var result = new ChatOperationResult("Message.Restore()", chat); + if (!IsDeleted) + { + result.Error = true; + result.Exception = new PNException("Can't restore a message that wasn't deleted!"); + return result; + } + var deleteAction = MessageActions.First(x => x.Type == PubnubMessageActionType.Deleted); + var restore = await chat.PubnubInstance.RemoveMessageAction().MessageTimetoken(long.Parse(TimeToken)) + .ActionTimetoken(long.Parse(deleteAction.TimeToken)).Channel(ChannelId).ExecuteAsync().ConfigureAwait(false); + result.RegisterOperation(restore); + MessageActions.RemoveAt(MessageActions.IndexOf(deleteAction)); + return result; } /// /// Deletes the message. /// - /// This method deletes the message. - /// It marks the message as deleted. - /// It means that the message will not be visible to other users, but the - /// message is treated as soft deleted. + /// A soft-deleted message can be restored, a hard-deleted one is permanently gone. /// /// + /// Whether to perform a soft delete (true) or hard delete (false). + /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// var message = // ...; - /// message.DeleteMessage(); + /// var result = await message.Delete(soft: true); /// /// /// /// - public virtual async Task Delete(bool soft) + public async Task Delete(bool soft) { - await Task.Run(() => + var result = new ChatOperationResult("Message.Delete()", chat); + if (soft) { - if (soft) + var add = await chat.PubnubInstance.AddMessageAction() + .MessageTimetoken(long.Parse(TimeToken)).Action(new PNMessageAction() + { + Type = "deleted", + Value = "deleted" + }).Channel(ChannelId).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(add)) { - var newPointer = pn_message_delete_message(pointer); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + return result; } - else + MessageActions.Add(new MessageAction() + { + TimeToken = add.Result.ActionTimetoken.ToString(), + UserId = chat.PubnubInstance.GetCurrentUserId(), + Type = PubnubMessageActionType.Deleted, + Value = "deleted" + }); + } + else + { + if (HasThread()) { - CUtilities.CheckCFunctionResult(pn_message_delete_message_hard(pointer)); + var getThread = await GetThread().ConfigureAwait(false); + if (result.RegisterOperation(getThread)) + { + return result; + } + var deleteThread = await getThread.Result.Delete().ConfigureAwait(false); + if (result.RegisterOperation(deleteThread)) + { + return result; + } } - }); + var startTimeToken = long.Parse(TimeToken) + 1; + var deleteMessage = await chat.PubnubInstance.DeleteMessages().Start(startTimeToken) + .End(long.Parse(TimeToken)).ExecuteAsync().ConfigureAwait(false); + result.RegisterOperation(deleteMessage); + } + return result; } - protected override void DisposePointer() + /// + /// Refreshes the message data from the server. + /// + /// Fetches the latest message information from the server and updates the local data, + /// including message actions, metadata, and other properties that may have changed. + /// + /// + /// A ChatOperationResult indicating the success or failure of the refresh operation. + /// + /// + /// var message = // ...; + /// var result = await message.Refresh(); + /// if (!result.Error) { + /// // Message data has been refreshed + /// Console.WriteLine($"Updated text: {message.MessageText}"); + /// } + /// + /// + public override async Task Refresh() { - pn_message_delete(pointer); + var result = new ChatOperationResult("Message.Refresh()", chat); + var get = await chat.GetMessage(ChannelId, TimeToken).ConfigureAwait(false); + if (result.RegisterOperation(get)) + { + return result; + } + MessageActions = get.Result.MessageActions; + Meta = get.Result.Meta; + return result; } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs index 074bce1..7e33207 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs @@ -1,14 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; -using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Newtonsoft.Json; -using PubnubChatApi.Entities.Data; -using PubnubChatApi.Utilities; +using DiffMatchPatch; -namespace PubNubChatAPI.Entities +namespace PubnubChatApi { public enum MentionType { @@ -31,7 +29,26 @@ public class SuggestedMention public string ReplaceFrom { get; set; } public string ReplaceTo { get; set; } public MentionTarget Target { get; set; } - }; + } + + /// + /// Internal class to track mentions within the draft text + /// + internal class InternalMention + { + public int Start { get; set; } + public int Length { get; set; } + public MentionTarget Target { get; set; } + + public int EndExclusive => Start + Length; + + public InternalMention(int start, int length, MentionTarget target) + { + Start = start; + Length = length; + Target = target; + } + } public class MessageElement { @@ -63,98 +80,199 @@ private class DraftCallbackDataHelper public List SuggestedMentions; } - #region DLL Imports - - [DllImport("pubnub-chat")] - private static extern void pn_message_draft_delete(IntPtr message_draft); - - [DllImport("pubnub-chat")] - private static extern int pn_message_draft_insert_text(IntPtr message_draft, int position, - string text_to_insert); + // Static regex patterns for mention detection + private static readonly Regex UserMentionRegex = new Regex(@"((?=\s?)@[a-zA-Z0-9_]+)", RegexOptions.Compiled); + private static readonly Regex ChannelReferenceRegex = new Regex(@"((?=\s?)#[a-zA-Z0-9_]+)", RegexOptions.Compiled); + + // Schema prefixes for rendering mentions + private static readonly string SchemaUser = "pn-user://"; + private static readonly string SchemaChannel = "pn-channel://"; + + // Internal state + private string _value = string.Empty; + private List _mentions = new (); + private diff_match_patch _diffMatchPatch = new (); + + public event Action, List> OnDraftUpdated; + + /// + /// Gets the current text of the draft + /// + public string Text => _value; + + /// + /// Gets the current message elements + /// + public List MessageElements => GetMessageElements(); - [DllImport("pubnub-chat")] - private static extern int pn_message_draft_remove_text(IntPtr message_draft, int position, int length); + public bool ShouldSearchForSuggestions { get; set; } + + private Channel channel; + private Chat chat; + + private bool isTypingIndicatorTriggered; + private UserSuggestionSource userSuggestionSource; + private int userLimit; + private int channelLimit; - [DllImport("pubnub-chat")] - private static extern int pn_message_draft_insert_suggested_mention(IntPtr message_draft, int offset, - string replace_from, string replace_to, string target_json, string text); + internal MessageDraft(Chat chat, Channel channel, UserSuggestionSource userSuggestionSource, bool isTypingIndicatorTriggered, int userLimit, int channelLimit, bool shouldSearchForSuggestions) + { + this.chat = chat; + this.channel = channel; + this.isTypingIndicatorTriggered = isTypingIndicatorTriggered; + this.userSuggestionSource = userSuggestionSource; + this.userLimit = userLimit; + this.channelLimit = channelLimit; + ShouldSearchForSuggestions = shouldSearchForSuggestions; + } - [DllImport("pubnub-chat")] - private static extern int pn_message_draft_add_mention(IntPtr message_draft, int start, int length, - string target); + private async void BroadcastDraftUpdate() + { + try + { + var messageElements = GetMessageElements(); + var suggestedMentions = ShouldSearchForSuggestions ? await GenerateSuggestedMentions().ConfigureAwait(false) : new List(); + OnDraftUpdated?.Invoke(messageElements, suggestedMentions); + } + catch (Exception e) + { + chat.Logger.Error($"Error has occured when trying to broadcast MessageDraft update: {e.Message}"); + } + } - [DllImport("pubnub-chat")] - private static extern int pn_message_draft_remove_mention(IntPtr message_draft, int start); + /// + /// Generates suggested mentions based on current text patterns + /// + private async Task> GenerateSuggestedMentions() + { + var suggestions = new List(); + var rawMentions = SuggestRawMentions(); - [DllImport("pubnub-chat")] - private static extern int pn_message_draft_update(IntPtr message_draft, string text); + foreach (var rawMention in rawMentions) + { + var suggestion = new SuggestedMention + { + Offset = rawMention.Start, + ReplaceFrom = _value.Substring(rawMention.Start, rawMention.Length), + }; + switch (rawMention.Target.Type) + { + case MentionType.User: + var usersWrapper = + await chat.GetUsers(filter: $"name LIKE \"{rawMention.Target.Target}*\"", limit:userLimit).ConfigureAwait(false); + if (!usersWrapper.Error && usersWrapper.Result.Users.Any()) + { + var user = usersWrapper.Result.Users[0]; + suggestion.Target = new MentionTarget() { Target = user.Id, Type = rawMention.Target.Type }; + suggestion.ReplaceTo = user.UserName; + if (userSuggestionSource == UserSuggestionSource.CHANNEL && + !(await user.IsPresentOn(channel.Id).ConfigureAwait(false)).Result) + { + continue; + } + } + else + { + continue; + } + break; + case MentionType.Channel: + var channelsWrapper = await chat.GetChannels(filter: $"name LIKE \"{rawMention.Target.Target}*\"", + limit: channelLimit).ConfigureAwait(false); + if (channelsWrapper.Channels != null && channelsWrapper.Channels.Any()) + { + var mentionedChannel = channelsWrapper.Channels[0]; + suggestion.Target = new MentionTarget() { Target = channel.Id, Type = rawMention.Target.Type }; + suggestion.ReplaceTo = mentionedChannel.Name; + } + else + { + continue; + } + break; + case MentionType.Url: + break; + default: + throw new ArgumentOutOfRangeException(); + } + suggestions.Add(suggestion); + } - [DllImport("pubnub-chat")] - private static extern int pn_message_draft_send(IntPtr message_draft, - bool store_in_history, - bool send_by_post, - string meta, - int mentioned_users_length, - int[] mentioned_users_indexes, - IntPtr[] mentioned_users, - IntPtr quoted_message); + return suggestions; + } + + /// + /// Suggests raw mentions based on regex patterns in the text + /// + internal List SuggestRawMentions() + { + var allMentions = new List(); - [DllImport("pubnub-chat")] - private static extern int pn_message_draft_consume_callback_data(IntPtr message_draft, StringBuilder data); + // Find user mentions (@username) + var userMatches = UserMentionRegex.Matches(_value); + foreach (Match match in userMatches) + { + bool alreadyMentioned = _mentions.Any(mention => mention.Start == match.Index); + if (!alreadyMentioned) + { + var target = new MentionTarget { Type = MentionType.User, Target = match.Value[1..] }; + allMentions.Add(new InternalMention(match.Index, match.Length, target)); + } + } - [DllImport("pubnub-chat")] - private static extern void pn_message_draft_set_search_for_suggestions(IntPtr message_draft, - bool search_for_suggestions); + // Find channel mentions (#channel) + var channelMatches = ChannelReferenceRegex.Matches(_value); + foreach (Match match in channelMatches) + { + bool alreadyMentioned = _mentions.Any(mention => mention.Start == match.Index); + if (!alreadyMentioned) + { + var target = new MentionTarget { Type = MentionType.Channel, Target = match.Value[1..] }; + allMentions.Add(new InternalMention(match.Index, match.Length, target)); + } + } - #endregion + // Sort by start position + allMentions.Sort((a, b) => a.Start.CompareTo(b.Start)); - private IntPtr pointer; - - public event Action, List> OnDraftUpdated; + return allMentions; + } - //TODO: will see if these stay non-accessible - /* /// - /// The Channel where this MessageDraft will be published. - /// - public Channel Channel { get; } - /// - /// The scope for searching for suggested users - either [UserSuggestionSource.GLOBAL] or [UserSuggestionSource.CHANNEL]. - /// - private UserSuggestionSource UserSuggestionSourceSetting { get; } - /// - /// Whether modifying the message text triggers the typing indicator on [channel]. - /// - public bool IsTypingIndicatorTriggered { get; } - /// - /// The limit on the number of users returned when searching for users to mention. - /// - public int UserLimit { get; } - /// - /// The limit on the number of channels returned when searching for channels to reference. - /// - public int ChannelLimit { get; } - /// - /// Can be used to set a [Message] to quote when sending this [MessageDraft]. + /// Inserts a suggested mention into the draft at the appropriate position. + /// + /// Insert mention into the MessageDraft according to SuggestedMention.Offset, SuggestedMention.ReplaceFrom and + /// SuggestedMention.target. + /// /// - public Message QuotedMessage { get; }*/ - - internal MessageDraft(IntPtr pointer) - { - this.pointer = pointer; - } - - private void BroadcastDraftUpdate() + /// A SuggestedMention that can be obtained from OnDraftUpdated when ShouldSearchForSuggestions is set to true + /// The text to replace SuggestedMention.ReplaceFrom with. SuggestedMention.ReplaceTo can be used for example. + public void InsertSuggestedMention(SuggestedMention mention, string text) { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(pn_message_draft_consume_callback_data(pointer, buffer)); - var callbackDataJson = buffer.ToString(); - var callbackData = JsonConvert.DeserializeObject(callbackDataJson); - if (callbackData == null) + if (mention == null || string.IsNullOrEmpty(text) || mention.Target == null) { return; } - OnDraftUpdated?.Invoke(callbackData.MessageElements, callbackData.SuggestedMentions); + if (!ValidateSuggestedMention(mention)) + { + return; + } + + TriggerTypingIndicator(); + + // Remove the text that should be replaced + ApplyRemoveTextInternal(mention.Offset, mention.ReplaceFrom.Length); + + // Insert the new text + ApplyInsertTextInternal(mention.Offset, text); + + // Add mention for the inserted text + _mentions.Add(new InternalMention(mention.Offset, text.Length, mention.Target)); + + // Sort mentions by start position + _mentions.Sort((a, b) => a.Start.CompareTo(b.Start)); + + BroadcastDraftUpdate(); } /// @@ -164,7 +282,37 @@ private void BroadcastDraftUpdate() /// Text the text to insert at the given offset public void InsertText(int offset, string text) { - CUtilities.CheckCFunctionResult(pn_message_draft_insert_text(pointer, offset, text)); + if (string.IsNullOrEmpty(text) || offset < 0 || offset > _value.Length) + { + return; + } + + TriggerTypingIndicator(); + + // Insert text at the specified position + _value = _value.Insert(offset, text); + + // Filter out mentions that overlap with the insertion point and adjust positions + var newMentions = new List(); + + foreach (var mention in _mentions) + { + // Only keep mentions that don't overlap with the insertion point + if (offset <= mention.Start || offset >= mention.EndExclusive) + { + var newMention = new InternalMention(mention.Start, mention.Length, mention.Target); + + // Adjust start position if the mention comes after the insertion point + if (offset <= mention.Start) + { + newMention.Start += text.Length; + } + + newMentions.Add(newMention); + } + } + + _mentions = newMentions; BroadcastDraftUpdate(); } @@ -175,21 +323,40 @@ public void InsertText(int offset, string text) /// Length the number of characters to remove, starting at the given offset public void RemoveText(int offset, int length) { - CUtilities.CheckCFunctionResult(pn_message_draft_remove_text(pointer, offset, length)); - BroadcastDraftUpdate(); - } + if (offset < 0 || offset >= _value.Length || length <= 0) + { + return; + } - /// - /// Insert mention into the MessageDraft according to SuggestedMention.Offset, SuggestedMention.ReplaceFrom and - /// SuggestedMention.target. - /// - /// A SuggestedMention that can be obtained from MessageDraftStateListener - /// The text to replace SuggestedMention.ReplaceFrom with. SuggestedMention.ReplaceTo can be used for example. - public void InsertSuggestedMention(SuggestedMention mention, string text) - { - var jsonMentionTarget = JsonConvert.SerializeObject(mention.Target); - CUtilities.CheckCFunctionResult(pn_message_draft_insert_suggested_mention(pointer, mention.Offset, - mention.ReplaceFrom, mention.ReplaceTo, jsonMentionTarget, text)); + TriggerTypingIndicator(); + + // Clamp length to not exceed the text bounds + length = Math.Min(length, _value.Length - offset); + + // Remove text from the specified position + _value = _value.Remove(offset, length); + + // Filter out mentions that overlap with the removal range and adjust positions + var newMentions = new List(); + + foreach (var mention in _mentions) + { + // Only keep mentions that don't overlap with the removal range + if (offset > mention.EndExclusive || offset + length <= mention.Start) + { + var newMention = new InternalMention(mention.Start, mention.Length, mention.Target); + + // Adjust start position if the mention comes after the removal range + if (offset < mention.Start) + { + newMention.Start -= Math.Min(length, mention.Start - offset); + } + + newMentions.Add(newMention); + } + } + + _mentions = newMentions; BroadcastDraftUpdate(); } @@ -201,8 +368,17 @@ public void InsertSuggestedMention(SuggestedMention mention, string text) /// The target of the mention public void AddMention(int offset, int length, MentionTarget target) { - var jsonMentionTarget = JsonConvert.SerializeObject(target); - CUtilities.CheckCFunctionResult(pn_message_draft_add_mention(pointer, offset, length, jsonMentionTarget)); + if (target == null || offset < 0 || length <= 0 || offset + length > _value.Length) + { + return; + } + + // Add the mention to the list + _mentions.Add(new InternalMention(offset, length, target)); + + // Sort mentions by start position + _mentions.Sort((a, b) => a.Start.CompareTo(b.Start)); + BroadcastDraftUpdate(); } @@ -212,7 +388,12 @@ public void AddMention(int offset, int length, MentionTarget target) /// Offset the start of the mention to remove public void RemoveMention(int offset) { - CUtilities.CheckCFunctionResult(pn_message_draft_remove_mention(pointer, offset)); + // Remove mentions that start at the specified offset + _mentions.RemoveAll(mention => mention.Start == offset); + + // Sort mentions by start position + _mentions.Sort((a, b) => a.Start.CompareTo(b.Start)); + BroadcastDraftUpdate(); } @@ -226,43 +407,324 @@ public void RemoveMention(int offset) /// public void Update(string text) { - CUtilities.CheckCFunctionResult(pn_message_draft_update(pointer, text)); + text ??= string.Empty; + + TriggerTypingIndicator(); + + // Use diff-match-patch to compute differences + var diffs = _diffMatchPatch.diff_main(_value, text); + _diffMatchPatch.diff_cleanupSemantic(diffs); + + int consumed = 0; + + // Apply each diff operation + foreach (var diff in diffs) + { + switch (diff.operation) + { + case Operation.DELETE: + // Apply removal without broadcasting + ApplyRemoveTextInternal(consumed, diff.text.Length); + break; + + case Operation.INSERT: + // Apply insertion without broadcasting + ApplyInsertTextInternal(consumed, diff.text); + consumed += diff.text.Length; + break; + + case Operation.EQUAL: + consumed += diff.text.Length; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + BroadcastDraftUpdate(); } /// - /// Send the MessageDraft, along with its quotedMessage if any, on the channel. + /// Internal method to apply text insertion without triggering broadcasts + /// + private void ApplyInsertTextInternal(int offset, string text) + { + text ??= string.Empty; + if (offset < 0 || offset > _value.Length) + { + return; + } + + // Insert text at the specified position + _value = _value.Insert(offset, text); + + // Filter out mentions that overlap with the insertion point and adjust positions + var newMentions = new List(); + + foreach (var mention in _mentions) + { + // Only keep mentions that don't overlap with the insertion point + if (offset <= mention.Start || offset >= mention.EndExclusive) + { + var newMention = new InternalMention(mention.Start, mention.Length, mention.Target); + + // Adjust start position if the mention comes after the insertion point + if (offset <= mention.Start) + { + newMention.Start += text.Length; + } + + newMentions.Add(newMention); + } + } + + _mentions = newMentions; + } + + /// + /// Internal method to apply text removal without triggering broadcasts /// - public async Task Send() + private void ApplyRemoveTextInternal(int offset, int length) { - await Send(new SendTextParams()); + if (offset < 0 || offset >= _value.Length || length <= 0) + { + return; + } + + // Clamp length to not exceed the text bounds + length = Math.Min(length, _value.Length - offset); + + // Remove text from the specified position + _value = _value.Remove(offset, length); + + // Filter out mentions that overlap with the removal range and adjust positions + var newMentions = new List(); + + foreach (var mention in _mentions) + { + // Only keep mentions that don't overlap with the removal range + if (offset > mention.EndExclusive || offset + length <= mention.Start) + { + var newMention = new InternalMention(mention.Start, mention.Length, mention.Target); + + // Adjust start position if the mention comes after the removal range + if (offset < mention.Start) + { + newMention.Start -= Math.Min(length, mention.Start - offset); + } + + newMentions.Add(newMention); + } + } + + _mentions = newMentions; } /// /// Send the MessageDraft, along with its quotedMessage if any, on the channel. /// + public async Task Send() + { + return await Send(new SendTextParams()).ConfigureAwait(false); + } + + /// + /// Send the rendered MessageDraft on the channel. + /// /// Additional parameters for sending the message. - public async Task Send(SendTextParams sendTextParams) + public async Task Send(SendTextParams sendTextParams) { - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_message_draft_send( - pointer, - sendTextParams.StoreInHistory, - sendTextParams.SendByPost, - sendTextParams.Meta, - sendTextParams.MentionedUsers.Count, - sendTextParams.MentionedUsers.Keys.ToArray(), - sendTextParams.MentionedUsers.Values.Select(x => x.Pointer).ToArray(), - sendTextParams.QuotedMessage == null ? IntPtr.Zero : sendTextParams.QuotedMessage.Pointer))); + var mentions = new Dictionary(); + //TODO: revisit if this is the final data format and how to solve that we don't include name anywhere + var userMentionIndex = 0; + var channelReferenceIndex = 0; + var textLinkIndex = 0; + foreach (var internalMention in _mentions) + { + switch (internalMention.Target.Type) + { + case MentionType.User: + mentions.Add(userMentionIndex++, new MentionedUser(){Id = internalMention.Target.Target}); + break; + case MentionType.Channel: + var reference = new ReferencedChannel() { Id = internalMention.Target.Target }; + if (sendTextParams.Meta.TryGetValue("referencedChannels", out var refs)) + { + if (refs is Dictionary referencedChannels) + { + referencedChannels.Add(channelReferenceIndex++, reference); + } + } + else + { + sendTextParams.Meta.Add("referencedChannels", new Dictionary(){{channelReferenceIndex++, reference}}); + } + break; + case MentionType.Url: + var link = new TextLink() { StartIndex = internalMention.Start, EndIndex = internalMention.EndExclusive, Link = internalMention.Target.Target }; + if (sendTextParams.Meta.TryGetValue("textLinks", out var linkObjects)) + { + if (linkObjects is Dictionary links) + { + links.Add(textLinkIndex++, link); + } + } + else + { + sendTextParams.Meta.Add("textLinks", new Dictionary(){{textLinkIndex++, link}}); + } + break; + default: + break; + } + } + sendTextParams.MentionedUsers = mentions; + return await channel.SendText(Render(), sendTextParams).ConfigureAwait(false); } - public void SetSearchForSuggestions(bool searchForSuggestions) + /// + /// Validates that a suggested mention is valid for the current text + /// + private bool ValidateSuggestedMention(SuggestedMention suggestedMention) { - pn_message_draft_set_search_for_suggestions(pointer, searchForSuggestions); + if (suggestedMention.Offset < 0 || suggestedMention.Offset >= _value.Length) + { + return false; + } + if (string.IsNullOrEmpty(suggestedMention.ReplaceFrom)) + { + return false; + } + if (suggestedMention.Offset + suggestedMention.ReplaceFrom.Length > _value.Length) + { + return false; + } + + var substring = _value.Substring(suggestedMention.Offset, suggestedMention.ReplaceFrom.Length); + return substring == suggestedMention.ReplaceFrom; + } + + /// + /// Validates that mentions don't overlap and are within valid text bounds. + /// + /// True if all mentions are valid, false otherwise. + public bool ValidateMentions() + { + for (int i = 0; i < _mentions.Count; i++) + { + if (i > 0 && _mentions[i].Start < _mentions[i - 1].EndExclusive) + { + return false; + } + } + return true; } - ~MessageDraft() + /// + /// Gets message elements with plain text and links + /// + public List GetMessageElements() + { + var elements = new List(); + int lastPosition = 0; + + foreach (var mention in _mentions) + { + // Add plain text before the mention + if (lastPosition < mention.Start) + { + var plainText = _value.Substring(lastPosition, mention.Start - lastPosition); + if (!string.IsNullOrEmpty(plainText)) + { + elements.Add(new MessageElement { Text = plainText, MentionTarget = null }); + } + } + + // Add the mention element + var mentionText = _value.Substring(mention.Start, mention.Length); + elements.Add(new MessageElement { Text = mentionText, MentionTarget = mention.Target }); + + lastPosition = mention.EndExclusive; + } + + // Add remaining text after last mention + if (lastPosition < _value.Length) + { + var remainingText = _value.Substring(lastPosition); + if (!string.IsNullOrEmpty(remainingText)) + { + elements.Add(new MessageElement { Text = remainingText, MentionTarget = null }); + } + } + + return elements; + } + + /// + /// Renders the draft text with mentions converted to their appropriate schema format. + /// + /// Renders the message with markdown-style links. + /// + /// + /// The rendered text with schema-formatted mentions. + public string Render() + { + var elements = GetMessageElements(); + var result = new System.Text.StringBuilder(); + + foreach (var element in elements) + { + if (element.MentionTarget == null) + { + result.Append(element.Text); + } + else + { + var escapedText = EscapeLinkText(element.Text); + var escapedUrl = EscapeLinkUrl(element.MentionTarget.Target); + + switch (element.MentionTarget.Type) + { + case MentionType.User: + result.Append($"[{escapedText}]({SchemaUser}{escapedUrl})"); + break; + case MentionType.Channel: + result.Append($"[{escapedText}]({SchemaChannel}{escapedUrl})"); + break; + case MentionType.Url: + result.Append($"[{escapedText}]({escapedUrl})"); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + return result.ToString(); + } + + private async void TriggerTypingIndicator() + { + if (isTypingIndicatorTriggered && channel.Type == "public") + { + await channel.StartTyping().ConfigureAwait(false); + } + } + + /// + /// Escapes text for use in markdown links + /// + private static string EscapeLinkText(string text) + { + return text?.Replace("\\", "\\\\").Replace("]", "\\]") ?? string.Empty; + } + + /// + /// Escapes URLs for use in markdown links + /// + private static string EscapeLinkUrl(string url) { - pn_message_draft_delete(pointer); + return url?.Replace("\\", "\\\\").Replace(")", "\\)") ?? string.Empty; } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs index 8957743..cca5b1a 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -1,151 +1,165 @@ -using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json; -using PubnubChatApi.Utilities; +using PubnubApi; -namespace PubNubChatAPI.Entities +namespace PubnubChatApi { public class ThreadChannel : Channel { - #region DLL Imports + public string ParentChannelId { get; } + public string ParentMessageTimeToken { get; } - [DllImport("pubnub-chat")] - private static extern void pn_thread_channel_dispose( - IntPtr thread_channel); + private bool initialised; - [DllImport("pubnub-chat")] - private static extern int pn_thread_channel_get_history( - IntPtr thread_channel, - string start_timetoken, - string end_timetoken, - int count, - StringBuilder thread_messages_pointers_json); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_channel_pin_message_to_parent_channel(IntPtr thread_channel, - IntPtr message); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_channel_unpin_message_from_parent_channel(IntPtr thread_channel); - - [DllImport("pubnub-chat")] - private static extern int pn_thread_channel_get_parent_channel_id(IntPtr thread_channel, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_channel_parent_message(IntPtr thread_channel); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_channel_pin_message_to_thread(IntPtr thread_channel, IntPtr message); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_channel_unpin_message_from_thread(IntPtr thread_channel); - - [DllImport("pubnub-chat")] - private static extern int pn_thread_channel_send_text(IntPtr thread_channel, string text); - - #endregion - - public string ParentChannelId + internal ThreadChannel(Chat chat, string channelId, string parentChannelId, string parentMessageTimeToken, + ChatChannelData data) : base(chat, channelId, data) { - get - { - var buffer = new StringBuilder(128); - CUtilities.CheckCFunctionResult(pn_thread_channel_get_parent_channel_id(pointer, buffer)); - return buffer.ToString(); - } + ParentChannelId = parentChannelId; + ParentMessageTimeToken = parentMessageTimeToken; + data.CustomData["parentChannelId"] = ParentChannelId; + data.CustomData["parentMessageTimetoken"] = ParentMessageTimeToken; } - - public Message ParentMessage + + private async Task InitThreadChannel() { - get + var result = new ChatOperationResult("ThreadChannel.InitThreadChannel()", chat); + var channelUpdate = await UpdateChannelData(chat, Id, channelData).ConfigureAwait(false); + if (result.RegisterOperation(channelUpdate)) { - var parentMessagePointer = pn_thread_channel_parent_message(pointer); - CUtilities.CheckCFunctionResult(parentMessagePointer); - chat.TryGetMessage(parentMessagePointer, out var message); - return message; + return result; } - } - - internal static string MessageToThreadChannelId(Message message) - { - return $"PUBNUB_INTERNAL_THREAD_{message.ChannelId}_{message.Id}"; + result.RegisterOperation(await chat.PubnubInstance.AddMessageAction() + .Action(new PNMessageAction() { Type = "threadRootId", Value = Id }).Channel(ParentChannelId) + .MessageTimetoken(long.Parse(ParentMessageTimeToken)).ExecuteAsync().ConfigureAwait(false)); + return result; } - internal ThreadChannel(Chat chat, Message sourceMessage, IntPtr channelPointer) : base(chat, - MessageToThreadChannelId(sourceMessage), - channelPointer) + public override async Task SendText(string message, SendTextParams sendTextParams) { - } + var result = new ChatOperationResult("ThreadChannel.SendText()", chat); + if (!initialised) + { + if (result.RegisterOperation(await InitThreadChannel().ConfigureAwait(false))) + { + return result; + } - public override async Task PinMessage(Message message) - { - var newPointer = await Task.Run(() => pn_thread_channel_pin_message_to_thread(pointer, message.Pointer)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); - } + initialised = true; + } - public override async Task UnpinMessage() - { - var newPointer = await Task.Run(() => pn_thread_channel_unpin_message_from_thread(pointer)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + return await base.SendText(message, sendTextParams).ConfigureAwait(false); } - public async Task> GetThreadHistory(string startTimeToken, string endTimeToken, int count) + /// + /// Gets the message history for this thread channel. + /// + /// Retrieves the list of messages from this thread within the specified time range and + /// returns them as ThreadMessage objects that contain additional context about the parent channel. + /// + /// + /// The start time token for the history range. + /// The end time token for the history range. + /// The maximum number of messages to retrieve. + /// A ChatOperationResult containing the list of ThreadMessage objects from this thread. + /// + /// + /// var threadChannel = // ...; + /// var result = await threadChannel.GetThreadHistory("start_token", "end_token", 50); + /// if (!result.Error) { + /// foreach (var threadMessage in result.Result) { + /// Console.WriteLine($"Thread message: {threadMessage.MessageText}"); + /// Console.WriteLine($"Parent channel: {threadMessage.ParentChannelId}"); + /// } + /// } + /// + /// + /// + /// + public async Task>> GetThreadHistory(string startTimeToken, + string endTimeToken, int count) { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_thread_channel_get_history(pointer, startTimeToken, endTimeToken, count, - buffer))); - var messagesPointersJson = buffer.ToString(); - var history = new List(); - if (!CUtilities.IsValidJson(messagesPointersJson)) + var result = new ChatOperationResult>("ThreadChannel.GetThreadHistory()", chat) { - return history; - } - - var messagePointers = JsonConvert.DeserializeObject(messagesPointersJson); - if (messagePointers == null) + Result = new List() + }; + var getHistory = await GetMessageHistory(startTimeToken, endTimeToken, count).ConfigureAwait(false); + if (result.RegisterOperation(getHistory)) { - return history; + return result; } - foreach (var threadMessagePointer in messagePointers) + foreach (var message in getHistory.Result) { - var id = ThreadMessage.GetThreadMessageIdFromPtr(threadMessagePointer); - //This will also add a new wrapper if there wasn't one already - if(chat.TryGetThreadMessage(id, threadMessagePointer, out var threadMessage)) - { - history.Add(threadMessage); - } - else - { - Debug.WriteLine("Thread history messages aren't found/aren't thread messages - SHOULD BE IMPOSSIBLE!"); - } + result.Result.Add(new ThreadMessage(chat, message.TimeToken, message.OriginalMessageText, + message.ChannelId, ParentChannelId, message.UserId, PubnubChatMessageType.Text, message.Meta, + message.MessageActions)); } - return history; + + return result; } - public async Task PinMessageToParentChannel(ThreadMessage message) + public override async Task EmitUserMention(string userId, string timeToken, string text) { - var newChannelPointer = await Task.Run(() => pn_thread_channel_pin_message_to_parent_channel(pointer, message.Pointer)); - CUtilities.CheckCFunctionResult(newChannelPointer); - chat.UpdateChannelPointer(ParentChannelId, newChannelPointer); + var jsonDict = new Dictionary() + { + {"text",text}, + {"messageTimetoken",timeToken}, + {"channel",Id}, + {"parentChannel", ParentChannelId} + }; + return await chat.EmitEvent(PubnubChatEventType.Mention, userId, + chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)).ConfigureAwait(false); } - public async Task UnPinMessageFromParentChannel() + /// + /// Pins a thread message to the parent channel. + /// + /// Takes a message from this thread and pins it to the parent channel where the thread originated. + /// This allows important thread messages to be highlighted in the main channel. + /// + /// + /// The thread message to pin to the parent channel. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var threadChannel = // ...; + /// var threadMessage = // ... get a thread message + /// var result = await threadChannel.PinMessageToParentChannel(threadMessage); + /// if (!result.Error) { + /// // Thread message has been pinned to the parent channel + /// } + /// + /// + /// + /// + /// + public async Task PinMessageToParentChannel(ThreadMessage message) { - var newChannelPointer = await Task.Run(() => pn_thread_channel_unpin_message_from_parent_channel(pointer)); - CUtilities.CheckCFunctionResult(newChannelPointer); - chat.UpdateChannelPointer(ParentChannelId, newChannelPointer); + return await chat.PinMessageToChannel(ParentChannelId, message).ConfigureAwait(false); } - protected override void DisposePointer() + /// + /// Unpins the currently pinned message from the parent channel. + /// + /// Removes the pinned message from the parent channel where this thread originated. + /// This undoes a previous pin operation performed by PinMessageToParentChannel. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var threadChannel = // ...; + /// var result = await threadChannel.UnPinMessageFromParentChannel(); + /// if (!result.Error) { + /// // Message has been unpinned from the parent channel + /// } + /// + /// + /// + /// + public async Task UnPinMessageFromParentChannel() { - pn_thread_channel_dispose(pointer); + return await chat.UnpinMessageFromChannel(ParentChannelId).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs index 1b72a42..781e99f 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -1,446 +1,86 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json; -using PubnubChatApi.Entities.Data; -using PubnubChatApi.Enums; -using PubnubChatApi.Utilities; +using PubnubApi; -namespace PubNubChatAPI.Entities +namespace PubnubChatApi { public class ThreadMessage : Message { - #region DLL Imports - - [DllImport("pubnub-chat")] - private static extern void pn_thread_message_dispose(IntPtr thread_message); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_message_edit_text( - IntPtr message, - string text); - - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_text(IntPtr message, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_message_delete_message(IntPtr message); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_delete_message_hard(IntPtr message); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_deleted(IntPtr message); - [DllImport("pubnub-chat")] - private static extern void pn_thread_message_get_timetoken(IntPtr message, StringBuilder result); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_get_data_type(IntPtr message); - [DllImport("pubnub-chat")] - private static extern void pn_thread_message_get_data_text(IntPtr message, StringBuilder result); - [DllImport("pubnub-chat")] - private static extern void pn_thread_message_get_data_channel_id(IntPtr message, StringBuilder result); - [DllImport("pubnub-chat")] - private static extern void pn_thread_message_get_data_user_id(IntPtr message, StringBuilder result); - [DllImport("pubnub-chat")] - private static extern void pn_thread_message_get_data_meta(IntPtr message, StringBuilder result); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_get_data_message_actions(IntPtr message, StringBuilder result); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_pin(IntPtr message); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_get_reactions(IntPtr message, StringBuilder reactions_json); - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_message_toggle_reaction(IntPtr message, string reaction); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_has_user_reaction(IntPtr message, string reaction); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_report(IntPtr message, string reason); - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_message_create_thread(IntPtr message); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_has_thread(IntPtr message); - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_message_get_thread(IntPtr message); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_remove_thread(IntPtr message); - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_message_update_with_base_message(IntPtr message, IntPtr base_message); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_mentioned_users(IntPtr message, IntPtr chat, StringBuilder result); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_referenced_channels(IntPtr message, IntPtr chat, - StringBuilder result); - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_message_quoted_message(IntPtr message); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_text_links(IntPtr message, StringBuilder result); - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_message_restore(IntPtr message); - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_message_consume_and_upgrade( - IntPtr message, - string parent_channel_id); - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_message_unpin_from_parent_channel(IntPtr thread_message); - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_message_pin_to_parent_channel(IntPtr thread_message); - [DllImport("pubnub-chat")] - private static extern int pn_thread_message_parent_channel_id(IntPtr thread_message, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_thread_message_stream_updates(IntPtr message); - - #endregion - public event Action OnThreadMessageUpdated; - //TODO: some code repetition with Message in these property overrides - /// - /// The text content of the message. - /// - /// This is the main content of the message. It can be any text that the user wants to send. - /// - /// - public override string MessageText - { - get - { - var buffer = new StringBuilder(32768); - CUtilities.CheckCFunctionResult(pn_thread_message_text(pointer, buffer)); - return buffer.ToString(); - } - } - - /// - /// The original, un-edited text of the thread message. - /// - public override string OriginalMessageText - { - get - { - var buffer = new StringBuilder(32768); - pn_thread_message_get_data_text(pointer, buffer); - return buffer.ToString(); - } - } + public string ParentChannelId { get; } - /// - /// The time token of the message. - /// - /// The time token is a unique identifier for the message. - /// It is used to identify the message in the chat. - /// - /// - public override string TimeToken + internal ThreadMessage(Chat chat, string timeToken, string originalMessageText, string channelId, + string parentChannelId, string userId, PubnubChatMessageType type, Dictionary meta, + List messageActions) : base(chat, timeToken, originalMessageText, channelId, userId, type, + meta, messageActions) { - get - { - var buffer = new StringBuilder(512); - pn_thread_message_get_timetoken(pointer, buffer); - return buffer.ToString(); - } + ParentChannelId = parentChannelId; } - /// - /// The channel ID of the channel that the message belongs to. - /// - /// This is the ID of the channel that the message was sent to. - /// - /// - public override string ChannelId + protected override SubscribeCallback CreateUpdateListener() { - get - { - var buffer = new StringBuilder(512); - pn_thread_message_get_data_channel_id(pointer, buffer); - return buffer.ToString(); - } - } - - /// - /// The user ID of the user that sent the message. - /// - /// This is the unique ID of the user that sent the message. - /// Do not confuse this with the username of the user. - /// - /// - public override string UserId - { - get - { - var buffer = new StringBuilder(512); - pn_thread_message_get_data_user_id(pointer, buffer); - return buffer.ToString(); - } - } - - public override PubnubChatMessageType Type => (PubnubChatMessageType)pn_thread_message_get_data_type(pointer); - - /// - /// The metadata of the message. - /// - /// The metadata is additional data that can be attached to the message. - /// It can be used to store additional information about the message. - /// - /// - public override string Meta - { - get - { - var buffer = new StringBuilder(4096); - pn_thread_message_get_data_meta(pointer, buffer); - return buffer.ToString(); - } + return chat.ListenerFactory.ProduceListener( + messageActionCallback: delegate(Pubnub pn, PNMessageActionEventResult e) + { + if (ChatParsers.TryParseMessageUpdate(chat, this, e)) + { + OnThreadMessageUpdated?.Invoke(this); + } + }); } /// - /// Whether the message has been deleted. + /// Pins this thread message to the parent channel. /// - /// This property indicates whether the message has been deleted. - /// If the message has been deleted, this property will be true. - /// It means that all the deletions are soft deletions. + /// Takes this message from the thread and pins it to the parent channel where the thread originated. + /// This allows important thread messages to be highlighted in the main channel for all users to see. /// /// - public override bool IsDeleted - { - get - { - var result = pn_thread_message_deleted(pointer); - CUtilities.CheckCFunctionResult(result); - return result == 1; - } - } - - public override List MentionedUsers - { - get - { - var buffer = new StringBuilder(1024); - CUtilities.CheckCFunctionResult(pn_thread_message_mentioned_users(pointer, chat.Pointer, buffer)); - var usersJson = buffer.ToString(); - if (!CUtilities.IsValidJson(usersJson)) - { - return new List(); - } - var jsonDict = JsonConvert.DeserializeObject>(usersJson); - if (jsonDict == null || !jsonDict.TryGetValue("value", out var pointers) || pointers == null) - { - return new List(); - } - return PointerParsers.ParseJsonUserPointers(chat, pointers); - } - } - - public override List ReferencedChannels - { - get - { - var buffer = new StringBuilder(1024); - CUtilities.CheckCFunctionResult(pn_thread_message_referenced_channels(pointer, chat.Pointer, buffer)); - var channelsJson = buffer.ToString(); - if (!CUtilities.IsValidJson(channelsJson)) - { - return new List(); - } - var jsonDict = JsonConvert.DeserializeObject>(channelsJson); - if (jsonDict == null || !jsonDict.TryGetValue("value", out var pointers) || pointers == null) - { - return new List(); - } - return PointerParsers.ParseJsonChannelPointers(chat, pointers); - } - } - - public override List TextLinks - { - get - { - var buffer = new StringBuilder(2048); - CUtilities.CheckCFunctionResult(pn_thread_message_text_links(pointer, buffer)); - var jsonString = buffer.ToString(); - if (!CUtilities.IsValidJson(jsonString)) - { - return new List(); - } - var textLinks = JsonConvert.DeserializeObject>>(jsonString); - if (textLinks == null || !textLinks.TryGetValue("value", out var links) || links == null) - { - return new List(); - } - return links; - } - } - - public override List MessageActions - { - get - { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(pn_thread_message_get_data_message_actions(pointer, buffer)); - return DeserializeMessageActions(buffer.ToString()); - } - } - - public override List Reactions - { - get - { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(pn_thread_message_get_reactions(pointer, buffer)); - return DeserializeMessageActions(buffer.ToString()); - } - } - - public string ParentChannelId - { - get - { - var buffer = new StringBuilder(128); - CUtilities.CheckCFunctionResult(pn_thread_message_parent_channel_id(pointer, buffer)); - return buffer.ToString(); - } - } - - internal ThreadMessage(Chat chat, IntPtr messagePointer, string timeToken) : base(chat, messagePointer, - timeToken) - { - } - - protected override IntPtr StreamUpdates() + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var threadMessage = // ... get a thread message + /// var result = await threadMessage.PinMessageToParentChannel(); + /// if (!result.Error) { + /// // This thread message has been pinned to the parent channel + /// } + /// + /// + /// + /// + /// + /// + public async Task PinMessageToParentChannel() { - return pn_thread_message_stream_updates(pointer); + return await chat.PinMessageToChannel(ParentChannelId, this).ConfigureAwait(false); } /// - /// Edits the text of the message. + /// Unpins the currently pinned message from the parent channel. /// - /// This method edits the text of the message. - /// It changes the text of the message to the new text provided. + /// Removes the pinned message from the parent channel where this thread originated. + /// This is typically used when this thread message was previously pinned to the parent channel + /// and now needs to be unpinned. /// /// - /// The new text of the message. + /// A ChatOperationResult indicating the success or failure of the operation. /// /// - /// var message = // ...; - /// message.EditMessageText("New text"); + /// var threadMessage = // ... get a thread message + /// var result = await threadMessage.UnPinMessageFromParentChannel(); + /// if (!result.Error) { + /// // Message has been unpinned from the parent channel + /// } /// /// - /// - public override async Task EditMessageText(string newText) - { - var newPointer = await Task.Run(() => pn_thread_message_edit_text(pointer, newText)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); - } - - public override bool TryGetQuotedMessage(out Message quotedMessage) - { - var quotedMessagePointer = pn_thread_message_quoted_message(pointer); - if (quotedMessagePointer == IntPtr.Zero) - { - Debug.WriteLine(CUtilities.GetErrorMessage()); - quotedMessage = null; - return false; - } - return chat.TryGetMessage(quotedMessagePointer, out quotedMessage); - } - - public override async Task Report(string reason) - { - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_thread_message_report(pointer, reason))); - } - - public override async Task Forward(string channelId) - { - if (chat.TryGetChannel(channelId, out var channel)) - { - await chat.ForwardMessage(this, channel); - } - } - - public override bool HasUserReaction(string reactionValue) - { - var result = pn_thread_message_has_user_reaction(pointer, reactionValue); - CUtilities.CheckCFunctionResult(result); - return result == 1; - } - - public override async Task ToggleReaction(string reactionValue) - { - var newPointer = await Task.Run(() => pn_thread_message_toggle_reaction(pointer, reactionValue)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); - } - - public override async Task Restore() - { - var newPointer = await Task.Run(() => pn_thread_message_restore(pointer)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); - } - - public override async Task Delete(bool soft) - { - await Task.Run(() => - { - if (soft) - { - var newPointer = pn_thread_message_delete_message(pointer); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); - } - else - { - CUtilities.CheckCFunctionResult(pn_thread_message_delete_message_hard(pointer)); - } - }); - } - - internal override void BroadcastMessageUpdate() - { - base.BroadcastMessageUpdate(); - OnThreadMessageUpdated?.Invoke(this); - } - - internal static string GetThreadMessageIdFromPtr(IntPtr threadMessagePointer) - { - var buffer = new StringBuilder(128); - pn_thread_message_get_timetoken(threadMessagePointer, buffer); - return buffer.ToString(); - } - - internal static string GetChannelIdFromThreadMessagePtr(IntPtr messagePointer) - { - var buffer = new StringBuilder(512); - pn_thread_message_get_data_channel_id(messagePointer, buffer); - return buffer.ToString(); - } - - internal override void UpdateWithPartialPtr(IntPtr partialPointer) - { - var newFullPointer = pn_thread_message_update_with_base_message(partialPointer, pointer); - CUtilities.CheckCFunctionResult(newFullPointer); - UpdatePointer(newFullPointer); - } - - public async Task PinMessageToParentChannel() - { - var newChannelPointer = await Task.Run(() => pn_thread_message_pin_to_parent_channel(pointer)); - CUtilities.CheckCFunctionResult(newChannelPointer); - chat.UpdateChannelPointer(newChannelPointer); - } - - public async Task UnPinMessageFromParentChannel() - { - var newChannelPointer = await Task.Run(() => pn_thread_message_unpin_from_parent_channel(pointer)); - CUtilities.CheckCFunctionResult(newChannelPointer); - chat.UpdateChannelPointer(newChannelPointer); - } - - protected override void DisposePointer() + /// + /// + /// + public async Task UnPinMessageFromParentChannel() { - pn_thread_message_dispose(pointer); + return await chat.UnpinMessageFromChannel(ParentChannelId).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index e82b824..12d4707 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -1,16 +1,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Text; +using System.Linq; using System.Threading.Tasks; -using Newtonsoft.Json; -using PubnubChatApi.Entities.Data; -using PubnubChatApi.Entities.Events; -using PubnubChatApi.Enums; -using PubnubChatApi.Utilities; +using PubnubApi; -namespace PubNubChatAPI.Entities +namespace PubnubChatApi { /// /// Represents a user in the chat. @@ -20,64 +14,7 @@ namespace PubNubChatAPI.Entities /// public class User : UniqueChatEntity { - #region DLL Imports - - [DllImport("pubnub-chat")] - private static extern void pn_user_destroy(IntPtr user); - - [DllImport("pubnub-chat")] - private static extern int pn_user_is_present_on(IntPtr user, string channel_id); - - [DllImport("pubnub-chat")] - private static extern int pn_user_where_present(IntPtr user, StringBuilder result_json); - - [DllImport("pubnub-chat")] - private static extern void pn_user_get_user_id(IntPtr user, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_user_get_data_user_name(IntPtr user, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_user_get_data_external_id(IntPtr user, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_user_get_data_profile_url(IntPtr user, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_user_get_data_email(IntPtr user, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_user_get_data_custom_data(IntPtr user, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_user_get_data_status(IntPtr user, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern void pn_user_get_data_type(IntPtr user, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_user_get_channel_restrictions( - IntPtr user, - IntPtr channel, - StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_user_update_with_base(IntPtr user, IntPtr base_user); - - [DllImport("pubnub-chat")] - private static extern int pn_user_get_channels_restrictions(IntPtr user, string sort, int limit, string next, - string prev, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern int pn_user_active(IntPtr user); - - [DllImport("pubnub-chat")] - private static extern int pn_user_last_active_timestamp(IntPtr user, StringBuilder result); - - [DllImport("pubnub-chat")] - private static extern IntPtr pn_user_stream_updates(IntPtr user); - - #endregion + private ChatUserData userData; /// /// The user's user name. @@ -85,15 +22,7 @@ private static extern int pn_user_get_channels_restrictions(IntPtr user, string /// This might be user's display name in the chat. /// /// - public string UserName - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_user_name(pointer, buffer); - return buffer.ToString(); - } - } + public string UserName => userData.Username; /// /// The user's external id. @@ -101,15 +30,7 @@ public string UserName /// This might be user's id in the external system (e.g. Database, CRM, etc.) /// /// - public string ExternalId - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_external_id(pointer, buffer); - return buffer.ToString(); - } - } + public string ExternalId => userData.ExternalId; /// /// The user's profile url. @@ -117,15 +38,7 @@ public string ExternalId /// This might be user's profile url to download the profile picture. /// /// - public string ProfileUrl - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_profile_url(pointer, buffer); - return buffer.ToString(); - } - } + public string ProfileUrl => userData.ProfileUrl; /// /// The user's email. @@ -133,15 +46,7 @@ public string ProfileUrl /// This should be user's email address. /// /// - public string Email - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_email(pointer, buffer); - return buffer.ToString(); - } - } + public string Email => userData.Email; /// /// The user's custom data. @@ -149,15 +54,7 @@ public string Email /// This might be any custom data that you want to store for the user. /// /// - public string CustomData - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_custom_data(pointer, buffer); - return buffer.ToString(); - } - } + public Dictionary CustomData => userData.CustomData; /// /// The user's status. @@ -165,15 +62,7 @@ public string CustomData /// This is a string that represents the user's status. /// /// - public string Status - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_status(pointer, buffer); - return buffer.ToString(); - } - } + public string Status => userData.Status; /// /// The user's data type. @@ -181,23 +70,20 @@ public string Status /// This is a string that represents the user's data type. /// /// - public string DataType - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_type(pointer, buffer); - return buffer.ToString(); - } - } + public string DataType => userData.Type; public bool Active { get { - var result = pn_user_active(pointer); - CUtilities.CheckCFunctionResult(result); - return result == 1; + if (CustomData == null || !CustomData.TryGetValue("lastActiveTimestamp", out var lastActiveTimestamp)) + { + return false; + } + var currentTimeStamp = ChatUtils.TimeTokenNowLong(); + var interval = chat.Config.StoreUserActivityInterval; + var lastActive = Convert.ToInt64(lastActiveTimestamp); + return currentTimeStamp - lastActive <= interval * 1000000; } } @@ -205,17 +91,14 @@ public string LastActiveTimeStamp { get { - var buffer = new StringBuilder(64); - CUtilities.CheckCFunctionResult(pn_user_last_active_timestamp(pointer, buffer)); - return buffer.ToString(); + if (CustomData == null || !CustomData.TryGetValue("lastActiveTimestamp", out var lastActiveTimestamp)) + { + return string.Empty; + } + return lastActiveTimestamp.ToString(); } } - - private Chat chat; - private IntPtr mentionsListeningHandle = IntPtr.Zero; - private IntPtr invitesListeningHandle = IntPtr.Zero; - private IntPtr moderationListeningHandle = IntPtr.Zero; - + /// /// Event that is triggered when the user is updated. /// @@ -235,96 +118,198 @@ public string LastActiveTimeStamp /// /// public event Action OnUserUpdated; - + + private Subscription mentionsSubscription; + private Subscription invitesSubscription; + private Subscription moderationSubscription; public event Action OnMentionEvent; public event Action OnInviteEvent; public event Action OnModerationEvent; - internal User(Chat chat, string userId, IntPtr userPointer) : base(userPointer, userId) + protected override string UpdateChannelId => Id; + + internal User(Chat chat, string userId, ChatUserData chatUserData) : base(chat, userId) { - this.chat = chat; + UpdateLocalData(chatUserData); } - public async void SetListeningForMentionEvents(bool listen) + protected override SubscribeCallback CreateUpdateListener() { - mentionsListeningHandle = await SetListening(mentionsListeningHandle, listen, - () => chat.ListenForEvents(Id, PubnubChatEventType.Mention)); + return chat.ListenerFactory.ProduceListener(objectEventCallback: delegate(Pubnub pn, PNObjectEventResult e) + { + if (ChatParsers.TryParseUserUpdate(chat, this, e, out var updatedData)) + { + UpdateLocalData(updatedData); + OnUserUpdated?.Invoke(this); + } + }); } - - internal void BroadcastMentionEvent(ChatEvent chatEvent) + + /// + /// Sets whether to listen for mention events for this user. + /// + /// When enabled, the user will receive mention events when they are mentioned in messages. + /// + /// + /// True to start listening, false to stop listening. + /// + public void SetListeningForMentionEvents(bool listen) { - OnMentionEvent?.Invoke(chatEvent); + SetListening(ref mentionsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Mention, out var mentionEvent)) + { + OnMentionEvent?.Invoke(mentionEvent); + chat.BroadcastAnyEvent(mentionEvent); + } + })); } - public async void SetListeningForInviteEvents(bool listen) - { - invitesListeningHandle = await SetListening(invitesListeningHandle, listen, - () => chat.ListenForEvents(Id, PubnubChatEventType.Invite)); - } - - internal void BroadcastInviteEvent(ChatEvent chatEvent) + /// + /// Sets whether to listen for invite events for this user. + /// + /// When enabled, the user will receive invite events when they are invited to channels. + /// + /// + /// True to start listening, false to stop listening. + /// + public void SetListeningForInviteEvents(bool listen) { - OnInviteEvent?.Invoke(chatEvent); + SetListening(ref invitesSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Invite, out var inviteEvent)) + { + OnInviteEvent?.Invoke(inviteEvent); + chat.BroadcastAnyEvent(inviteEvent); + } + })); } - public async void SetListeningForModerationEvents(bool listen) + /// + /// Sets whether to listen for moderation events for this user. + /// + /// When enabled, the user will receive moderation events such as bans, mutes, and other restrictions. + /// + /// + /// True to start listening, false to stop listening. + /// + public void SetListeningForModerationEvents(bool listen) { - moderationListeningHandle = await SetListening(moderationListeningHandle, listen, - () => chat.ListenForEvents($"PUBNUB_INTERNAL_MODERATION.{Id}", PubnubChatEventType.Moderation)); + SetListening(ref moderationSubscription, SubscriptionOptions.None, listen, Chat.INTERNAL_MODERATION_PREFIX+Id, chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Moderation, out var moderationEvent)) + { + OnModerationEvent?.Invoke(moderationEvent); + chat.BroadcastAnyEvent(moderationEvent); + } + })); } - internal void BroadcastModerationEvent(ChatEvent chatEvent) - { - OnModerationEvent?.Invoke(chatEvent); - } - - protected override IntPtr StreamUpdates() + /// + /// Updates the user. + /// + /// This method updates the user's data. + /// + /// + /// The updated data for the user. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var user = // ...; + /// var result = await user.Update(new ChatUserData + /// { + /// UserName = "New User Name", + /// }); + /// + /// + /// + public async Task Update(ChatUserData updatedData) { - return pn_user_stream_updates(pointer); + UpdateLocalData(updatedData); + var result = new ChatOperationResult("User.Update()", chat); + result.RegisterOperation(await UpdateUserData(chat, Id, updatedData).ConfigureAwait(false)); + return result; } - internal static string GetUserIdFromPtr(IntPtr userPointer) + internal static async Task> UpdateUserData(Chat chat, string userId, ChatUserData chatUserData) { - var buffer = new StringBuilder(512); - pn_user_get_user_id(userPointer, buffer); - return buffer.ToString(); + var operation = chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true).IncludeStatus(true).IncludeType(true).Uuid(userId); + if (!string.IsNullOrEmpty(chatUserData.Username)) + { + operation = operation.Name(chatUserData.Username); + } + if (!string.IsNullOrEmpty(chatUserData.Email)) + { + operation = operation.Email(chatUserData.Email); + } + if (!string.IsNullOrEmpty(chatUserData.ExternalId)) + { + operation = operation.ExternalId(chatUserData.ExternalId); + } + if (!string.IsNullOrEmpty(chatUserData.ProfileUrl)) + { + operation = operation.ProfileUrl(chatUserData.ProfileUrl); + } + if (!string.IsNullOrEmpty(chatUserData.Type)) + { + operation = operation.Type(chatUserData.Type); + } + if (!string.IsNullOrEmpty(chatUserData.Status)) + { + operation = operation.Status(chatUserData.Status); + } + if (chatUserData.CustomData != null) + { + operation = operation.Custom(chatUserData.CustomData); + } + return await operation.ExecuteAsync().ConfigureAwait(false); } - - internal override void UpdateWithPartialPtr(IntPtr partialPointer) + + internal static async Task> GetUserData(Chat chat, string userId) { - var newFullPointer = pn_user_update_with_base(partialPointer, pointer); - CUtilities.CheckCFunctionResult(newFullPointer); - UpdatePointer(newFullPointer); + return await chat.PubnubInstance.GetUuidMetadata().Uuid(userId).IncludeCustom(true).ExecuteAsync().ConfigureAwait(false); } - internal void BroadcastUserUpdate() + internal void UpdateLocalData(ChatUserData? newData) { - OnUserUpdated?.Invoke(this); + if (newData == null) + { + return; + } + userData = newData; } - + /// - /// Updates the user. + /// Refreshes the user data from the server. /// - /// This method updates the user's data. + /// Fetches the latest user information from the server and updates the local data. + /// This is useful when you want to ensure you have the most up-to-date user information. /// /// - /// The updated data for the user. - /// - /// This exception might be thrown when any error occurs while updating the user. - /// + /// A ChatOperationResult indicating the success or failure of the refresh operation. /// /// /// var user = // ...; - /// user.UpdateUser(new ChatUserData - /// { - /// UserName = "New User Name", - /// }); + /// var result = await user.Refresh(); + /// if (!result.Error) { + /// // User data has been refreshed + /// Console.WriteLine($"User name: {user.UserName}"); + /// } /// /// - /// - public async Task Update(ChatUserData updatedData) + public override async Task Refresh() { - await chat.UpdateUser(Id, updatedData); + var result = new ChatOperationResult("User.Refresh()", chat); + var getUserData = await GetUserData(chat, Id).ConfigureAwait(false); + if (result.RegisterOperation(getUserData)) + { + return result; + } + UpdateLocalData(getUserData.Result); + return result; } /// @@ -334,18 +319,16 @@ public async Task Update(ChatUserData updatedData) /// It will remove the user from all the channels and delete the user's data. /// /// - /// - /// This exception might be thrown when any error occurs while deleting the user. - /// + /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// var user = // ...; - /// user.DeleteUser(); + /// await user.DeleteUser(); /// /// - public async Task DeleteUser() + public async Task DeleteUser() { - await chat.DeleteUser(Id); + return await chat.DeleteUser(Id).ConfigureAwait(false); } /// @@ -359,73 +342,158 @@ public async Task DeleteUser() /// If set to true, the user is banned from the channel. /// If set to true, the user is muted on the channel. /// The reason for setting the restrictions on the user. - /// - /// This exception might be thrown when any error occurs while setting the restrictions on the user. - /// /// /// /// var user = // ...; /// user.SetRestrictions("channel_id", true, false, "Banned from the channel"); /// /// - public async Task SetRestriction(string channelId, bool banUser, bool muteUser, string reason) + public async Task SetRestriction(string channelId, bool banUser, bool muteUser, string reason) { - await chat.SetRestriction(Id, channelId, banUser, muteUser, reason); + return await chat.SetRestriction(Id, channelId, banUser, muteUser, reason).ConfigureAwait(false); } - public async Task SetRestriction(string channelId, Restriction restriction) + /// + /// Sets restrictions on the user using a Restriction object. + /// + /// This method sets the restrictions on the user using a structured Restriction object + /// that contains ban, mute, and reason information. + /// + /// + /// The channel id on which the restrictions are set. + /// The restriction object containing ban, mute, and reason information. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + public async Task SetRestriction(string channelId, Restriction restriction) { - await chat.SetRestriction(Id, channelId, restriction); + return await chat.SetRestriction(Id, channelId, restriction).ConfigureAwait(false); } /// - /// Gets the restrictions on the user for the channel. + /// Gets the restrictions on the user for a specific channel. /// - /// This method gets the restrictions on the user for the channel. - /// You can get the restrictions on the user for the channel. + /// This method gets the restrictions (bans and mutes) that have been applied to this user + /// on the specified channel. /// /// - /// The channel id for which the restrictions are to be fetched. - /// The limit on the number of restrictions to be fetched. - /// The start time token from which the restrictions are to be fetched. - /// The end time token till which the restrictions are to be fetched. - /// - /// The restrictions on the user for the channel. - /// - /// - /// This exception might be thrown when any error occurs while getting the restrictions on the user for the channel. - /// - public async Task GetChannelRestrictions(Channel channel) + /// The channel for which the restrictions are to be fetched. + /// A ChatOperationResult containing the Restriction object if restrictions exist for this user on the channel, error otherwise. + /// + /// + /// var user = // ...; + /// var channel = // ...; + /// var result = await user.GetChannelRestrictions(channel); + /// var restriction = result.Result; + /// + /// + /// + public async Task> GetChannelRestrictions(Channel channel) { - var buffer = new StringBuilder(8192); - CUtilities.CheckCFunctionResult(await Task.Run(() => - pn_user_get_channel_restrictions(pointer, channel.Pointer, buffer))); - var restrictionJson = buffer.ToString(); - var restriction = new Restriction(); - if (CUtilities.IsValidJson(restrictionJson)) + var result = new ChatOperationResult("User.GetChannelRestrictions()", chat); + var membersResult = await chat.PubnubInstance.GetChannelMembers().Channel($"{Chat.INTERNAL_MODERATION_PREFIX}_{channel.Id}").Include(new[] + { + PNChannelMemberField.CUSTOM + }).Filter($"uuid.id == \"{Id}\"").IncludeCount(true).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(membersResult) || membersResult.Result.ChannelMembers == null || !membersResult.Result.ChannelMembers.Any()) { - restriction = JsonConvert.DeserializeObject(restrictionJson); + result.Error = true; + return result; } - - return restriction; + var member = membersResult.Result.ChannelMembers[0]; + try + { + result.Result = new Restriction() + { + Ban = (bool)member.Custom["ban"], + Mute = (bool)member.Custom["mute"], + Reason = (string)member.Custom["reason"] + }; + } + catch (Exception e) + { + result.Error = true; + result.Exception = e; + } + return result; } - public async Task GetChannelsRestrictions(string sort = "", int limit = 0, - Page page = null) + /// + /// Gets all channel restrictions for this user across all channels. + /// + /// This method retrieves all restrictions (bans and mutes) that have been applied to this user + /// across all channels where they have restrictions. + /// + /// + /// Sort criteria for restrictions. + /// The maximum number of restrictions to retrieve. + /// Pagination object for retrieving specific page results. + /// A ChatOperationResult containing the wrapper with all channel restrictions for this user. + /// + /// + /// var user = // ...; + /// var result = await user.GetChannelsRestrictions(limit: 10); + /// var restrictions = result.Result.Restrictions; + /// foreach (var restriction in restrictions) { + /// Console.WriteLine($"Channel: {restriction.ChannelId}, Ban: {restriction.Ban}, Mute: {restriction.Mute}"); + /// } + /// + /// + /// + /// + public async Task> GetChannelsRestrictions(string sort = "", int limit = 0, + PNPageObject page = null) { - page ??= new Page(); - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(await Task.Run(() => - pn_user_get_channels_restrictions(pointer, sort, limit, page.Next, page.Previous, buffer))); - var restrictionsJson = buffer.ToString(); - if (!CUtilities.IsValidJson(restrictionsJson)) + var result = new ChatOperationResult("User.GetChannelsRestrictions()", chat){Result = new ChannelsRestrictionsWrapper()}; + var operation = chat.PubnubInstance.GetMemberships().Uuid(Id) + .Include(new[] + { + PNMembershipField.CUSTOM, + PNMembershipField.CHANNEL + }).Filter($"channel.id LIKE \"{Chat.INTERNAL_MODERATION_PREFIX}_*\"").IncludeCount(true); + if (!string.IsNullOrEmpty(sort)) + { + operation = operation.Sort(new List() { sort }); + } + if (limit > 0) + { + operation = operation.Limit(limit); + } + if (page != null) { - return new ChannelsRestrictionsWrapper(); + operation = operation.Page(page); + } + var membershipsResult = await operation.ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(membershipsResult)) + { + return result; } - var wrapper = JsonConvert.DeserializeObject(restrictionsJson); - wrapper ??= new ChannelsRestrictionsWrapper(); - return wrapper; + result.Result.Page = membershipsResult.Result.Page; + result.Result.Total = membershipsResult.Result.TotalCount; + foreach (var membership in membershipsResult.Result.Memberships) + { + try + { + var internalChannelId = membership.ChannelMetadata.Channel; + var removeString = $"{Chat.INTERNAL_MODERATION_PREFIX}_"; + var index = internalChannelId.IndexOf(removeString, StringComparison.Ordinal); + var channelId = (index < 0) + ? internalChannelId + : internalChannelId.Remove(index, removeString.Length); + result.Result.Restrictions.Add(new ChannelRestriction() + { + Ban = (bool)membership.Custom["ban"], + Mute = (bool)membership.Custom["mute"], + Reason = (string)membership.Custom["reason"], + ChannelId = channelId + }); + } + catch (Exception e) + { + chat.Logger.Warn($"Incorrect data was encountered when parsing Channel Restriction for User \"{Id}\" in Channel \"{membership.ChannelMetadata.Channel}\". Exception was: {e.Message}"); + } + } + return result; } /// @@ -436,11 +504,8 @@ public async Task GetChannelsRestrictions(string so /// /// The channel id on which the user's presence is to be checked. /// - /// true if the user is present on the channel; otherwise, false. + /// A ChatOperationResult with true if the user is present on the channel; otherwise, false. /// - /// - /// This exception might be thrown when any error occurs while checking if the user is present on the channel. - /// /// /// /// var user = // ...; @@ -449,11 +514,16 @@ public async Task GetChannelsRestrictions(string so /// } /// /// - public async Task IsPresentOn(string channelId) + public async Task> IsPresentOn(string channelId) { - var result = await Task.Run(() => pn_user_is_present_on(pointer, channelId)); - CUtilities.CheckCFunctionResult(result); - return result == 1; + var result = new ChatOperationResult("User.IsPresentOn()", chat); + var response = await chat.PubnubInstance.WhereNow().Uuid(Id).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(response)) + { + return result; + } + result.Result = response.Result.Channels.Contains(channelId); + return result; } /// @@ -463,36 +533,35 @@ public async Task IsPresentOn(string channelId) /// /// /// - /// The list of channels where the user is present. + /// A ChatOperationResult containing the list of channels where the user is present. /// /// /// The list is kept as a list of channel ids. /// - /// - /// This exception might be thrown when any error occurs while getting the list of channels where the user is present. - /// /// /// /// var user = // ...; - /// var channels = user.WherePresent(); + /// var result = await user.WherePresent(); + /// var channels = result.Result; /// foreach (var channel in channels) { /// Console.WriteLine(channel); /// } /// /// - public async Task> WherePresent() + public async Task>> WherePresent() { - var buffer = new StringBuilder(32768); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_user_where_present(pointer, buffer))); - var jsonChannelIds = buffer.ToString(); - var channelIds = new List(); - if (!string.IsNullOrEmpty(jsonChannelIds) && jsonChannelIds != "[]" && jsonChannelIds != "{}") + var result = new ChatOperationResult>("User.WherePresent()", chat); + var where = await chat.PubnubInstance.WhereNow().Uuid(Id).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(where)) { - channelIds = JsonConvert.DeserializeObject>(jsonChannelIds); - channelIds ??= new List(); + return result; } - - return channelIds; + result.Result = new List(); + if (where.Result != null) + { + result.Result.AddRange(where.Result.Channels); + } + return result; } /// @@ -502,46 +571,28 @@ public async Task> WherePresent() /// All the relationships of the user with the channels are considered as memberships. /// /// + /// The filter parameter. + /// The sort parameter. /// The limit on the number of memberships to be fetched. - /// The start time token from which the memberships are to be fetched. - /// The end time token till which the memberships are to be fetched. + /// The page object for pagination. /// - /// The list of memberships of the user. + /// A ChatOperationResult containing the list of memberships of the user. /// - /// - /// This exception might be thrown when any error occurs while getting the list of memberships of the user. - /// /// /// /// var user = // ...; - /// var memberships = user.GetMemberships(50, "99999999999999999", "00000000000000000"); + /// var result = await user.GetMemberships(limit: 50); + /// var memberships = result.Result.Memberships; /// foreach (var membership in memberships) { /// Console.WriteLine(membership.ChannelId); /// } /// /// /// - public async Task GetMemberships(string filter = "", string sort = "", int limit = 0, - Page page = null) - { - return await chat.GetUserMemberships(Id, filter, sort, limit, page); - } - - protected override async Task CleanupConnectionHandles() - { - await base.CleanupConnectionHandles(); - mentionsListeningHandle = await SetListening(mentionsListeningHandle, false, - () => chat.ListenForEvents(Id, PubnubChatEventType.Mention)); - invitesListeningHandle = await SetListening(invitesListeningHandle, false, - () => chat.ListenForEvents(Id, PubnubChatEventType.Invite)); - moderationListeningHandle = await SetListening(moderationListeningHandle, false, - () => chat.ListenForEvents(Id, PubnubChatEventType.Moderation)); - } - - protected override void DisposePointer() + public async Task> GetMemberships(string filter = "", string sort = "", int limit = 0, + PNPageObject page = null) { - pn_user_destroy(pointer); - pointer = IntPtr.Zero; + return await chat.GetUserMemberships(Id, filter, sort, limit, page).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubAccessPermission.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubAccessPermission.cs index 4b556e5..e85e0fa 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubAccessPermission.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubAccessPermission.cs @@ -1,4 +1,4 @@ -namespace PubnubChatApi.Enums +namespace PubnubChatApi { public enum PubnubAccessPermission { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubAccessResourceType.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubAccessResourceType.cs index 94b0399..0bf0e79 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubAccessResourceType.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubAccessResourceType.cs @@ -1,4 +1,4 @@ -namespace PubnubChatApi.Enums +namespace PubnubChatApi { public enum PubnubAccessResourceType { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubChannelType.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubChannelType.cs index 3bb22fa..a73a6af 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubChannelType.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubChannelType.cs @@ -1,4 +1,4 @@ -namespace PubnubChatApi.Enums +namespace PubnubChatApi { public enum PubnubChannelType { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubChatEventType.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubChatEventType.cs index dfd9e91..3a2cdfc 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubChatEventType.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubChatEventType.cs @@ -1,4 +1,4 @@ -namespace PubnubChatApi.Enums +namespace PubnubChatApi { public enum PubnubChatEventType { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubChatMessageType.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubChatMessageType.cs index 68c3959..56369c4 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubChatMessageType.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubChatMessageType.cs @@ -1,4 +1,4 @@ -namespace PubnubChatApi.Enums +namespace PubnubChatApi { /// /// Represents the type of a chat message. diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubMessageActionType.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubMessageActionType.cs index 8c7690f..d060b03 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubMessageActionType.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Enums/PubnubMessageActionType.cs @@ -1,4 +1,4 @@ -namespace PubnubChatApi.Enums +namespace PubnubChatApi { public enum PubnubMessageActionType { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj b/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj index 3d08172..86d783e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj @@ -12,14 +12,9 @@ bin\Release\PubnubChatApi.xml - - - PreserveNewest - - - + diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/CUtilities.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/CUtilities.cs deleted file mode 100644 index 59c57f8..0000000 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/CUtilities.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Text; - -namespace PubnubChatApi.Utilities -{ - public class PubNubCCoreException : Exception - { - public PubNubCCoreException(string message) : base(message) - { - } - } - - internal static class CUtilities - { - [DllImport("pubnub-chat")] - private static extern void pn_c_get_error_message(StringBuilder buffer); - - private static void ThrowCError() - { - var errorMessage = GetErrorMessage(); - Debug.WriteLine($"Throwing C-side Error: {errorMessage}"); - throw new PubNubCCoreException(errorMessage); - } - - internal static string GetErrorMessage() - { - var buffer = new StringBuilder(4096); - pn_c_get_error_message(buffer); - return buffer.ToString(); - } - - internal static void CheckCFunctionResult(int result) - { - if (result == -1) - { - ThrowCError(); - } - } - - internal static void CheckCFunctionResult(IntPtr result) - { - if (result == IntPtr.Zero) - { - ThrowCError(); - } - } - - internal static bool IsValidJson(string json) - { - return !string.IsNullOrEmpty(json) && json != "{}" && json != "[]"; - } - } -} diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatEnumConverters.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatEnumConverters.cs new file mode 100644 index 0000000..37c05cd --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatEnumConverters.cs @@ -0,0 +1,75 @@ +using System; + +namespace PubnubChatApi +{ + internal static class ChatEnumConverters + { + internal static string ChatEventTypeToString(PubnubChatEventType eventType) + { + switch(eventType) + { + case PubnubChatEventType.Typing: + return "typing"; + case PubnubChatEventType.Report: + return "report"; + case PubnubChatEventType.Receipt: + return "receipt"; + case PubnubChatEventType.Mention: + return "mention"; + case PubnubChatEventType.Invite: + return "invite"; + case PubnubChatEventType.Custom: + return "custom"; + case PubnubChatEventType.Moderation: + return "moderation"; + default: + return "incorrect_chat_event_type"; + break; + } + } + + internal static PubnubChatEventType StringToEventType(string eventString) + { + switch (eventString) + { + case "typing": + return PubnubChatEventType.Typing; + case "report": + return PubnubChatEventType.Report; + case "receipt": + return PubnubChatEventType.Receipt; + case "mention": + return PubnubChatEventType.Mention; + case "invite": + return PubnubChatEventType.Invite; + case "custom": + return PubnubChatEventType.Custom; + case "moderation": + return PubnubChatEventType.Moderation; + default: + throw new ArgumentOutOfRangeException(); + } + } + + internal static PubnubMessageActionType StringToActionType(string actionString) + { + switch (actionString) + { + case "reaction": + return PubnubMessageActionType.Reaction; + case "receipt": + return PubnubMessageActionType.Receipt; + case "custom": + return PubnubMessageActionType.Custom; + case "edited": + return PubnubMessageActionType.Edited; + case "deleted": + return PubnubMessageActionType.Deleted; + case "threadRootId": + return PubnubMessageActionType.ThreadRootId; + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatListenerFactory.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatListenerFactory.cs new file mode 100644 index 0000000..4fd35d7 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatListenerFactory.cs @@ -0,0 +1,16 @@ +using System; +using PubnubApi; + +namespace PubnubChatApi +{ + public abstract class ChatListenerFactory + { + public abstract SubscribeCallback ProduceListener(Action>? messageCallback = null, + Action? presenceCallback = null, + Action>? signalCallback = null, + Action? objectEventCallback = null, + Action? messageActionCallback = null, + Action? fileCallback = null, + Action? statusCallback = null); + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs new file mode 100644 index 0000000..94248cf --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PubnubApi; + +namespace PubnubChatApi +{ + internal static class ChatParsers + { + internal static bool TryParseMessageResult(Chat chat, PNMessageResult messageResult, out Message message) + { + try + { + var messageDict = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(messageResult.Message + .ToString()); + + if (!messageDict.TryGetValue("type", out var typeValue) || typeValue.ToString() != "text") + { + message = null; + return false; + } + + //TODO: later more types I guess? + var type = PubnubChatMessageType.Text; + var text = messageDict["text"].ToString(); + var meta = messageResult.UserMetadata ?? new Dictionary(); + + message = new Message(chat, messageResult.Timetoken.ToString(), text, messageResult.Channel, messageResult.Publisher, type, meta, new List()); + return true; + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNMessageResult with payload: {messageResult.Message} into chat Message entity. Exception was: {e.Message}"); + message = null; + return false; + } + } + + internal static bool TryParseMessageFromHistory(Chat chat, string channelId, PNHistoryItemResult historyItem, out Message message) + { + try + { + var messageDict = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(historyItem.Entry.ToString()); + + if (!messageDict.TryGetValue("type", out var typeValue) || typeValue.ToString() != "text") + { + message = null; + return false; + } + + //TODO: later more types I guess? + var type = PubnubChatMessageType.Text; + var text = messageDict["text"].ToString(); + + var actions = new List(); + if (historyItem.ActionItems != null) + { + foreach (var kvp in historyItem.ActionItems) + { + var actionType = ChatEnumConverters.StringToActionType(kvp.Key); + foreach (var actionEntry in kvp.Value) + { + actions.Add(new MessageAction() + { + TimeToken = actionEntry.ActionTimetoken.ToString(), + UserId = actionEntry.Uuid, + Type = actionType, + Value = actionEntry.Action.Value + }); + } + } + } + message = new Message(chat, historyItem.Timetoken.ToString(), text, channelId, historyItem.Uuid, type, historyItem.Meta, actions); + return true; + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNHistoryItemResult with payload: {historyItem.Entry} into chat Message entity. Exception was: {e.Message}"); + message = null; + return false; + } + } + + internal static bool TryParseMembershipUpdate(Chat chat, Membership membership, PNObjectEventResult objectEvent, out ChatMembershipData updatedData) + { + try + { + var channel = objectEvent.MembershipMetadata.Channel; + var user = objectEvent.MembershipMetadata.Uuid; + var type = objectEvent.Type; + if (type == "membership" && channel == membership.ChannelId && user == membership.UserId) + { + updatedData = new ChatMembershipData() + { + Status = objectEvent.MembershipMetadata.Status, + CustomData = objectEvent.MembershipMetadata.Custom, + Type = objectEvent.MembershipMetadata.Type + }; + return true; + } + else + { + updatedData = null; + return false; + } + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into Membership update. Exception was: {e.Message}"); + updatedData = null; + return false; + } + } + + internal static bool TryParseUserUpdate(Chat chat, User user, PNObjectEventResult objectEvent, out ChatUserData updatedData) + { + try + { + var uuid = objectEvent.UuidMetadata.Uuid; + var type = objectEvent.Type; + if (type == "uuid" && uuid == user.Id) + { + updatedData = objectEvent.UuidMetadata; + return true; + } + else + { + updatedData = null; + return false; + } + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into User update. Exception was: {e.Message}"); + updatedData = null; + return false; + } + } + + internal static bool TryParseChannelUpdate(Chat chat, Channel channel, PNObjectEventResult objectEvent, out ChatChannelData updatedData) + { + try + { + var channelId = objectEvent.Channel; + var type = objectEvent.Type; + if (type == "channel" && channelId == channel.Id) + { + updatedData = objectEvent.ChannelMetadata; + return true; + } + else + { + updatedData = null; + return false; + } + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into Channel update. Exception was: {e.Message}"); + updatedData = null; + return false; + } + } + + internal static bool TryParseMessageUpdate(Chat chat, Message message, PNMessageActionEventResult actionEvent) + { + try + { + if (actionEvent.MessageTimetoken.ToString() == message.TimeToken && actionEvent.Uuid == message.UserId && actionEvent.Channel == message.ChannelId) + { + if (actionEvent.Event != "removed") + { + //already has it + if (message.MessageActions.Any(x => x.TimeToken == actionEvent.ActionTimetoken.ToString())) + { + return true; + } + message.MessageActions.Add(new MessageAction() + { + TimeToken = actionEvent.ActionTimetoken.ToString(), + Type = ChatEnumConverters.StringToActionType(actionEvent.Action.Type), + Value = actionEvent.Action.Value, + UserId = actionEvent.Uuid + }); + } + else + { + var dict = message.MessageActions.ToDictionary(x => x.TimeToken, y => y); + dict.Remove(actionEvent.ActionTimetoken.ToString()); + message.MessageActions = dict.Values.ToList(); + } + return true; + } + else + { + return false; + } + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNMessageActionEventResult into Message update. Exception was: {e.Message}"); + return false; + } + } + + internal static bool TryParseEvent(Chat chat, PNMessageResult messageResult, PubnubChatEventType eventType, out ChatEvent chatEvent) + { + try + { + var jsonDict = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(messageResult.Message + .ToString()); + if (!jsonDict.TryGetValue("type", out var typeString)) + { + chatEvent = default; + return false; + } + var receivedEventType = ChatEnumConverters.StringToEventType(typeString.ToString()); + if (receivedEventType != eventType) + { + chatEvent = default; + return false; + } + chatEvent = new ChatEvent() + { + TimeToken = messageResult.Timetoken.ToString(), + Type = eventType, + ChannelId = messageResult.Channel, + UserId = messageResult.Publisher, + Payload = messageResult.Message.ToString() + }; + return true; + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNMessageResult into ChatEvent of type \"{eventType}\". Exception was: {e.Message}"); + chatEvent = default; + return false; + } + } + + internal static bool TryParseEventFromHistory(Chat chat, string channelId, PNHistoryItemResult historyItem, out ChatEvent chatEvent) + { + try + { + var jsonDict = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(historyItem.Entry.ToString()); + if (!jsonDict.TryGetValue("type", out var typeString)) + { + chatEvent = default; + return false; + } + var receivedEventType = ChatEnumConverters.StringToEventType(typeString.ToString()); + chatEvent = new ChatEvent() + { + TimeToken = historyItem.Timetoken.ToString(), + Type = receivedEventType, + ChannelId = channelId, + UserId = historyItem.Uuid, + Payload = historyItem.Entry.ToString() + }; + return true; + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNHistoryItemResult into ChatEvent. Exception was: {e.Message}"); + chatEvent = default; + return false; + } + } + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs new file mode 100644 index 0000000..3ab1e40 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using PubnubApi; + +namespace PubnubChatApi +{ + internal static class ChatUtils + { + internal static string TimeTokenNow() + { + var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var timeStamp = Convert.ToInt64(timeSpan.TotalSeconds * 10000000); + return timeStamp.ToString(CultureInfo.InvariantCulture); + } + + internal static long TimeTokenNowLong() + { + var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var timeStamp = Convert.ToInt64(timeSpan.TotalSeconds * 10000000); + return timeStamp; + } + + internal static ChatOperationResult ToChatOperationResult(this PNResult result, string operationName, Chat chat) + { + var operationResult = new ChatOperationResult(operationName, chat); + operationResult.RegisterOperation(result); + return operationResult; + } + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/DiffMatchPatch.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/DiffMatchPatch.cs new file mode 100644 index 0000000..b158ff4 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/DiffMatchPatch.cs @@ -0,0 +1,2298 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Web; + +namespace DiffMatchPatch { + internal static class CompatibilityExtensions { + // JScript splice function + public static List Splice(this List input, int start, int count, + params T[] objects) { + List deletedRange = input.GetRange(start, count); + input.RemoveRange(start, count); + input.InsertRange(start, objects); + + return deletedRange; + } + + // Java substring function + public static string JavaSubstring(this string s, int begin, int end) { + return s.Substring(begin, end - begin); + } + } + + /**- + * The data structure representing a diff is a List of Diff objects: + * {Diff(Operation.DELETE, "Hello"), Diff(Operation.INSERT, "Goodbye"), + * Diff(Operation.EQUAL, " world.")} + * which means: delete "Hello", add "Goodbye" and keep " world." + */ + public enum Operation { + DELETE, INSERT, EQUAL + } + + + /** + * Class representing one diff operation. + */ + public class Diff { + public Operation operation; + // One of: INSERT, DELETE or EQUAL. + public string text; + // The text associated with this diff operation. + + /** + * Constructor. Initializes the diff with the provided values. + * @param operation One of INSERT, DELETE or EQUAL. + * @param text The text being applied. + */ + public Diff(Operation operation, string text) { + // Construct a diff with the specified operation and text. + this.operation = operation; + this.text = text; + } + + /** + * Display a human-readable version of this Diff. + * @return text version. + */ + public override string ToString() { + string prettyText = this.text.Replace('\n', '\u00b6'); + return "Diff(" + this.operation + ",\"" + prettyText + "\")"; + } + + /** + * Is this Diff equivalent to another Diff? + * @param d Another Diff to compare against. + * @return true or false. + */ + public override bool Equals(Object obj) { + // If parameter is null return false. + if (obj == null) { + return false; + } + + // If parameter cannot be cast to Diff return false. + Diff p = obj as Diff; + if ((System.Object)p == null) { + return false; + } + + // Return true if the fields match. + return p.operation == this.operation && p.text == this.text; + } + + public bool Equals(Diff obj) { + // If parameter is null return false. + if (obj == null) { + return false; + } + + // Return true if the fields match. + return obj.operation == this.operation && obj.text == this.text; + } + + public override int GetHashCode() { + return text.GetHashCode() ^ operation.GetHashCode(); + } + } + + + /** + * Class representing one patch operation. + */ + public class Patch { + public List diffs = new List(); + public int start1; + public int start2; + public int length1; + public int length2; + + /** + * Emulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indices are printed as 1-based, not 0-based. + * @return The GNU diff string. + */ + public override string ToString() { + string coords1, coords2; + if (this.length1 == 0) { + coords1 = this.start1 + ",0"; + } else if (this.length1 == 1) { + coords1 = Convert.ToString(this.start1 + 1); + } else { + coords1 = (this.start1 + 1) + "," + this.length1; + } + if (this.length2 == 0) { + coords2 = this.start2 + ",0"; + } else if (this.length2 == 1) { + coords2 = Convert.ToString(this.start2 + 1); + } else { + coords2 = (this.start2 + 1) + "," + this.length2; + } + StringBuilder text = new StringBuilder(); + text.Append("@@ -").Append(coords1).Append(" +").Append(coords2) + .Append(" @@\n"); + // Escape the body of the patch with %xx notation. + foreach (Diff aDiff in this.diffs) { + switch (aDiff.operation) { + case Operation.INSERT: + text.Append('+'); + break; + case Operation.DELETE: + text.Append('-'); + break; + case Operation.EQUAL: + text.Append(' '); + break; + } + + text.Append(diff_match_patch.encodeURI(aDiff.text)).Append("\n"); + } + return text.ToString(); + } + } + + + /** + * Class containing the diff, match and patch methods. + * Also Contains the behaviour settings. + */ + public class diff_match_patch { + // Defaults. + // Set these on your diff_match_patch instance to override the defaults. + + // Number of seconds to map a diff before giving up (0 for infinity). + public float Diff_Timeout = 1.0f; + // Cost of an empty edit operation in terms of edit characters. + public short Diff_EditCost = 4; + // At what point is no match declared (0.0 = perfection, 1.0 = very loose). + public float Match_Threshold = 0.5f; + // How far to search for a match (0 = exact location, 1000+ = broad match). + // A match this many characters away from the expected location will add + // 1.0 to the score (0.0 is a perfect match). + public int Match_Distance = 1000; + // When deleting a large block of text (over ~64 characters), how close + // do the contents have to be to match the expected contents. (0.0 = + // perfection, 1.0 = very loose). Note that Match_Threshold controls + // how closely the end points of a delete need to match. + public float Patch_DeleteThreshold = 0.5f; + // Chunk size for context length. + public short Patch_Margin = 4; + + // The number of bits in an int. + private short Match_MaxBits = 32; + + + // DIFF FUNCTIONS + + + /** + * Find the differences between two texts. + * Run a faster, slightly less optimal diff. + * This method allows the 'checklines' of diff_main() to be optional. + * Most of the time checklines is wanted, so default to true. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @return List of Diff objects. + */ + public List diff_main(string text1, string text2) { + return diff_main(text1, text2, true); + } + + /** + * Find the differences between two texts. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @return List of Diff objects. + */ + public List diff_main(string text1, string text2, bool checklines) { + // Set a deadline by which time the diff must be complete. + DateTime deadline; + if (this.Diff_Timeout <= 0) { + deadline = DateTime.MaxValue; + } else { + deadline = DateTime.Now + + new TimeSpan(((long)(Diff_Timeout * 1000)) * 10000); + } + return diff_main(text1, text2, checklines, deadline); + } + + /** + * Find the differences between two texts. Simplifies the problem by + * stripping any common prefix or suffix off the texts before diffing. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. Used + * internally for recursive calls. Users should set DiffTimeout + * instead. + * @return List of Diff objects. + */ + private List diff_main(string text1, string text2, bool checklines, + DateTime deadline) { + // Check for null inputs not needed since null can't be passed in C#. + + // Check for equality (speedup). + List diffs; + if (text1 == text2) { + diffs = new List(); + if (text1.Length != 0) { + diffs.Add(new Diff(Operation.EQUAL, text1)); + } + return diffs; + } + + // Trim off common prefix (speedup). + int commonlength = diff_commonPrefix(text1, text2); + string commonprefix = text1.Substring(0, commonlength); + text1 = text1.Substring(commonlength); + text2 = text2.Substring(commonlength); + + // Trim off common suffix (speedup). + commonlength = diff_commonSuffix(text1, text2); + string commonsuffix = text1.Substring(text1.Length - commonlength); + text1 = text1.Substring(0, text1.Length - commonlength); + text2 = text2.Substring(0, text2.Length - commonlength); + + // Compute the diff on the middle block. + diffs = diff_compute(text1, text2, checklines, deadline); + + // Restore the prefix and suffix. + if (commonprefix.Length != 0) { + diffs.Insert(0, (new Diff(Operation.EQUAL, commonprefix))); + } + if (commonsuffix.Length != 0) { + diffs.Add(new Diff(Operation.EQUAL, commonsuffix)); + } + + diff_cleanupMerge(diffs); + return diffs; + } + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. + * @return List of Diff objects. + */ + private List diff_compute(string text1, string text2, + bool checklines, DateTime deadline) { + List diffs = new List(); + + if (text1.Length == 0) { + // Just add some text (speedup). + diffs.Add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + if (text2.Length == 0) { + // Just delete some text (speedup). + diffs.Add(new Diff(Operation.DELETE, text1)); + return diffs; + } + + string longtext = text1.Length > text2.Length ? text1 : text2; + string shorttext = text1.Length > text2.Length ? text2 : text1; + int i = longtext.IndexOf(shorttext, StringComparison.Ordinal); + if (i != -1) { + // Shorter text is inside the longer text (speedup). + Operation op = (text1.Length > text2.Length) ? + Operation.DELETE : Operation.INSERT; + diffs.Add(new Diff(op, longtext.Substring(0, i))); + diffs.Add(new Diff(Operation.EQUAL, shorttext)); + diffs.Add(new Diff(op, longtext.Substring(i + shorttext.Length))); + return diffs; + } + + if (shorttext.Length == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + diffs.Add(new Diff(Operation.DELETE, text1)); + diffs.Add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + // Check to see if the problem can be split in two. + string[] hm = diff_halfMatch(text1, text2); + if (hm != null) { + // A half-match was found, sort out the return data. + string text1_a = hm[0]; + string text1_b = hm[1]; + string text2_a = hm[2]; + string text2_b = hm[3]; + string mid_common = hm[4]; + // Send both pairs off for separate processing. + List diffs_a = diff_main(text1_a, text2_a, checklines, deadline); + List diffs_b = diff_main(text1_b, text2_b, checklines, deadline); + // Merge the results. + diffs = diffs_a; + diffs.Add(new Diff(Operation.EQUAL, mid_common)); + diffs.AddRange(diffs_b); + return diffs; + } + + if (checklines && text1.Length > 100 && text2.Length > 100) { + return diff_lineMode(text1, text2, deadline); + } + + return diff_bisect(text1, text2, deadline); + } + + /** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time when the diff should be complete by. + * @return List of Diff objects. + */ + private List diff_lineMode(string text1, string text2, + DateTime deadline) { + // Scan the text on a line-by-line basis first. + Object[] a = diff_linesToChars(text1, text2); + text1 = (string)a[0]; + text2 = (string)a[1]; + List linearray = (List)a[2]; + + List diffs = diff_main(text1, text2, false, deadline); + + // Convert the diff back to original text. + diff_charsToLines(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + diff_cleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.Add(new Diff(Operation.EQUAL, string.Empty)); + int pointer = 0; + int count_delete = 0; + int count_insert = 0; + string text_delete = string.Empty; + string text_insert = string.Empty; + while (pointer < diffs.Count) { + switch (diffs[pointer].operation) { + case Operation.INSERT: + count_insert++; + text_insert += diffs[pointer].text; + break; + case Operation.DELETE: + count_delete++; + text_delete += diffs[pointer].text; + break; + case Operation.EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + diffs.RemoveRange(pointer - count_delete - count_insert, + count_delete + count_insert); + pointer = pointer - count_delete - count_insert; + List subDiff = + this.diff_main(text_delete, text_insert, false, deadline); + diffs.InsertRange(pointer, subDiff); + pointer = pointer + subDiff.Count; + } + count_insert = 0; + count_delete = 0; + text_delete = string.Empty; + text_insert = string.Empty; + break; + default: + throw new ArgumentOutOfRangeException(); + } + pointer++; + } + diffs.RemoveAt(diffs.Count - 1); // Remove the dummy entry at the end. + + return diffs; + } + + /** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time at which to bail if not yet complete. + * @return List of Diff objects. + */ + protected List diff_bisect(string text1, string text2, + DateTime deadline) { + // Cache the text lengths to prevent multiple calls. + int text1_length = text1.Length; + int text2_length = text2.Length; + int max_d = (text1_length + text2_length + 1) / 2; + int v_offset = max_d; + int v_length = 2 * max_d; + int[] v1 = new int[v_length]; + int[] v2 = new int[v_length]; + for (int x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + int delta = text1_length - text2_length; + // If the total number of characters is odd, then the front path will + // collide with the reverse path. + bool front = (delta % 2 != 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + int k1start = 0; + int k1end = 0; + int k2start = 0; + int k2end = 0; + for (int d = 0; d < max_d; d++) { + // Bail out if deadline is reached. + if (DateTime.Now > deadline) { + break; + } + + // Walk the front path one step. + for (int k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + int k1_offset = v_offset + k1; + int x1; + if (k1 == -d || k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1]) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + int y1 = x1 - k1; + while (x1 < text1_length && y1 < text2_length + && text1[x1] == text2[y1]) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + int k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + int x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + + // Walk the reverse path one step. + for (int k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + int k2_offset = v_offset + k2; + int x2; + if (k2 == -d || k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1]) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + int y2 = x2 - k2; + while (x2 < text1_length && y2 < text2_length + && text1[text1_length - x2 - 1] + == text2[text2_length - y2 - 1]) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + int k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + int x1 = v1[k1_offset]; + int y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + List diffs = new List(); + diffs.Add(new Diff(Operation.DELETE, text1)); + diffs.Add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + /** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param x Index of split point in text1. + * @param y Index of split point in text2. + * @param deadline Time at which to bail if not yet complete. + * @return LinkedList of Diff objects. + */ + private List diff_bisectSplit(string text1, string text2, + int x, int y, DateTime deadline) { + string text1a = text1.Substring(0, x); + string text2a = text2.Substring(0, y); + string text1b = text1.Substring(x); + string text2b = text2.Substring(y); + + // Compute both diffs serially. + List diffs = diff_main(text1a, text2a, false, deadline); + List diffsb = diff_main(text1b, text2b, false, deadline); + + diffs.AddRange(diffsb); + return diffs; + } + + /** + * Split two texts into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text1 First string. + * @param text2 Second string. + * @return Three element Object array, containing the encoded text1, the + * encoded text2 and the List of unique strings. The zeroth element + * of the List of unique strings is intentionally blank. + */ + protected Object[] diff_linesToChars(string text1, string text2) { + List lineArray = new List(); + Dictionary lineHash = new Dictionary(); + // e.g. linearray[4] == "Hello\n" + // e.g. linehash.get("Hello\n") == 4 + + // "\x00" is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray.Add(string.Empty); + + // Allocate 2/3rds of the space for text1, the rest for text2. + string chars1 = diff_linesToCharsMunge(text1, lineArray, lineHash, 40000); + string chars2 = diff_linesToCharsMunge(text2, lineArray, lineHash, 65535); + return new Object[] { chars1, chars2, lineArray }; + } + + /** + * Split a text into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text String to encode. + * @param lineArray List of unique strings. + * @param lineHash Map of strings to indices. + * @param maxLines Maximum length of lineArray. + * @return Encoded string. + */ + private string diff_linesToCharsMunge(string text, List lineArray, + Dictionary lineHash, int maxLines) { + int lineStart = 0; + int lineEnd = -1; + string line; + StringBuilder chars = new StringBuilder(); + // Walk the text, pulling out a Substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + while (lineEnd < text.Length - 1) { + lineEnd = text.IndexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = text.Length - 1; + } + line = text.JavaSubstring(lineStart, lineEnd + 1); + + if (lineHash.ContainsKey(line)) { + chars.Append(((char)(int)lineHash[line])); + } else { + if (lineArray.Count == maxLines) { + // Bail out at 65535 because char 65536 == char 0. + line = text.Substring(lineStart); + lineEnd = text.Length; + } + lineArray.Add(line); + lineHash.Add(line, lineArray.Count - 1); + chars.Append(((char)(lineArray.Count - 1))); + } + lineStart = lineEnd + 1; + } + return chars.ToString(); + } + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines + * of text. + * @param diffs List of Diff objects. + * @param lineArray List of unique strings. + */ + protected void diff_charsToLines(ICollection diffs, + IList lineArray) { + StringBuilder text; + foreach (Diff diff in diffs) { + text = new StringBuilder(); + for (int j = 0; j < diff.text.Length; j++) { + text.Append(lineArray[diff.text[j]]); + } + diff.text = text.ToString(); + } + } + + /** + * Determine the common prefix of two strings. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the start of each string. + */ + public int diff_commonPrefix(string text1, string text2) { + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + int n = Math.Min(text1.Length, text2.Length); + for (int i = 0; i < n; i++) { + if (text1[i] != text2[i]) { + return i; + } + } + return n; + } + + /** + * Determine the common suffix of two strings. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of each string. + */ + public int diff_commonSuffix(string text1, string text2) { + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + int text1_length = text1.Length; + int text2_length = text2.Length; + int n = Math.Min(text1.Length, text2.Length); + for (int i = 1; i <= n; i++) { + if (text1[text1_length - i] != text2[text2_length - i]) { + return i - 1; + } + } + return n; + } + + /** + * Determine if the suffix of one string is the prefix of another. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of the first + * string and the start of the second string. + */ + protected int diff_commonOverlap(string text1, string text2) { + // Cache the text lengths to prevent multiple calls. + int text1_length = text1.Length; + int text2_length = text2.Length; + // Eliminate the null case. + if (text1_length == 0 || text2_length == 0) { + return 0; + } + // Truncate the longer string. + if (text1_length > text2_length) { + text1 = text1.Substring(text1_length - text2_length); + } else if (text1_length < text2_length) { + text2 = text2.Substring(0, text1_length); + } + int text_length = Math.Min(text1_length, text2_length); + // Quick check for the worst case. + if (text1 == text2) { + return text_length; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: https://neil.fraser.name/news/2010/11/04/ + int best = 0; + int length = 1; + while (true) { + string pattern = text1.Substring(text_length - length); + int found = text2.IndexOf(pattern, StringComparison.Ordinal); + if (found == -1) { + return best; + } + length += found; + if (found == 0 || text1.Substring(text_length - length) == + text2.Substring(0, length)) { + best = length; + length++; + } + } + } + + /** + * Do the two texts share a Substring which is at least half the length of + * the longer text? + * This speedup can produce non-minimal diffs. + * @param text1 First string. + * @param text2 Second string. + * @return Five element String array, containing the prefix of text1, the + * suffix of text1, the prefix of text2, the suffix of text2 and the + * common middle. Or null if there was no match. + */ + + protected string[] diff_halfMatch(string text1, string text2) { + if (this.Diff_Timeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return null; + } + string longtext = text1.Length > text2.Length ? text1 : text2; + string shorttext = text1.Length > text2.Length ? text2 : text1; + if (longtext.Length < 4 || shorttext.Length * 2 < longtext.Length) { + return null; // Pointless. + } + + // First check if the second quarter is the seed for a half-match. + string[] hm1 = diff_halfMatchI(longtext, shorttext, + (longtext.Length + 3) / 4); + // Check again based on the third quarter. + string[] hm2 = diff_halfMatchI(longtext, shorttext, + (longtext.Length + 1) / 2); + string[] hm; + if (hm1 == null && hm2 == null) { + return null; + } else if (hm2 == null) { + hm = hm1; + } else if (hm1 == null) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].Length > hm2[4].Length ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + if (text1.Length > text2.Length) { + return hm; + //return new string[]{hm[0], hm[1], hm[2], hm[3], hm[4]}; + } else { + return new string[] { hm[2], hm[3], hm[0], hm[1], hm[4] }; + } + } + + /** + * Does a Substring of shorttext exist within longtext such that the + * Substring is at least half the length of longtext? + * @param longtext Longer string. + * @param shorttext Shorter string. + * @param i Start index of quarter length Substring within longtext. + * @return Five element string array, containing the prefix of longtext, the + * suffix of longtext, the prefix of shorttext, the suffix of shorttext + * and the common middle. Or null if there was no match. + */ + private string[] diff_halfMatchI(string longtext, string shorttext, int i) { + // Start with a 1/4 length Substring at position i as a seed. + string seed = longtext.Substring(i, longtext.Length / 4); + int j = -1; + string best_common = string.Empty; + string best_longtext_a = string.Empty, best_longtext_b = string.Empty; + string best_shorttext_a = string.Empty, best_shorttext_b = string.Empty; + while (j < shorttext.Length && (j = shorttext.IndexOf(seed, j + 1, + StringComparison.Ordinal)) != -1) { + int prefixLength = diff_commonPrefix(longtext.Substring(i), + shorttext.Substring(j)); + int suffixLength = diff_commonSuffix(longtext.Substring(0, i), + shorttext.Substring(0, j)); + if (best_common.Length < suffixLength + prefixLength) { + best_common = shorttext.Substring(j - suffixLength, suffixLength) + + shorttext.Substring(j, prefixLength); + best_longtext_a = longtext.Substring(0, i - suffixLength); + best_longtext_b = longtext.Substring(i + prefixLength); + best_shorttext_a = shorttext.Substring(0, j - suffixLength); + best_shorttext_b = shorttext.Substring(j + prefixLength); + } + } + if (best_common.Length * 2 >= longtext.Length) { + return new string[]{best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common}; + } else { + return null; + } + } + + /** + * Reduce the number of edits by eliminating semantically trivial + * equalities. + * @param diffs List of Diff objects. + */ + public void diff_cleanupSemantic(List diffs) { + bool changes = false; + // Stack of indices where equalities are found. + Stack equalities = new Stack(); + // Always equal to equalities[equalitiesLength-1][1] + string lastEquality = null; + int pointer = 0; // Index of current position. + // Number of characters that changed prior to the equality. + int length_insertions1 = 0; + int length_deletions1 = 0; + // Number of characters that changed after the equality. + int length_insertions2 = 0; + int length_deletions2 = 0; + while (pointer < diffs.Count) { + if (diffs[pointer].operation == Operation.EQUAL) { // Equality found. + equalities.Push(pointer); + length_insertions1 = length_insertions2; + length_deletions1 = length_deletions2; + length_insertions2 = 0; + length_deletions2 = 0; + lastEquality = diffs[pointer].text; + } else { // an insertion or deletion + if (diffs[pointer].operation == Operation.INSERT) { + length_insertions2 += diffs[pointer].text.Length; + } else { + length_deletions2 += diffs[pointer].text.Length; + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastEquality != null && (lastEquality.Length + <= Math.Max(length_insertions1, length_deletions1)) + && (lastEquality.Length + <= Math.Max(length_insertions2, length_deletions2))) { + // Duplicate record. + diffs.Insert(equalities.Peek(), + new Diff(Operation.DELETE, lastEquality)); + // Change second copy to insert. + diffs[equalities.Peek() + 1].operation = Operation.INSERT; + // Throw away the equality we just deleted. + equalities.Pop(); + if (equalities.Count > 0) { + equalities.Pop(); + } + pointer = equalities.Count > 0 ? equalities.Peek() : -1; + length_insertions1 = 0; // Reset the counters. + length_deletions1 = 0; + length_insertions2 = 0; + length_deletions2 = 0; + lastEquality = null; + changes = true; + } + } + pointer++; + } + + // Normalize the diff. + if (changes) { + diff_cleanupMerge(diffs); + } + diff_cleanupSemanticLossless(diffs); + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1; + while (pointer < diffs.Count) { + if (diffs[pointer - 1].operation == Operation.DELETE && + diffs[pointer].operation == Operation.INSERT) { + string deletion = diffs[pointer - 1].text; + string insertion = diffs[pointer].text; + int overlap_length1 = diff_commonOverlap(deletion, insertion); + int overlap_length2 = diff_commonOverlap(insertion, deletion); + if (overlap_length1 >= overlap_length2) { + if (overlap_length1 >= deletion.Length / 2.0 || + overlap_length1 >= insertion.Length / 2.0) { + // Overlap found. + // Insert an equality and trim the surrounding edits. + diffs.Insert(pointer, new Diff(Operation.EQUAL, + insertion.Substring(0, overlap_length1))); + diffs[pointer - 1].text = + deletion.Substring(0, deletion.Length - overlap_length1); + diffs[pointer + 1].text = insertion.Substring(overlap_length1); + pointer++; + } + } else { + if (overlap_length2 >= deletion.Length / 2.0 || + overlap_length2 >= insertion.Length / 2.0) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + diffs.Insert(pointer, new Diff(Operation.EQUAL, + deletion.Substring(0, overlap_length2))); + diffs[pointer - 1].operation = Operation.INSERT; + diffs[pointer - 1].text = + insertion.Substring(0, insertion.Length - overlap_length2); + diffs[pointer + 1].operation = Operation.DELETE; + diffs[pointer + 1].text = deletion.Substring(overlap_length2); + pointer++; + } + } + pointer++; + } + pointer++; + } + } + + /** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * @param diffs List of Diff objects. + */ + public void diff_cleanupSemanticLossless(List diffs) { + int pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.Count - 1) { + if (diffs[pointer - 1].operation == Operation.EQUAL && + diffs[pointer + 1].operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + string equality1 = diffs[pointer - 1].text; + string edit = diffs[pointer].text; + string equality2 = diffs[pointer + 1].text; + + // First, shift the edit as far left as possible. + int commonOffset = this.diff_commonSuffix(equality1, edit); + if (commonOffset > 0) { + string commonString = edit.Substring(edit.Length - commonOffset); + equality1 = equality1.Substring(0, equality1.Length - commonOffset); + edit = commonString + edit.Substring(0, edit.Length - commonOffset); + equality2 = commonString + equality2; + } + + // Second, step character by character right, + // looking for the best fit. + string bestEquality1 = equality1; + string bestEdit = edit; + string bestEquality2 = equality2; + int bestScore = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + while (edit.Length != 0 && equality2.Length != 0 + && edit[0] == equality2[0]) { + equality1 += edit[0]; + edit = edit.Substring(1) + equality2[0]; + equality2 = equality2.Substring(1); + int score = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + // The >= encourages trailing rather than leading whitespace on + // edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (diffs[pointer - 1].text != bestEquality1) { + // We have an improvement, save it back to the diff. + if (bestEquality1.Length != 0) { + diffs[pointer - 1].text = bestEquality1; + } else { + diffs.RemoveAt(pointer - 1); + pointer--; + } + diffs[pointer].text = bestEdit; + if (bestEquality2.Length != 0) { + diffs[pointer + 1].text = bestEquality2; + } else { + diffs.RemoveAt(pointer + 1); + pointer--; + } + } + } + pointer++; + } + } + + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 6 (best) to 0 (worst). + * @param one First string. + * @param two Second string. + * @return The score. + */ + private int diff_cleanupSemanticScore(string one, string two) { + if (one.Length == 0 || two.Length == 0) { + // Edges are the best. + return 6; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + char char1 = one[one.Length - 1]; + char char2 = two[0]; + bool nonAlphaNumeric1 = !Char.IsLetterOrDigit(char1); + bool nonAlphaNumeric2 = !Char.IsLetterOrDigit(char2); + bool whitespace1 = nonAlphaNumeric1 && Char.IsWhiteSpace(char1); + bool whitespace2 = nonAlphaNumeric2 && Char.IsWhiteSpace(char2); + bool lineBreak1 = whitespace1 && Char.IsControl(char1); + bool lineBreak2 = whitespace2 && Char.IsControl(char2); + bool blankLine1 = lineBreak1 && BLANKLINEEND.IsMatch(one); + bool blankLine2 = lineBreak2 && BLANKLINESTART.IsMatch(two); + + if (blankLine1 || blankLine2) { + // Five points for blank lines. + return 5; + } else if (lineBreak1 || lineBreak2) { + // Four points for line breaks. + return 4; + } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { + // Three points for end of sentences. + return 3; + } else if (whitespace1 || whitespace2) { + // Two points for whitespace. + return 2; + } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { + // One point for non-alphanumeric. + return 1; + } + return 0; + } + + // Define some regex patterns for matching boundaries. + private Regex BLANKLINEEND = new Regex("\\n\\r?\\n\\Z"); + private Regex BLANKLINESTART = new Regex("\\A\\r?\\n\\r?\\n"); + + /** + * Reduce the number of edits by eliminating operationally trivial + * equalities. + * @param diffs List of Diff objects. + */ + public void diff_cleanupEfficiency(List diffs) { + bool changes = false; + // Stack of indices where equalities are found. + Stack equalities = new Stack(); + // Always equal to equalities[equalitiesLength-1][1] + string lastEquality = string.Empty; + int pointer = 0; // Index of current position. + // Is there an insertion operation before the last equality. + bool pre_ins = false; + // Is there a deletion operation before the last equality. + bool pre_del = false; + // Is there an insertion operation after the last equality. + bool post_ins = false; + // Is there a deletion operation after the last equality. + bool post_del = false; + while (pointer < diffs.Count) { + if (diffs[pointer].operation == Operation.EQUAL) { // Equality found. + if (diffs[pointer].text.Length < this.Diff_EditCost + && (post_ins || post_del)) { + // Candidate found. + equalities.Push(pointer); + pre_ins = post_ins; + pre_del = post_del; + lastEquality = diffs[pointer].text; + } else { + // Not a candidate, and can never become one. + equalities.Clear(); + lastEquality = string.Empty; + } + post_ins = post_del = false; + } else { // An insertion or deletion. + if (diffs[pointer].operation == Operation.DELETE) { + post_del = true; + } else { + post_ins = true; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if ((lastEquality.Length != 0) + && ((pre_ins && pre_del && post_ins && post_del) + || ((lastEquality.Length < this.Diff_EditCost / 2) + && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0) + (post_ins ? 1 : 0) + + (post_del ? 1 : 0)) == 3))) { + // Duplicate record. + diffs.Insert(equalities.Peek(), + new Diff(Operation.DELETE, lastEquality)); + // Change second copy to insert. + diffs[equalities.Peek() + 1].operation = Operation.INSERT; + equalities.Pop(); // Throw away the equality we just deleted. + lastEquality = string.Empty; + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = true; + equalities.Clear(); + } else { + if (equalities.Count > 0) { + equalities.Pop(); + } + + pointer = equalities.Count > 0 ? equalities.Peek() : -1; + post_ins = post_del = false; + } + changes = true; + } + } + pointer++; + } + + if (changes) { + diff_cleanupMerge(diffs); + } + } + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param diffs List of Diff objects. + */ + public void diff_cleanupMerge(List diffs) { + // Add a dummy entry at the end. + diffs.Add(new Diff(Operation.EQUAL, string.Empty)); + int pointer = 0; + int count_delete = 0; + int count_insert = 0; + string text_delete = string.Empty; + string text_insert = string.Empty; + int commonlength; + while (pointer < diffs.Count) { + switch (diffs[pointer].operation) { + case Operation.INSERT: + count_insert++; + text_insert += diffs[pointer].text; + pointer++; + break; + case Operation.DELETE: + count_delete++; + text_delete += diffs[pointer].text; + pointer++; + break; + case Operation.EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete + count_insert > 1) { + if (count_delete != 0 && count_insert != 0) { + // Factor out any common prefixies. + commonlength = this.diff_commonPrefix(text_insert, text_delete); + if (commonlength != 0) { + if ((pointer - count_delete - count_insert) > 0 && + diffs[pointer - count_delete - count_insert - 1].operation + == Operation.EQUAL) { + diffs[pointer - count_delete - count_insert - 1].text + += text_insert.Substring(0, commonlength); + } else { + diffs.Insert(0, new Diff(Operation.EQUAL, + text_insert.Substring(0, commonlength))); + pointer++; + } + text_insert = text_insert.Substring(commonlength); + text_delete = text_delete.Substring(commonlength); + } + // Factor out any common suffixies. + commonlength = this.diff_commonSuffix(text_insert, text_delete); + if (commonlength != 0) { + diffs[pointer].text = text_insert.Substring(text_insert.Length + - commonlength) + diffs[pointer].text; + text_insert = text_insert.Substring(0, text_insert.Length + - commonlength); + text_delete = text_delete.Substring(0, text_delete.Length + - commonlength); + } + } + // Delete the offending records and add the merged ones. + pointer -= count_delete + count_insert; + diffs.Splice(pointer, count_delete + count_insert); + if (text_delete.Length != 0) { + diffs.Splice(pointer, 0, + new Diff(Operation.DELETE, text_delete)); + pointer++; + } + if (text_insert.Length != 0) { + diffs.Splice(pointer, 0, + new Diff(Operation.INSERT, text_insert)); + pointer++; + } + pointer++; + } else if (pointer != 0 + && diffs[pointer - 1].operation == Operation.EQUAL) { + // Merge this equality with the previous one. + diffs[pointer - 1].text += diffs[pointer].text; + diffs.RemoveAt(pointer); + } else { + pointer++; + } + count_insert = 0; + count_delete = 0; + text_delete = string.Empty; + text_insert = string.Empty; + break; + } + } + if (diffs[diffs.Count - 1].text.Length == 0) { + diffs.RemoveAt(diffs.Count - 1); // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by + // equalities which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + bool changes = false; + pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < (diffs.Count - 1)) { + if (diffs[pointer - 1].operation == Operation.EQUAL && + diffs[pointer + 1].operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + if (diffs[pointer].text.EndsWith(diffs[pointer - 1].text, + StringComparison.Ordinal)) { + // Shift the edit over the previous equality. + diffs[pointer].text = diffs[pointer - 1].text + + diffs[pointer].text.Substring(0, diffs[pointer].text.Length - + diffs[pointer - 1].text.Length); + diffs[pointer + 1].text = diffs[pointer - 1].text + + diffs[pointer + 1].text; + diffs.Splice(pointer - 1, 1); + changes = true; + } else if (diffs[pointer].text.StartsWith(diffs[pointer + 1].text, + StringComparison.Ordinal)) { + // Shift the edit over the next equality. + diffs[pointer - 1].text += diffs[pointer + 1].text; + diffs[pointer].text = + diffs[pointer].text.Substring(diffs[pointer + 1].text.Length) + + diffs[pointer + 1].text; + diffs.Splice(pointer + 1, 1); + changes = true; + } + } + pointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + this.diff_cleanupMerge(diffs); + } + } + + /** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. "The cat" vs "The big cat", 1->1, 5->8 + * @param diffs List of Diff objects. + * @param loc Location within text1. + * @return Location within text2. + */ + public int diff_xIndex(List diffs, int loc) { + int chars1 = 0; + int chars2 = 0; + int last_chars1 = 0; + int last_chars2 = 0; + Diff lastDiff = null; + foreach (Diff aDiff in diffs) { + if (aDiff.operation != Operation.INSERT) { + // Equality or deletion. + chars1 += aDiff.text.Length; + } + if (aDiff.operation != Operation.DELETE) { + // Equality or insertion. + chars2 += aDiff.text.Length; + } + if (chars1 > loc) { + // Overshot the location. + lastDiff = aDiff; + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + if (lastDiff != null && lastDiff.operation == Operation.DELETE) { + // The location was deleted. + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); + } + + /** + * Convert a Diff list into a pretty HTML report. + * @param diffs List of Diff objects. + * @return HTML representation. + */ + public string diff_prettyHtml(List diffs) { + StringBuilder html = new StringBuilder(); + foreach (Diff aDiff in diffs) { + string text = aDiff.text.Replace("&", "&").Replace("<", "<") + .Replace(">", ">").Replace("\n", "¶
"); + switch (aDiff.operation) { + case Operation.INSERT: + html.Append("").Append(text) + .Append(""); + break; + case Operation.DELETE: + html.Append("").Append(text) + .Append(""); + break; + case Operation.EQUAL: + html.Append("").Append(text).Append(""); + break; + } + } + return html.ToString(); + } + + /** + * Compute and return the source text (all equalities and deletions). + * @param diffs List of Diff objects. + * @return Source text. + */ + public string diff_text1(List diffs) { + StringBuilder text = new StringBuilder(); + foreach (Diff aDiff in diffs) { + if (aDiff.operation != Operation.INSERT) { + text.Append(aDiff.text); + } + } + return text.ToString(); + } + + /** + * Compute and return the destination text (all equalities and insertions). + * @param diffs List of Diff objects. + * @return Destination text. + */ + public string diff_text2(List diffs) { + StringBuilder text = new StringBuilder(); + foreach (Diff aDiff in diffs) { + if (aDiff.operation != Operation.DELETE) { + text.Append(aDiff.text); + } + } + return text.ToString(); + } + + /** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param diffs List of Diff objects. + * @return Number of changes. + */ + public int diff_levenshtein(List diffs) { + int levenshtein = 0; + int insertions = 0; + int deletions = 0; + foreach (Diff aDiff in diffs) { + switch (aDiff.operation) { + case Operation.INSERT: + insertions += aDiff.text.Length; + break; + case Operation.DELETE: + deletions += aDiff.text.Length; + break; + case Operation.EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += Math.Max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += Math.Max(insertions, deletions); + return levenshtein; + } + + /** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx + * notation. + * @param diffs Array of Diff objects. + * @return Delta text. + */ + public string diff_toDelta(List diffs) { + StringBuilder text = new StringBuilder(); + foreach (Diff aDiff in diffs) { + switch (aDiff.operation) { + case Operation.INSERT: + text.Append("+").Append(encodeURI(aDiff.text)).Append("\t"); + break; + case Operation.DELETE: + text.Append("-").Append(aDiff.text.Length).Append("\t"); + break; + case Operation.EQUAL: + text.Append("=").Append(aDiff.text.Length).Append("\t"); + break; + } + } + string delta = text.ToString(); + if (delta.Length != 0) { + // Strip off trailing tab character. + delta = delta.Substring(0, delta.Length - 1); + } + return delta; + } + + /** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * @param text1 Source string for the diff. + * @param delta Delta text. + * @return Array of Diff objects or null if invalid. + * @throws ArgumentException If invalid input. + */ + public List diff_fromDelta(string text1, string delta) { + List diffs = new List(); + int pointer = 0; // Cursor in text1 + string[] tokens = delta.Split(new string[] { "\t" }, + StringSplitOptions.None); + foreach (string token in tokens) { + if (token.Length == 0) { + // Blank tokens are ok (from a trailing \t). + continue; + } + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + string param = token.Substring(1); + switch (token[0]) { + case '+': + // decode would change all "+" to " " + param = param.Replace("+", "%2b"); + + param = HttpUtility.UrlDecode(param); + //} catch (UnsupportedEncodingException e) { + // // Not likely on modern system. + // throw new Error("This system does not support UTF-8.", e); + //} catch (IllegalArgumentException e) { + // // Malformed URI sequence. + // throw new IllegalArgumentException( + // "Illegal escape in diff_fromDelta: " + param, e); + //} + diffs.Add(new Diff(Operation.INSERT, param)); + break; + case '-': + // Fall through. + case '=': + int n; + try { + n = Convert.ToInt32(param); + } catch (FormatException e) { + throw new ArgumentException( + "Invalid number in diff_fromDelta: " + param, e); + } + if (n < 0) { + throw new ArgumentException( + "Negative number in diff_fromDelta: " + param); + } + string text; + try { + text = text1.Substring(pointer, n); + pointer += n; + } catch (ArgumentOutOfRangeException e) { + throw new ArgumentException("Delta length (" + pointer + + ") larger than source text length (" + text1.Length + + ").", e); + } + if (token[0] == '=') { + diffs.Add(new Diff(Operation.EQUAL, text)); + } else { + diffs.Add(new Diff(Operation.DELETE, text)); + } + break; + default: + // Anything else is an error. + throw new ArgumentException( + "Invalid diff operation in diff_fromDelta: " + token[0]); + } + } + if (pointer != text1.Length) { + throw new ArgumentException("Delta length (" + pointer + + ") smaller than source text length (" + text1.Length + ")."); + } + return diffs; + } + + + // MATCH FUNCTIONS + + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + public int match_main(string text, string pattern, int loc) { + // Check for null inputs not needed since null can't be passed in C#. + + loc = Math.Max(0, Math.Min(loc, text.Length)); + if (text == pattern) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (text.Length == 0) { + // Nothing to match. + return -1; + } else if (loc + pattern.Length <= text.Length + && text.Substring(loc, pattern.Length) == pattern) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return match_bitap(text, pattern, loc); + } + } + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + protected int match_bitap(string text, string pattern, int loc) { + // assert (Match_MaxBits == 0 || pattern.Length <= Match_MaxBits) + // : "Pattern too long for this application."; + + // Initialise the alphabet. + Dictionary s = match_alphabet(pattern); + + // Highest score beyond which we give up. + double score_threshold = Match_Threshold; + // Is there a nearby exact match? (speedup) + int best_loc = text.IndexOf(pattern, loc, StringComparison.Ordinal); + if (best_loc != -1) { + score_threshold = Math.Min(match_bitapScore(0, best_loc, loc, + pattern), score_threshold); + // What about in the other direction? (speedup) + best_loc = text.LastIndexOf(pattern, + Math.Min(loc + pattern.Length, text.Length), + StringComparison.Ordinal); + if (best_loc != -1) { + score_threshold = Math.Min(match_bitapScore(0, best_loc, loc, + pattern), score_threshold); + } + } + + // Initialise the bit arrays. + int matchmask = 1 << (pattern.Length - 1); + best_loc = -1; + + int bin_min, bin_mid; + int bin_max = pattern.Length + text.Length; + // Empty initialization added to appease C# compiler. + int[] last_rd = new int[0]; + for (int d = 0; d < pattern.Length; d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at + // this error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (match_bitapScore(d, loc + bin_mid, loc, pattern) + <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = (bin_max - bin_min) / 2 + bin_min; + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + int start = Math.Max(1, loc - bin_mid + 1); + int finish = Math.Min(loc + bin_mid, text.Length) + pattern.Length; + + int[] rd = new int[finish + 2]; + rd[finish + 1] = (1 << d) - 1; + for (int j = finish; j >= start; j--) { + int charMatch; + if (text.Length <= j - 1 || !s.ContainsKey(text[j - 1])) { + // Out of range. + charMatch = 0; + } else { + charMatch = s[text[j - 1]]; + } + if (d == 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch + | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]; + } + if ((rd[j] & matchmask) != 0) { + double score = match_bitapScore(d, j - 1, loc, pattern); + // This match will almost certainly be better than any existing + // match. But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.Max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + if (match_bitapScore(d + 1, loc, loc, pattern) > score_threshold) { + // No hope for a (better) match at greater error levels. + break; + } + last_rd = rd; + } + return best_loc; + } + + /** + * Compute and return the score for a match with e errors and x location. + * @param e Number of errors in match. + * @param x Location of match. + * @param loc Expected location of match. + * @param pattern Pattern being sought. + * @return Overall score for match (0.0 = good, 1.0 = bad). + */ + private double match_bitapScore(int e, int x, int loc, string pattern) { + float accuracy = (float)e / pattern.Length; + int proximity = Math.Abs(loc - x); + if (Match_Distance == 0) { + // Dodge divide by zero error. + return proximity == 0 ? accuracy : 1.0; + } + return accuracy + (proximity / (float) Match_Distance); + } + + /** + * Initialise the alphabet for the Bitap algorithm. + * @param pattern The text to encode. + * @return Hash of character locations. + */ + protected Dictionary match_alphabet(string pattern) { + Dictionary s = new Dictionary(); + char[] char_pattern = pattern.ToCharArray(); + foreach (char c in char_pattern) { + if (!s.ContainsKey(c)) { + s.Add(c, 0); + } + } + int i = 0; + foreach (char c in char_pattern) { + int value = s[c] | (1 << (pattern.Length - i - 1)); + s[c] = value; + i++; + } + return s; + } + + + // PATCH FUNCTIONS + + + /** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param patch The patch to grow. + * @param text Source text. + */ + protected void patch_addContext(Patch patch, string text) { + if (text.Length == 0) { + return; + } + string pattern = text.Substring(patch.start2, patch.length1); + int padding = 0; + + // Look for the first and last matches of pattern in text. If two + // different matches are found, increase the pattern length. + while (text.IndexOf(pattern, StringComparison.Ordinal) + != text.LastIndexOf(pattern, StringComparison.Ordinal) + && pattern.Length < Match_MaxBits - Patch_Margin - Patch_Margin) { + padding += Patch_Margin; + pattern = text.JavaSubstring(Math.Max(0, patch.start2 - padding), + Math.Min(text.Length, patch.start2 + patch.length1 + padding)); + } + // Add one chunk for good luck. + padding += Patch_Margin; + + // Add the prefix. + string prefix = text.JavaSubstring(Math.Max(0, patch.start2 - padding), + patch.start2); + if (prefix.Length != 0) { + patch.diffs.Insert(0, new Diff(Operation.EQUAL, prefix)); + } + // Add the suffix. + string suffix = text.JavaSubstring(patch.start2 + patch.length1, + Math.Min(text.Length, patch.start2 + patch.length1 + padding)); + if (suffix.Length != 0) { + patch.diffs.Add(new Diff(Operation.EQUAL, suffix)); + } + + // Roll back the start points. + patch.start1 -= prefix.Length; + patch.start2 -= prefix.Length; + // Extend the lengths. + patch.length1 += prefix.Length + suffix.Length; + patch.length2 += prefix.Length + suffix.Length; + } + + /** + * Compute a list of patches to turn text1 into text2. + * A set of diffs will be computed. + * @param text1 Old text. + * @param text2 New text. + * @return List of Patch objects. + */ + public List patch_make(string text1, string text2) { + // Check for null inputs not needed since null can't be passed in C#. + // No diffs provided, compute our own. + List diffs = diff_main(text1, text2, true); + if (diffs.Count > 2) { + diff_cleanupSemantic(diffs); + diff_cleanupEfficiency(diffs); + } + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text1 will be derived from the provided diffs. + * @param diffs Array of Diff objects for text1 to text2. + * @return List of Patch objects. + */ + public List patch_make(List diffs) { + // Check for null inputs not needed since null can't be passed in C#. + // No origin string provided, compute our own. + string text1 = diff_text1(diffs); + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is ignored, diffs are the delta between text1 and text2. + * @param text1 Old text + * @param text2 Ignored. + * @param diffs Array of Diff objects for text1 to text2. + * @return List of Patch objects. + * @deprecated Prefer patch_make(string text1, List diffs). + */ + public List patch_make(string text1, string text2, + List diffs) { + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is not provided, diffs are the delta between text1 and text2. + * @param text1 Old text. + * @param diffs Array of Diff objects for text1 to text2. + * @return List of Patch objects. + */ + public List patch_make(string text1, List diffs) { + // Check for null inputs not needed since null can't be passed in C#. + List patches = new List(); + if (diffs.Count == 0) { + return patches; // Get rid of the null case. + } + Patch patch = new Patch(); + int char_count1 = 0; // Number of characters into the text1 string. + int char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + string prepatch_text = text1; + string postpatch_text = text1; + foreach (Diff aDiff in diffs) { + if (patch.diffs.Count == 0 && aDiff.operation != Operation.EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (aDiff.operation) { + case Operation.INSERT: + patch.diffs.Add(aDiff); + patch.length2 += aDiff.text.Length; + postpatch_text = postpatch_text.Insert(char_count2, aDiff.text); + break; + case Operation.DELETE: + patch.length1 += aDiff.text.Length; + patch.diffs.Add(aDiff); + postpatch_text = postpatch_text.Remove(char_count2, + aDiff.text.Length); + break; + case Operation.EQUAL: + if (aDiff.text.Length <= 2 * Patch_Margin + && patch.diffs.Count() != 0 && aDiff != diffs.Last()) { + // Small equality inside a patch. + patch.diffs.Add(aDiff); + patch.length1 += aDiff.text.Length; + patch.length2 += aDiff.text.Length; + } + + if (aDiff.text.Length >= 2 * Patch_Margin) { + // Time for a new patch. + if (patch.diffs.Count != 0) { + patch_addContext(patch, prepatch_text); + patches.Add(patch); + patch = new Patch(); + // Unlike Unidiff, our patch lists have a rolling context. + // https://github.com/google/diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (aDiff.operation != Operation.INSERT) { + char_count1 += aDiff.text.Length; + } + if (aDiff.operation != Operation.DELETE) { + char_count2 += aDiff.text.Length; + } + } + // Pick up the leftover patch if not empty. + if (patch.diffs.Count != 0) { + patch_addContext(patch, prepatch_text); + patches.Add(patch); + } + + return patches; + } + + /** + * Given an array of patches, return another array that is identical. + * @param patches Array of Patch objects. + * @return Array of Patch objects. + */ + public List patch_deepCopy(List patches) { + List patchesCopy = new List(); + foreach (Patch aPatch in patches) { + Patch patchCopy = new Patch(); + foreach (Diff aDiff in aPatch.diffs) { + Diff diffCopy = new Diff(aDiff.operation, aDiff.text); + patchCopy.diffs.Add(diffCopy); + } + patchCopy.start1 = aPatch.start1; + patchCopy.start2 = aPatch.start2; + patchCopy.length1 = aPatch.length1; + patchCopy.length2 = aPatch.length2; + patchesCopy.Add(patchCopy); + } + return patchesCopy; + } + + /** + * Merge a set of patches onto the text. Return a patched text, as well + * as an array of true/false values indicating which patches were applied. + * @param patches Array of Patch objects + * @param text Old text. + * @return Two element Object array, containing the new text and an array of + * bool values. + */ + public Object[] patch_apply(List patches, string text) { + if (patches.Count == 0) { + return new Object[] { text, new bool[0] }; + } + + // Deep copy the patches so that no changes are made to originals. + patches = patch_deepCopy(patches); + + string nullPadding = this.patch_addPadding(patches); + text = nullPadding + text + nullPadding; + patch_splitMax(patches); + + int x = 0; + // delta keeps track of the offset between the expected and actual + // location of the previous patch. If there are patches expected at + // positions 10 and 20, but the first patch was found at 12, delta is 2 + // and the second patch has an effective expected position of 22. + int delta = 0; + bool[] results = new bool[patches.Count]; + foreach (Patch aPatch in patches) { + int expected_loc = aPatch.start2 + delta; + string text1 = diff_text1(aPatch.diffs); + int start_loc; + int end_loc = -1; + if (text1.Length > this.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern + // in the case of a monster delete. + start_loc = match_main(text, + text1.Substring(0, this.Match_MaxBits), expected_loc); + if (start_loc != -1) { + end_loc = match_main(text, + text1.Substring(text1.Length - this.Match_MaxBits), + expected_loc + text1.Length - this.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = this.match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= aPatch.length2 - aPatch.length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + string text2; + if (end_loc == -1) { + text2 = text.JavaSubstring(start_loc, + Math.Min(start_loc + text1.Length, text.Length)); + } else { + text2 = text.JavaSubstring(start_loc, + Math.Min(end_loc + this.Match_MaxBits, text.Length)); + } + if (text1 == text2) { + // Perfect match, just shove the Replacement text in. + text = text.Substring(0, start_loc) + diff_text2(aPatch.diffs) + + text.Substring(start_loc + text1.Length); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + List diffs = diff_main(text1, text2, false); + if (text1.Length > this.Match_MaxBits + && this.diff_levenshtein(diffs) / (float) text1.Length + > this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + diff_cleanupSemanticLossless(diffs); + int index1 = 0; + foreach (Diff aDiff in aPatch.diffs) { + if (aDiff.operation != Operation.EQUAL) { + int index2 = diff_xIndex(diffs, index1); + if (aDiff.operation == Operation.INSERT) { + // Insertion + text = text.Insert(start_loc + index2, aDiff.text); + } else if (aDiff.operation == Operation.DELETE) { + // Deletion + text = text.Remove(start_loc + index2, diff_xIndex(diffs, + index1 + aDiff.text.Length) - index2); + } + } + if (aDiff.operation != Operation.DELETE) { + index1 += aDiff.text.Length; + } + } + } + } + } + x++; + } + // Strip the padding off. + text = text.Substring(nullPadding.Length, text.Length + - 2 * nullPadding.Length); + return new Object[] { text, results }; + } + + /** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param patches Array of Patch objects. + * @return The padding string added to each side. + */ + public string patch_addPadding(List patches) { + short paddingLength = this.Patch_Margin; + string nullPadding = string.Empty; + for (short x = 1; x <= paddingLength; x++) { + nullPadding += (char)x; + } + + // Bump all the patches forward. + foreach (Patch aPatch in patches) { + aPatch.start1 += paddingLength; + aPatch.start2 += paddingLength; + } + + // Add some padding on start of first diff. + Patch patch = patches.First(); + List diffs = patch.diffs; + if (diffs.Count == 0 || diffs.First().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.Insert(0, new Diff(Operation.EQUAL, nullPadding)); + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.First().text.Length) { + // Grow first equality. + Diff firstDiff = diffs.First(); + int extraLength = paddingLength - firstDiff.text.Length; + firstDiff.text = nullPadding.Substring(firstDiff.text.Length) + + firstDiff.text; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches.Last(); + diffs = patch.diffs; + if (diffs.Count == 0 || diffs.Last().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.Add(new Diff(Operation.EQUAL, nullPadding)); + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.Last().text.Length) { + // Grow last equality. + Diff lastDiff = diffs.Last(); + int extraLength = paddingLength - lastDiff.text.Length; + lastDiff.text += nullPadding.Substring(0, extraLength); + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding; + } + + /** + * Look through the patches and break up any which are longer than the + * maximum limit of the match algorithm. + * Intended to be called only from within patch_apply. + * @param patches List of Patch objects. + */ + public void patch_splitMax(List patches) { + short patch_size = this.Match_MaxBits; + for (int x = 0; x < patches.Count; x++) { + if (patches[x].length1 <= patch_size) { + continue; + } + Patch bigpatch = patches[x]; + // Remove the big old patch. + patches.Splice(x--, 1); + int start1 = bigpatch.start1; + int start2 = bigpatch.start2; + string precontext = string.Empty; + while (bigpatch.diffs.Count != 0) { + // Create one of several smaller patches. + Patch patch = new Patch(); + bool empty = true; + patch.start1 = start1 - precontext.Length; + patch.start2 = start2 - precontext.Length; + if (precontext.Length != 0) { + patch.length1 = patch.length2 = precontext.Length; + patch.diffs.Add(new Diff(Operation.EQUAL, precontext)); + } + while (bigpatch.diffs.Count != 0 + && patch.length1 < patch_size - this.Patch_Margin) { + Operation diff_type = bigpatch.diffs[0].operation; + string diff_text = bigpatch.diffs[0].text; + if (diff_type == Operation.INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.Length; + start2 += diff_text.Length; + patch.diffs.Add(bigpatch.diffs.First()); + bigpatch.diffs.RemoveAt(0); + empty = false; + } else if (diff_type == Operation.DELETE && patch.diffs.Count == 1 + && patch.diffs.First().operation == Operation.EQUAL + && diff_text.Length > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.Length; + start1 += diff_text.Length; + empty = false; + patch.diffs.Add(new Diff(diff_type, diff_text)); + bigpatch.diffs.RemoveAt(0); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.Substring(0, Math.Min(diff_text.Length, + patch_size - patch.length1 - Patch_Margin)); + patch.length1 += diff_text.Length; + start1 += diff_text.Length; + if (diff_type == Operation.EQUAL) { + patch.length2 += diff_text.Length; + start2 += diff_text.Length; + } else { + empty = false; + } + patch.diffs.Add(new Diff(diff_type, diff_text)); + if (diff_text == bigpatch.diffs[0].text) { + bigpatch.diffs.RemoveAt(0); + } else { + bigpatch.diffs[0].text = + bigpatch.diffs[0].text.Substring(diff_text.Length); + } + } + } + // Compute the head context for the next patch. + precontext = this.diff_text2(patch.diffs); + precontext = precontext.Substring(Math.Max(0, + precontext.Length - this.Patch_Margin)); + + string postcontext = null; + // Append the end context for this patch. + if (diff_text1(bigpatch.diffs).Length > Patch_Margin) { + postcontext = diff_text1(bigpatch.diffs) + .Substring(0, Patch_Margin); + } else { + postcontext = diff_text1(bigpatch.diffs); + } + + if (postcontext.Length != 0) { + patch.length1 += postcontext.Length; + patch.length2 += postcontext.Length; + if (patch.diffs.Count != 0 + && patch.diffs[patch.diffs.Count - 1].operation + == Operation.EQUAL) { + patch.diffs[patch.diffs.Count - 1].text += postcontext; + } else { + patch.diffs.Add(new Diff(Operation.EQUAL, postcontext)); + } + } + if (!empty) { + patches.Splice(++x, 0, patch); + } + } + } + } + + /** + * Take a list of patches and return a textual representation. + * @param patches List of Patch objects. + * @return Text representation of patches. + */ + public string patch_toText(List patches) { + StringBuilder text = new StringBuilder(); + foreach (Patch aPatch in patches) { + text.Append(aPatch); + } + return text.ToString(); + } + + /** + * Parse a textual representation of patches and return a List of Patch + * objects. + * @param textline Text representation of patches. + * @return List of Patch objects. + * @throws ArgumentException If invalid input. + */ + public List patch_fromText(string textline) { + List patches = new List(); + if (textline.Length == 0) { + return patches; + } + string[] text = textline.Split('\n'); + int textPointer = 0; + Patch patch; + Regex patchHeader + = new Regex("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$"); + Match m; + char sign; + string line; + while (textPointer < text.Length) { + m = patchHeader.Match(text[textPointer]); + if (!m.Success) { + throw new ArgumentException("Invalid patch string: " + + text[textPointer]); + } + patch = new Patch(); + patches.Add(patch); + patch.start1 = Convert.ToInt32(m.Groups[1].Value); + if (m.Groups[2].Length == 0) { + patch.start1--; + patch.length1 = 1; + } else if (m.Groups[2].Value == "0") { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = Convert.ToInt32(m.Groups[2].Value); + } + + patch.start2 = Convert.ToInt32(m.Groups[3].Value); + if (m.Groups[4].Length == 0) { + patch.start2--; + patch.length2 = 1; + } else if (m.Groups[4].Value == "0") { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = Convert.ToInt32(m.Groups[4].Value); + } + textPointer++; + + while (textPointer < text.Length) { + try { + sign = text[textPointer][0]; + } catch (IndexOutOfRangeException) { + // Blank line? Whatever. + textPointer++; + continue; + } + line = text[textPointer].Substring(1); + line = line.Replace("+", "%2b"); + line = HttpUtility.UrlDecode(line); + if (sign == '-') { + // Deletion. + patch.diffs.Add(new Diff(Operation.DELETE, line)); + } else if (sign == '+') { + // Insertion. + patch.diffs.Add(new Diff(Operation.INSERT, line)); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.Add(new Diff(Operation.EQUAL, line)); + } else if (sign == '@') { + // Start of next patch. + break; + } else { + // WTF? + throw new ArgumentException( + "Invalid patch mode '" + sign + "' in: " + line); + } + textPointer++; + } + } + return patches; + } + + /** + * Encodes a string with URI-style % escaping. + * Compatible with JavaScript's encodeURI function. + * + * @param str The string to encode. + * @return The encoded string. + */ + public static string encodeURI(string str) { + // C# is overzealous in the replacements. Walk back on a few. + return new StringBuilder(HttpUtility.UrlEncode(str)) + .Replace('+', ' ').Replace("%20", " ").Replace("%21", "!") + .Replace("%2a", "*").Replace("%27", "'").Replace("%28", "(") + .Replace("%29", ")").Replace("%3b", ";").Replace("%2f", "/") + .Replace("%3f", "?").Replace("%3a", ":").Replace("%40", "@") + .Replace("%26", "&").Replace("%3d", "=").Replace("%2b", "+") + .Replace("%24", "$").Replace("%2c", ",").Replace("%23", "#") + .Replace("%7e", "~") + .ToString(); + } + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/DotNetListenerFactory.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/DotNetListenerFactory.cs new file mode 100644 index 0000000..1258861 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/DotNetListenerFactory.cs @@ -0,0 +1,20 @@ +using System; +using PubnubApi; + +namespace PubnubChatApi +{ + public class DotNetListenerFactory : ChatListenerFactory + { + public override SubscribeCallback ProduceListener( + Action>? messageCallback = null, + Action? presenceCallback = null, + Action>? signalCallback = null, + Action? objectEventCallback = null, + Action? messageActionCallback = null, + Action? fileCallback = null, Action? statusCallback = null) + { + return new SubscribeCallbackExt(messageCallback, presenceCallback, signalCallback, objectEventCallback, + messageActionCallback, fileCallback, statusCallback); + } + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ExponentialRateLimiter.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ExponentialRateLimiter.cs new file mode 100644 index 0000000..d0421ab --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ExponentialRateLimiter.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace PubnubChatApi +{ + public class ExponentialRateLimiter : IDisposable + { + private const int ThreadsMaxSleepMs = 1000; + private const int NoSleepRequired = -1; + + private readonly float _exponentialFactor; + private readonly ConcurrentDictionary _limiters; + private readonly CancellationTokenSource _cancellationTokenSource; + private Task _processorTask; + private readonly object _processorLock = new object(); + private bool _disposed = false; + + public ExponentialRateLimiter(float exponentialFactor) + { + _exponentialFactor = exponentialFactor; + _limiters = new ConcurrentDictionary(); + _cancellationTokenSource = new CancellationTokenSource(); + } + + public async void RunWithinLimits(string id, int baseIntervalMs, Func> task, Action callback, Action errorCallback) + { + if (baseIntervalMs == 0) + { + // Execute immediately for zero interval + try + { + var result = await task().ConfigureAwait(false); + callback(result); + } + catch (Exception e) + { + errorCallback(e); + } + return; + } + + var limiter = _limiters.GetOrAdd(id, _ => new RateLimiterRoot + { + Queue = new Queue(), + CurrentPenalty = 0, + BaseIntervalMs = baseIntervalMs, + NextIntervalMs = 0, + ElapsedMs = 0, + Finished = false, + LastTaskStartTime = DateTimeOffset.UtcNow + }); + + lock (limiter.Queue) + { + limiter.Queue.Enqueue(new TaskElement + { + Task = task, + Callback = callback, + ErrorCallback = errorCallback, + Penalty = limiter.CurrentPenalty + }); + } + + EnsureProcessorRunning(); + } + + private void EnsureProcessorRunning() + { + lock (_processorLock) + { + if (_processorTask == null || _processorTask.IsCompleted) + { + _processorTask = Task.Run(ProcessorLoop, _cancellationTokenSource.Token); + } + } + } + + private async Task ProcessorLoop() + { + var slept = 0; + var toSleep = 0; + + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + if (toSleep == NoSleepRequired) + { + break; + } + + if (slept >= toSleep) + { + toSleep = await ProcessQueue(slept).ConfigureAwait(false); + } + else + { + toSleep -= slept; + } + + slept = Math.Min(toSleep, ThreadsMaxSleepMs); + + if (slept > 0) + { + await Task.Delay(slept, _cancellationTokenSource.Token).ConfigureAwait(false); + } + } + } + + private async Task ProcessQueue(int sleptMs) + { + var toSleep = ThreadsMaxSleepMs; + var itemsToRemove = new List(); + var processingTasks = new List(); + + foreach (var kvp in _limiters) + { + var id = kvp.Key; + var limiter = kvp.Value; + + lock (limiter.Queue) + { + limiter.ElapsedMs += sleptMs; + + if (limiter.NextIntervalMs > limiter.ElapsedMs) + { + toSleep = Math.Min(toSleep, limiter.NextIntervalMs - limiter.ElapsedMs); + continue; + } + + // Start processing the task asynchronously + var processingTask = ProcessLimiterAsync(limiter); + processingTasks.Add(processingTask); + + limiter.CurrentPenalty++; + limiter.NextIntervalMs = (int)(limiter.BaseIntervalMs * Math.Pow(_exponentialFactor, limiter.CurrentPenalty)); + limiter.ElapsedMs = 0; + limiter.LastTaskStartTime = DateTimeOffset.UtcNow; + + toSleep = Math.Min(toSleep, limiter.NextIntervalMs); + + if (limiter.Finished) + { + itemsToRemove.Add(id); + } + } + } + + // Wait for all processing tasks to complete before continuing + // This ensures we don't overwhelm the system with concurrent tasks + if (processingTasks.Count > 0) + { + await Task.WhenAll(processingTasks).ConfigureAwait(false); + } + + // Remove finished limiters + foreach (var id in itemsToRemove) + { + _limiters.TryRemove(id, out _); + } + + if (_limiters.IsEmpty) + { + return NoSleepRequired; + } + + return toSleep; + } + + private async Task ProcessLimiterAsync(RateLimiterRoot limiterRoot) + { + TaskElement element; + + // Queue is already locked by caller, but we need to dequeue safely + lock (limiterRoot.Queue) + { + if (limiterRoot.Queue.Count == 0) + { + limiterRoot.Finished = true; + return; + } + + element = limiterRoot.Queue.Dequeue(); + } + + try + { + var result = await element.Task().ConfigureAwait(false); + element.Callback(result); + } + catch (Exception e) + { + element.ErrorCallback(e); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed && disposing) + { + _cancellationTokenSource?.Cancel(); + + try + { + _processorTask?.Wait(5000); // Wait up to 5 seconds for graceful shutdown + } + catch (AggregateException) + { + // Ignore cancellation exceptions during shutdown + } + + _cancellationTokenSource?.Dispose(); + _disposed = true; + } + } + + private class RateLimiterRoot + { + public Queue Queue { get; set; } + public int CurrentPenalty { get; set; } + public int BaseIntervalMs { get; set; } + public int NextIntervalMs { get; set; } + public int ElapsedMs { get; set; } + public bool Finished { get; set; } + public DateTimeOffset LastTaskStartTime { get; set; } + } + + private class TaskElement + { + public Func> Task { get; set; } + public Action Callback { get; set; } + public Action ErrorCallback { get; set; } + public int Penalty { get; set; } + } + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs deleted file mode 100644 index 8cb92e9..0000000 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using PubNubChatAPI.Entities; - -namespace PubnubChatApi.Utilities -{ - internal static class PointerParsers - { - internal static List ParseJsonMembershipPointers(Chat chat, string membershipPointersJson) - { - var memberships = new List(); - if (CUtilities.IsValidJson(membershipPointersJson)) - { - var membershipPointers = JsonConvert.DeserializeObject(membershipPointersJson); - if (membershipPointers == null) - { - return memberships; - } - - memberships = ParseJsonMembershipPointers(chat, membershipPointers); - } - - return memberships; - } - - internal static List ParseJsonMembershipPointers(Chat chat, IntPtr[] membershipPointers) - { - var memberships = new List(); - foreach (var membershipPointer in membershipPointers) - { - var id = Membership.GetMembershipIdFromPtr(membershipPointer); - if (chat.TryGetMembership(id, membershipPointer, out var membership)) - { - memberships.Add(membership); - } - } - return memberships; - } - - internal static List ParseJsonChannelPointers(Chat chat, string channelPointersJson) - { - var channels = new List(); - if (CUtilities.IsValidJson(channelPointersJson)) - { - var channelPointers = JsonConvert.DeserializeObject(channelPointersJson); - if (channelPointers == null) - { - return channels; - } - - channels = ParseJsonChannelPointers(chat, channelPointers); - } - - return channels; - } - - internal static List ParseJsonChannelPointers(Chat chat, IntPtr[] channelPointers) - { - var channels = new List(); - foreach (var channelPointer in channelPointers) - { - var id = Channel.GetChannelIdFromPtr(channelPointer); - if (chat.TryGetChannel(id, channelPointer, out var channel)) - { - channels.Add(channel); - } - } - return channels; - } - - internal static List ParseJsonUserPointers(Chat chat, string userPointersJson) - { - var users = new List(); - if (CUtilities.IsValidJson(userPointersJson)) - { - var userPointers = JsonConvert.DeserializeObject(userPointersJson); - if (userPointers == null) - { - return users; - } - - users = ParseJsonUserPointers(chat, userPointers); - } - - return users; - } - - internal static List ParseJsonUserPointers(Chat chat, IntPtr[] userPointers) - { - var users = new List(); - foreach (var userPointer in userPointers) - { - var id = User.GetUserIdFromPtr(userPointer); - if (chat.TryGetUser(id, userPointer, out var user)) - { - users.Add(user); - } - } - return users; - } - } -} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PubnubChatDotNetPNSDKSource.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PubnubChatDotNetPNSDKSource.cs new file mode 100644 index 0000000..e44ed08 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PubnubChatDotNetPNSDKSource.cs @@ -0,0 +1,20 @@ +using System.Globalization; +using System.Reflection; +using PubnubApi; +using PubnubApi.PNSDK; + +namespace PubnubChatApi +{ + public class PubnubChatDotNetPNSDKSource : IPNSDKSource + { + public string GetPNSDK() + { + var assembly = typeof(Pubnub).GetTypeInfo().Assembly; + var assemblyName = new AssemblyName(assembly.FullName); + string assemblyVersion = assemblyName.Version.ToString(); + var targetFramework = assembly.GetCustomAttribute()?.FrameworkDisplayName?.Replace(".",string.Empty).Replace(" ", string.Empty); + + return string.Format(CultureInfo.InvariantCulture, "{0}/CSharpChat/{1}", targetFramework??"UNKNOWN", assemblyVersion); + } + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/pubnub-chat.dll b/c-sharp-chat/PubnubChatApi/PubnubChatApi/pubnub-chat.dll deleted file mode 100644 index 0dba6f9..0000000 Binary files a/c-sharp-chat/PubnubChatApi/PubnubChatApi/pubnub-chat.dll and /dev/null differ diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.dll b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.dll deleted file mode 100644 index 05e215b..0000000 Binary files a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.dll and /dev/null differ diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.dll.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.dll.meta deleted file mode 100644 index 8ef7153..0000000 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.dll.meta +++ /dev/null @@ -1,33 +0,0 @@ -fileFormatVersion: 2 -guid: 2444a6b350ecfa04ca03c42e645f9b58 -PluginImporter: - externalObjects: {} - serializedVersion: 2 - iconMap: {} - executionOrder: {} - defineConstraints: [] - isPreloaded: 0 - isOverridable: 0 - isExplicitlyReferenced: 0 - validateReferences: 1 - platformData: - - first: - Any: - second: - enabled: 1 - settings: {} - - first: - Editor: Editor - second: - enabled: 0 - settings: - DefaultValueInitialized: true - - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.xml.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.meta similarity index 57% rename from unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.xml.meta rename to unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.meta index 77a3ae3..cc4760f 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.xml.meta +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.meta @@ -1,6 +1,7 @@ fileFormatVersion: 2 -guid: bc7e6b0a5099aae4e93b9d356d9ff027 -TextScriptImporter: +guid: 8939de9f1000f6146af8246da03349e7 +folderAsset: yes +DefaultImporter: externalObjects: {} userData: assetBundleName: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.xml b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.xml deleted file mode 100644 index 1354345..0000000 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.xml +++ /dev/null @@ -1,1574 +0,0 @@ - - - - PubnubChatApi - - - - - Class Channel represents a chat channel. - - - A channel is a entity that allows users to publish and receive messages. - - - - - - The name of the channel. - - - The name of the channel that is human meaningful. - - - The name of the channel. - - - - - The custom data of the channel. - - - The custom data that can be used to store additional information about the channel. - - - - The custom data is stored in JSON format. - - - - - The information about the last update of the channel. - - The time when the channel was last updated. - - - - - - The status of the channel. - - The last status response received from the server. - - - - - - The type of the channel. - - The type of the response received from the server when the channel was created. - - - - - - Event that is triggered when a message is received. - - - The event is triggered when a message is received in the channel - when the channel is connected. - - - The event that is triggered when a message is received. - - - var channel = //... - channel.OnMessageReceived += (message) => { - Console.WriteLine($"Message received: {message.Text}"); - }; - channel.Connect(); - - - - - - Event that is triggered when the channel is updated. - - - The event is triggered when the channel is updated by the user - or by any other entity. - - - The event that is triggered when the channel is updated. - - - var channel = //... - channel.OnChannelUpdate += (channel) => { - Console.WriteLine($"Channel updated: {channel.Name}"); - }; - channel.Connect(); - - - - - - Event that is triggered when any presence update occurs. - - - Presence update occurs when a user joins or leaves the channel. - - - The event that is triggered when any presence update occurs. - - - var channel = //... - channel.OnPresenceUpdate += (users) => { - Console.WriteLine($"Users present: {string.Join(", ", users)}"); - }; - channel.Connect(); - - - - - - - Tries to get the Message pinned to this Channel. - - The pinned Message object, null if there wasn't one. - True of a pinned Message was found, false otherwise. - - - - - Asynchronously tries to get the Message pinned to this Channel. - - The pinned Message object if there was one, null otherwise. - - - - Creates a new MessageDraft. - - Source of the user suggestions - Typing indicator trigger status. - User limit. - Channel limit. - - - - - Connects to the channel. - - Connects to the channel and starts receiving messages. - After connecting, the event is triggered when a message is received. - - - - - var channel = //... - channel.OnMessageReceived += (message) => { - Console.WriteLine($"Message received: {message.Text}"); - }; - channel.Connect(); - - - Thrown when an error occurs while connecting to the channel. - - - - - - - Joins the channel. - - Joins the channel and starts receiving messages. - After joining, the event is triggered when a message is received. - Additionally, there is a possibility to add additional parameters to the join request. - It also adds the membership to the channel. - - - - - var channel = //... - channel.OnMessageReceived += (message) => { - Console.WriteLine($"Message received: {message.Text}"); - }; - channel.Join(); - - - Thrown when an error occurs while joining the channel. - - - - - - - Disconnects from the channel. - - Disconnects from the channel and stops receiving messages. - Additionally, all the other listeners gets the presence update that the user has left the channel. - - - - - var channel = //... - channel.Connect(); - //... - channel.Disconnect(); - - - Thrown when an error occurs while disconnecting from the channel. - - - - - - Leaves the channel. - - Leaves the channel and stops receiving messages. - Additionally, all the other listeners gets the presence update that the user has left the channel. - The membership is also removed from the channel. - - - - - var channel = //... - channel.Join(); - //... - channel.Leave(); - - - Thrown when an error occurs while leaving the channel. - - - - - - - Sets the restrictions for the user. - - Sets the information about the restrictions for the user. - The restrictions include banning and muting the user. - - - The user identifier. - if set to true the user is banned. - if set to true the user is muted. - The reason for the restrictions. - - - var channel = //... - channel.SetRestrictions("user1", true, false, "Spamming"); - - - Thrown when an error occurs while setting the restrictions. - - - - - Sends the text message. - - Sends the text message to the channel. - The message is sent in the form of a text. - - - The message to be sent. - - - var channel = //... - channel.SendText("Hello, World!"); - - - Thrown when an error occurs while sending the message. - - - - - Updates the channel. - - Updates the channel with the new data. - The data includes the name, description, custom data, and type of the channel. - - - The updated data of the channel. - - - var channel = //... - channel.UpdateChannel(new ChatChannelData { - Name = "newName", - Description = "newDescription", - CustomDataJson = "{\"key\": \"value\"}", - Type = "newType" - }); - - - Thrown when an error occurs while updating the channel. - - - - - - Deletes the channel. - - Deletes the channel and removes all the messages and memberships from the channel. - - - - - var channel = //... - channel.DeleteChannel(); - - - Thrown when an error occurs while deleting the channel. - - - - Gets the user restrictions. - - Gets the user restrictions that include the information about the bans and mutes. - - - The user identifier. - The maximum amount of the restrictions received. - The start timetoken of the restrictions. - The end timetoken of the restrictions. - The user restrictions in JSON format. - - - var channel = //... - var restrictions = channel.GetUserRestrictions( - "user1", - 10, - "16686902600029072" - "16686902600028961", - ); - - - Thrown when an error occurs while getting the user restrictions. - - - - - Determines whether the user is present in the channel. - - The method checks whether the user is present in the channel. - - - The user identifier. - true if the user is present in the channel; otherwise, false. - - - var channel = //... - var isUserPresent = channel.IsUserPresent("user1"); - Console.WriteLine($"User present: {isUserPresent}"); - - - Thrown when an error occurs while checking the presence of the user. - - - - - Gets the list of users present in the channel. - - Gets all the users that are present in the channel. - - - The list of users present in the channel. - - - var channel = //... - var users = channel.WhoIsPresent(); - foreach (var user in users) { - Console.WriteLine($"User present: {user}"); - } - - - Thrown when an error occurs while getting the list of users present in the channel. - - - - - Gets the list of the Membership objects. - - Gets the list of the Membership objects that represent the users that are members - of the channel and the relationships between the users and the channel. - - - The maximum amount of the memberships received. - The start timetoken of the memberships. - The end timetoken of the memberships. - The list of the Membership objects. - - - var channel = //... - var memberships = channel.GetMemberships(10, "16686902600029072", "16686902600028961"); - foreach (var membership in memberships) { - Console.WriteLine($"Membership: {membership.UserId}"); - } - - - Thrown when an error occurs while getting the list of memberships. - - - - - Gets the Message object for the given timetoken. - - Gets the Message object for the given timetoken. - The timetoken is used to identify the message. - - - The timetoken of the message. - The out parameter that contains the Message object. - true if the message is found; otherwise, false. - - - var channel = //... - if (channel.TryGetMessage("16686902600029072", out var message)) { - Console.WriteLine($"Message: {message.Text}"); - } - - - - - - - - Asynchronously gets the Message object for the given timetoken sent from this Channel. - - TimeToken of the searched-for message. - Message object if one was found, null otherwise. - - - - Main class for the chat. - - Contains all the methods to interact with the chat. - It should be treated as a root of the chat system. - - - - The class is responsible for creating and managing channels, users, and messages. - - - - - Asynchronously initializes a new instance of the class. - - Creates a new chat instance. - - - Config with PubNub keys and values - - The constructor initializes the chat instance with the provided keys and user ID from the Config. - - - - - Creates a new public conversation. - - Creates a new public conversation with the provided channel ID. - Conversation allows users to interact with each other. - - - The channel ID. - The created channel. - - The method creates a chat channel with the provided channel ID. - - - - var chat = // ... - var channel = chat.CreatePublicConversation("channel_id"); - - - - - - - Creates a new public conversation. - - Creates a new public conversation with the provided channel ID. - Conversation allows users to interact with each other. - - - The channel ID. - The additional data for the channel. - The created channel. - - The method creates a chat channel with the provided channel ID. - - - - var chat = // ... - var channel = chat.CreatePublicConversation("channel_id"); - - - - - - - - Gets the channel by the provided channel ID. - - Tries to get the channel by the provided channel ID. - - - The channel ID. - The out channel. - True if the channel was found, false otherwise. - - - var chat = // ... - if (chat.TryGetChannel("channel_id", out var channel)) { - // Channel found - } - - - - - - - - Performs an async retrieval of a Channel object with a given ID. - - ID of the channel. - Channel object if it exists, null otherwise. - - - - Updates the channel with the provided channel ID. - - Updates the channel with the provided channel ID with the provided data. - - - The channel ID. - The updated data for the channel. - Throws an exception if the channel with the provided ID does not exist or any connection problem persists. - - - var chat = // ... - chat.UpdateChannel("channel_id", new ChatChannelData { - ChannelName = "new_name" - // ... - }); - - - - - - - Deletes the channel with the provided channel ID. - - The channel is deleted with all the messages and users. - - - The channel ID. - Throws an exception if the channel with the provided ID does not exist or any connection problem persists. - - - var chat = // ... - chat.DeleteChannel("channel_id"); - - - - - - Tries to retrieve the current User object for this chat. - - The retrieved current User object. - True if chat has a current user, false otherwise. - - - - - Asynchronously tries to retrieve the current User object for this chat. - - User object if there is a current user, null otherwise. - - - - Sets the restrictions for the user with the provided user ID. - - Sets the restrictions for the user with the provided user ID in the provided channel. - - - The user ID. - The channel ID. - The ban user flag. - The mute user flag. - The reason for the restrictions. - Throws an exception if the user with the provided ID does not exist or any connection problem persists. - - - var chat = // ... - chat.SetRestrictions("user_id", "channel_id", true, true, "Spamming"); - - - - - - Creates a new user with the provided user ID. - - Creates a new user with the empty data and the provided user ID. - - - The user ID. - The created user. - - The data for user is empty. - - Throws an exception if any connection problem persists. - - - var chat = // ... - var user = chat.CreateUser("user_id"); - - - - - - - Creates a new user with the provided user ID. - - Creates a new user with the provided data and the provided user ID. - - - The user ID. - The additional data for the user. - The created user. - Throws an exception if any connection problem persists. - - - var chat = // ... - var user = chat.CreateUser("user_id"); - - - - - - - Checks if the user with the provided user ID is present in the provided channel. - - Checks if the user with the provided user ID is present in the provided channel. - - - The user ID. - The channel ID. - True if the user is present, false otherwise. - Throws an exception if any connection problem persists. - - - var chat = // ... - if (chat.IsPresent("user_id", "channel_id")) { - // User is present - } - - - - - - - - Gets the list of users present in the provided channel. - - Gets all the users as a list of the strings present in the provided channel. - - - The channel ID. - The list of the users present in the channel. - Throws an exception if any connection problem persists. - - - var chat = // ... - var users = chat.WhoIsPresent("channel_id"); - foreach (var user in users) { - // User is present on the channel - } - - - - - - - - Gets the list of channels where the user with the provided user ID is present. - - Gets all the channels as a list of the strings where the user with the provided user ID is present. - - - The user ID. - The list of the channels where the user is present. - Throws an exception if any connection problem persists. - - - var chat = // ... - var channels = chat.WherePresent("user_id"); - foreach (var channel in channels) { - // Channel where User is IsPresent - }; - - - - - - - - Gets the user with the provided user ID. - - Tries to get the user with the provided user ID. - - - The user ID. - The out user. - True if the user was found, false otherwise. - Throws an exception if any connection problem persists. - - - var chat = // ... - if (chat.TryGetUser("user_id", out var user)) { - // User found - } - - - - - - - - Asynchronously gets the user with the provided user ID. - - ID of the User to get. - User object if one with given ID is found, null otherwise. - - - - Gets the list of users with the provided parameters. - - Gets all the users that matches the provided parameters. - - - The include parameter. - The amount of userts to get. - The start time token of the users. - The end time token of the users. - The list of the users that matches the provided parameters. - Throws an exception if any connection problem persists. - - - var chat = // ... - var users = chat.GetUsers( - "admin", - 10, - "16686902600029072" - "16686902600028961", - ); - foreach (var user in users) { - // User found - }; - - - - - - - Updates the user with the provided user ID. - - Updates the user with the provided user ID with the provided data. - - - The user ID. - The updated data for the user. - Throws an exception if the user with the provided ID does not exist or any connection problem persists. - - - var chat = // ... - chat.UpdateUser("user_id", new ChatUserData { - Username = "new_name" - // ... - }); - - - - - - - Deletes the user with the provided user ID. - - The user is deleted with all the messages and channels. - - - The user ID. - Throws an exception if the user with the provided ID does not exist or any connection problem persists. - - - var chat = // ... - chat.DeleteUser("user_id"); - - - - - - Gets the memberships of the user with the provided user ID. - - Gets all the memberships of the user with the provided user ID. - The memberships are limited by the provided limit and the time tokens. - - - The user ID. - The maximum amount of the memberships. - The start time token of the memberships. - The end time token of the memberships. - The list of the memberships of the user. - Throws an exception if the user with the provided ID does not exist or any connection problem persists. - - - var chat = // ... - var memberships = chat.GetUserMemberships( - "user_id", - 10, - "16686902600029072", - "16686902600028961" - ); - foreach (var membership in memberships) { - // Membership found - }; - - - - - - - Gets the memberships of the channel with the provided channel ID. - - Gets all the memberships of the channel with the provided channel ID. - The memberships are limited by the provided limit and the time tokens. - - - The channel ID. - The maximum amount of the memberships. - The start time token of the memberships. - The end time token of the memberships. - The list of the memberships of the channel. - Throws an exception if the channel with the provided ID does not exist or any connection problem persists. - - - var chat = // ... - var memberships = chat.GetChannelMemberships( - "user_id", - 10, - "16686902600029072", - "16686902600028961" - ); - foreach (var membership in memberships) { - // Membership found - }; - - - - - - - Gets the Message object for the given timetoken. - - Gets the Message object from the channel for the given timetoken. - The timetoken is used to identify the message. - - - The channel ID. - The timetoken of the message. - The out parameter that contains the Message object. - true if the message is found; otherwise, false. - - - var chat = // ... - if (chat.TryGetMessage("channel_id", "timetoken", out var message)) { - // Message found - }; - - - - - - - - Asynchronously gets the Message object for the given timetoken. - - ID of the channel on which the message was sent. - TimeToken of the searched-for message. - Message object if one was found, null otherwise. - - - - Tries to retrieve a ThreadChannel object from a Message object if there is one. - - Message on which the ThreadChannel is supposed to be. - Retrieved ThreadChannel or null if it wasn't found/ - True if a ThreadChannel was found, false otherwise. - - - - - Asynchronously tries to retrieve a ThreadChannel object from a Message object if there is one. - - Message on which the ThreadChannel is supposed to be. - The ThreadChannel object if one was found, null otherwise. - - - - Gets the channel message history. - - Gets the list of the messages that were sent in the channel with the provided parameters. - The history is limited by the provided count of messages, start time token, and end time token. - - - The channel ID. - The start time token of the messages. - The end time token of the messages. - The maximum amount of the messages. - The list of the messages that were sent in the channel. - Throws an exception if the channel with the provided ID does not exist or any connection problem persists. - - - var chat = // ... - var messages = chat.GetChannelMessageHistory("channel_id", "start_time_token", "end_time_token", 10); - foreach (var message in messages) { - // Message found - }; - - - - - - - Sets a new token for this Chat instance. - - - - - Decodes an existing token. - - A JSON string object containing permissions embedded in that token. - - - - Sets a new custom origin value. - - - - - - Represents a membership of a user in a channel. - - Memberships are relations between users and channels. They are used to determine - which users are allowed to send messages to which channels. - - - - Memberships are created when a user joins a channel and are deleted when a user leaves a channel. - - - - - - - - The user ID of the user that this membership belongs to. - - - - - The channel ID of the channel that this membership belongs to. - - - - - Returns a class with additional Membership data. - - - - - Event that is triggered when the membership is updated. - - This event is triggered when the membership is updated by the server. - Every time the membership is updated, this event is triggered. - - - - - membership.OnMembershipUpdated += (membership) => - { - Console.WriteLine("Membership updated!"); - }; - - - - - - - Updates the membership with a ChatMembershipData object. - - This method updates the membership with a ChatMembershipData object. This object can be used to store - additional information about the membership. - - - The ChatMembershipData object to update the membership with. - - - - - Represents a message in a chat channel. - - Messages are sent by users to chat channels. They can contain text - and other data, such as metadata or message actions. - - - - - - - - The text content of the message. - - This is the main content of the message. It can be any text that the user wants to send. - - - - - - The original, un-edited text of the message. - - - - - The time token of the message. - - The time token is a unique identifier for the message. - It is used to identify the message in the chat. - - - - - - The channel ID of the channel that the message belongs to. - - This is the ID of the channel that the message was sent to. - - - - - - The user ID of the user that sent the message. - - This is the unique ID of the user that sent the message. - Do not confuse this with the username of the user. - - - - - - The metadata of the message. - - The metadata is additional data that can be attached to the message. - It can be used to store additional information about the message. - - - - - - Whether the message has been deleted. - - This property indicates whether the message has been deleted. - If the message has been deleted, this property will be true. - It means that all the deletions are soft deletions. - - - - - - The data type of the message. - - This is the type of the message data. - It can be used to determine the type of the message. - - - - - - - Event that is triggered when the message is updated. - - This event is triggered when the message is updated by the server. - Every time the message is updated, this event is triggered. - - - - - var message = // ...; - message.OnMessageUpdated += (message) => - { - Console.WriteLine("Message updated!"); - }; - - - - - - - - Edits the text of the message. - - This method edits the text of the message. - It changes the text of the message to the new text provided. - - - The new text of the message. - - - var message = // ...; - message.EditMessageText("New text"); - - - - - - - Tries to get the ThreadChannel started on this Message. - - The retrieved ThreadChannel object, null if one wasn't found. - True if a ThreadChannel object has been found, false otherwise. - - - - - Asynchronously tries to get the ThreadChannel started on this Message. - - The retrieved ThreadChannel object, null if one wasn't found. - - - - Deletes the message. - - This method deletes the message. - It marks the message as deleted. - It means that the message will not be visible to other users, but the - message is treated as soft deleted. - - - - - var message = // ...; - message.DeleteMessage(); - - - - - - - - Enum describing the source for getting user suggestions for mentions. - - - - - Search for users globally. - - - - - Search only for users that are members of this channel. - - - - - Insert some text into the MessageDraft text at the given offset. - - The position from the start of the message draft where insertion will occur - Text the text to insert at the given offset - - - - Remove a number of characters from the MessageDraft text at the given offset. - - The position from the start of the message draft where removal will occur - Length the number of characters to remove, starting at the given offset - - - - Insert mention into the MessageDraft according to SuggestedMention.Offset, SuggestedMention.ReplaceFrom and - SuggestedMention.target. - - A SuggestedMention that can be obtained from MessageDraftStateListener - The text to replace SuggestedMention.ReplaceFrom with. SuggestedMention.ReplaceTo can be used for example. - - - - Add a mention to a user, channel or link specified by target at the given offset. - - The start of the mention - The number of characters (length) of the mention - The target of the mention - - - - Remove a mention starting at the given offset, if any. - - Offset the start of the mention to remove - - - - Update the whole message draft text with a new value. - Internally MessageDraft will try to calculate the most - optimal set of insertions and removals that will convert the current text to the provided text, in order to - preserve any mentions. This is a best effort operation, and if any mention text is found to be modified, - the mention will be invalidated and removed. - - - - - - Send the MessageDraft, along with its quotedMessage if any, on the channel. - - - - - Send the MessageDraft, along with its quotedMessage if any, on the channel. - - Additional parameters for sending the message. - - - - The text content of the message. - - This is the main content of the message. It can be any text that the user wants to send. - - - - - - The original, un-edited text of the thread message. - - - - - The time token of the message. - - The time token is a unique identifier for the message. - It is used to identify the message in the chat. - - - - - - The channel ID of the channel that the message belongs to. - - This is the ID of the channel that the message was sent to. - - - - - - The user ID of the user that sent the message. - - This is the unique ID of the user that sent the message. - Do not confuse this with the username of the user. - - - - - - The metadata of the message. - - The metadata is additional data that can be attached to the message. - It can be used to store additional information about the message. - - - - - - Whether the message has been deleted. - - This property indicates whether the message has been deleted. - If the message has been deleted, this property will be true. - It means that all the deletions are soft deletions. - - - - - - Edits the text of the message. - - This method edits the text of the message. - It changes the text of the message to the new text provided. - - - The new text of the message. - - - var message = // ...; - message.EditMessageText("New text"); - - - - - - - Represents a user in the chat. - - You can get information about the user, update the user's data, delete the user, set restrictions on the user, - - - - - - The user's user name. - - This might be user's display name in the chat. - - - - - - The user's external id. - - This might be user's id in the external system (e.g. Database, CRM, etc.) - - - - - - The user's profile url. - - This might be user's profile url to download the profile picture. - - - - - - The user's email. - - This should be user's email address. - - - - - - The user's custom data. - - This might be any custom data that you want to store for the user. - - - - - - The user's status. - - This is a string that represents the user's status. - - - - - - The user's data type. - - This is a string that represents the user's data type. - - - - - - Event that is triggered when the user is updated. - - This event is triggered when the user's data is updated. - You can subscribe to this event to get notified when the user is updated. - - - - - // var user = // ...; - user.OnUserUpdated += (user) => - { - Console.WriteLine($"User {user.UserName} is updated."); - }; - - - - - - - - Updates the user. - - This method updates the user's data. - - - The updated data for the user. - - This exception might be thrown when any error occurs while updating the user. - - - - var user = // ...; - user.UpdateUser(new ChatUserData - { - UserName = "New User Name", - }); - - - - - - - Deletes the user. - - This method deletes the user from the chat. - It will remove the user from all the channels and delete the user's data. - - - - This exception might be thrown when any error occurs while deleting the user. - - - - var user = // ...; - user.DeleteUser(); - - - - - - Sets restrictions on the user. - - This method sets the restrictions on the user. - You can ban the user from a channel, mute the user on the channel, or set the restrictions on the user. - - - The channel id on which the restrictions are set. - If set to true, the user is banned from the channel. - If set to true, the user is muted on the channel. - The reason for setting the restrictions on the user. - - This exception might be thrown when any error occurs while setting the restrictions on the user. - - - - var user = // ...; - user.SetRestrictions("channel_id", true, false, "Banned from the channel"); - - - - - - - Checks if the user is present on the channel. - - This method checks if the user is present on the channel. - - - The channel id on which the user's presence is to be checked. - - true if the user is present on the channel; otherwise, false. - - - This exception might be thrown when any error occurs while checking if the user is present on the channel. - - - - var user = // ...; - if (user.IsPresentOn("channel_id")) { - // User is present on the channel - } - - - - - - Gets the list of channels where the user is present. - - This method gets the list of channels where the user is present. - - - - The list of channels where the user is present. - - - The list is kept as a list of channel ids. - - - This exception might be thrown when any error occurs while getting the list of channels where the user is present. - - - - var user = // ...; - var channels = user.WherePresent(); - foreach (var channel in channels) { - Console.WriteLine(channel); - } - - - - - - Gets the list of memberships of the user. - - This methods gets the list of memberships of the user. - All the relationships of the user with the channels are considered as memberships. - - - The limit on the number of memberships to be fetched. - The start time token from which the memberships are to be fetched. - The end time token till which the memberships are to be fetched. - - The list of memberships of the user. - - - This exception might be thrown when any error occurs while getting the list of memberships of the user. - - - - var user = // ...; - var memberships = user.GetMemberships(50, "99999999999999999", "00000000000000000"); - foreach (var membership in memberships) { - Console.WriteLine(membership.ChannelId); - } - - - - - - - Data class for the chat channel. - - Contains all the data related to the chat channel. - - - - By default, all the properties are set to empty strings. - - - - - Data class for a chat membership. - - Contains all the additional data related to the chat membership. - - - - By default, all the properties are set to empty strings. - - - - - Data class for the chat user. - - Contains all the data related to the chat user. - - - - By default, all the properties are set to empty strings. - - - - - Data struct for restriction. - - - - - Represents the type of a chat message. - - Chat messages can have different types, such as text messages or message actions. - - - - - diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities.meta new file mode 100644 index 0000000..cc3c6f9 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 16ad60e4bf3194343bfc2dba617a87e7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base.meta new file mode 100644 index 0000000..6fe7877 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 297ae23e00377a349ad37d9e67894f67 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/ChatEntity.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/ChatEntity.cs new file mode 100644 index 0000000..7a5f6e2 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/ChatEntity.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using PubnubApi; + +namespace PubnubChatApi +{ + public abstract class ChatEntity + { + protected Chat chat; + protected Subscription updateSubscription; + protected abstract string UpdateChannelId { get; } + + internal ChatEntity(Chat chat) + { + this.chat = chat; + } + + protected void SetListening(ref Subscription subscription, SubscriptionOptions subscriptionOptions, bool listen, string channelId, SubscribeCallback listener) + { + if (listen) + { + if (subscription != null) + { + return; + } + subscription = chat.PubnubInstance.Channel(channelId).Subscription(subscriptionOptions); + subscription.AddListener(listener); + subscription.Subscribe(); + } + else + { + subscription?.Unsubscribe(); + } + } + + public virtual void SetListeningForUpdates(bool listen) + { + SetListening(ref updateSubscription, SubscriptionOptions.None, listen, UpdateChannelId, CreateUpdateListener()); + } + + protected abstract SubscribeCallback CreateUpdateListener(); + + public abstract Task Refresh(); + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/ChatEntity.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/ChatEntity.cs.meta new file mode 100644 index 0000000..cfd7f1c --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/ChatEntity.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9bcb8b482d639164384d2f91a98d64cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/UniqueChatEntity.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/UniqueChatEntity.cs new file mode 100644 index 0000000..c6241fe --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/UniqueChatEntity.cs @@ -0,0 +1,14 @@ +using System; + +namespace PubnubChatApi +{ + public abstract class UniqueChatEntity : ChatEntity + { + public string Id { get; protected set; } + + internal UniqueChatEntity(Chat chat, string uniqueId) : base(chat) + { + Id = uniqueId; + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/UniqueChatEntity.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/UniqueChatEntity.cs.meta new file mode 100644 index 0000000..2f288d3 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/UniqueChatEntity.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1e245bfd62a67c24eb9df4fcea6fd94c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs new file mode 100644 index 0000000..8b340cb --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs @@ -0,0 +1,1057 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Timers; +using PubnubApi; + +namespace PubnubChatApi +{ + /// + /// Class Channel represents a chat channel. + /// + /// + /// A channel is a entity that allows users to publish and receive messages. + /// + /// + public class Channel : UniqueChatEntity + { + /// + /// The name of the channel. + /// + /// + /// The name of the channel that is human meaningful. + /// + /// + /// The name of the channel. + public string Name => channelData.Name; + + /// + /// The description of the channel. + /// + /// + /// The description that allows users to understand the purpose of the channel. + /// + public string Description => channelData.Description; + + /// + /// The custom data of the channel. + /// + /// + /// The custom data that can be used to store additional information about the channel. + /// + /// + public Dictionary CustomData => channelData.CustomData ?? new (); + + /// + /// The information about the last update of the channel. + /// + /// The time when the channel was last updated. + /// + /// + public string Updated => channelData.Updated; + + /// + /// The status of the channel. + /// + /// The last status response received from the server. + /// + /// + public string Status => channelData.Status; + + /// + /// The type of the channel. + /// + /// The type of the response received from the server when the channel was created. + /// + /// + public string Type => channelData.Type; + + protected ChatChannelData channelData; + + protected Subscription? subscription; + + private Dictionary typingIndicators = new(); + + /// + /// Event that is triggered when a message is received. + /// + /// + /// The event is triggered when a message is received in the channel + /// when the channel is connected. + /// + /// + /// The event that is triggered when a message is received. + /// + /// + /// var channel = //... + /// channel.OnMessageReceived += (message) => { + /// Console.WriteLine($"Message received: {message.Text}"); + /// }; + /// channel.Connect(); + /// + /// + public event Action OnMessageReceived; + + /// + /// Event that is triggered when the channel is updated. + /// + /// + /// The event is triggered when the channel is updated by the user + /// or by any other entity. + /// + /// + /// The event that is triggered when the channel is updated. + /// + /// + /// var channel = //... + /// channel.OnChannelUpdate += (channel) => { + /// Console.WriteLine($"Channel updated: {channel.Name}"); + /// }; + /// channel.Connect(); + /// + /// + public event Action OnChannelUpdate; + + private Subscription presenceEventsSubscription; + /// + /// Event that is triggered when any presence update occurs. + /// + /// + /// Presence update occurs when a user joins or leaves the channel. + /// + /// + /// The event that is triggered when any presence update occurs. + /// + /// + /// var channel = //... + /// channel.OnPresenceUpdate += (users) => { + /// Console.WriteLine($"Users present: {string.Join(", ", users)}"); + /// }; + /// channel.Connect(); + /// + /// + /// + public event Action> OnPresenceUpdate; + + private Subscription typingEventsSubscription; + public event Action> OnUsersTyping; + private Subscription readReceiptsSubscription; + public event Action>> OnReadReceiptEvent; + private Subscription reportEventsSubscription; + public event Action OnReportEvent; + private Subscription customEventsSubscription; + public event Action OnCustomEvent; + + protected override string UpdateChannelId => Id; + + internal Channel(Chat chat, string channelId, ChatChannelData data) : base(chat, channelId) + { + UpdateLocalData(data); + } + + protected override SubscribeCallback CreateUpdateListener() + { + return chat.ListenerFactory.ProduceListener(objectEventCallback: delegate(Pubnub pn, PNObjectEventResult e) + { + if (ChatParsers.TryParseChannelUpdate(chat, this, e, out var updatedData)) + { + UpdateLocalData(updatedData); + OnChannelUpdate?.Invoke(this); + } + }); + } + + internal void UpdateLocalData(ChatChannelData? newData) + { + if (newData == null) + { + return; + } + channelData = newData; + } + + internal static async Task> UpdateChannelData(Chat chat, string channelId, ChatChannelData data) + { + var operation = chat.PubnubInstance.SetChannelMetadata().IncludeCustom(true) + .Channel(channelId); + if (!string.IsNullOrEmpty(data.Name)) + { + operation = operation.Name(data.Name); + } + if (!string.IsNullOrEmpty(data.Description)) + { + operation = operation.Description(data.Description); + } + if (!string.IsNullOrEmpty(data.Status)) + { + operation = operation.Status(data.Status); + } + if (data.CustomData != null) + { + operation = operation.Custom(data.CustomData); + } + if (!string.IsNullOrEmpty(data.Type)) + { + operation = operation.Type(data.Type); + } + return await operation.ExecuteAsync().ConfigureAwait(false); + } + + internal static async Task> GetChannelData(Chat chat, string channelId) + { + return await chat.PubnubInstance.GetChannelMetadata().IncludeCustom(true) + .Channel(channelId) + .ExecuteAsync().ConfigureAwait(false); + } + + public override async Task Refresh() + { + var result = new ChatOperationResult("Channel.Refresh()", chat); + var getData = await GetChannelData(chat, Id).ConfigureAwait(false); + if (result.RegisterOperation(getData)) + { + return result; + } + UpdateLocalData(getData.Result); + return result; + } + + /// + /// Sets whether to listen for custom events on this channel. + /// + /// True to start listening, false to stop listening. + public void SetListeningForCustomEvents(bool listen) + { + SetListening(ref customEventsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Custom, out var customEvent)) + { + OnCustomEvent?.Invoke(customEvent); + chat.BroadcastAnyEvent(customEvent); + } + })); + } + + /// + /// Sets whether to listen for report events on this channel. + /// + /// True to start listening, false to stop listening. + public void SetListeningForReportEvents(bool listen) + { + SetListening(ref reportEventsSubscription, SubscriptionOptions.None, listen, $"{Chat.INTERNAL_MODERATION_PREFIX}_{Id}", chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Report, out var reportEvent)) + { + OnReportEvent?.Invoke(reportEvent); + chat.BroadcastAnyEvent(reportEvent); + } + })); + } + + /// + /// Sets whether to listen for read receipt events on this channel. + /// + /// True to start listening, false to stop listening. + public void SetListeningForReadReceiptsEvents(bool listen) + { + SetListening(ref readReceiptsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + async delegate(Pubnub _, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Receipt, out var readEvent)) + { + var getMembers = await chat.GetChannelMemberships(Id).ConfigureAwait(false); + if (getMembers.Error) + { + return; + } + var members = getMembers.Result; + var outputDict = members.Memberships + .GroupBy(membership => membership.LastReadMessageTimeToken) + .ToDictionary( + g => g.Key, + g => g.Select(membership => membership.UserId).ToList() ?? new List() + ) ?? new Dictionary>(); + OnReadReceiptEvent?.Invoke(outputDict); + chat.BroadcastAnyEvent(readEvent); + } + })); + } + + /// + /// Sets whether to listen for typing events on this channel. + /// + /// True to start listening, false to stop listening. + public void SetListeningForTyping(bool listen) + { + SetListening(ref typingEventsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Typing, out var rawTypingEvent)) + { + try + { + var typingEvent = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(rawTypingEvent + .Payload); + var isTyping = (bool)typingEvent["value"]; + var userId = rawTypingEvent.UserId; + + chat.BroadcastAnyEvent(rawTypingEvent); + + //stop typing + if (!isTyping) + { + if (typingIndicators.TryGetValue(userId, out var timer)) + { + timer.Stop(); + typingIndicators.Remove(userId); + timer.Dispose(); + } + } + //start or restart typing + else + { + //Stop the old timer + if (typingIndicators.TryGetValue(userId, out var typingTimer)) + { + typingTimer.Stop(); + } + + //Create and start new timer + var newTimer = new Timer(chat.Config.TypingTimeout); + newTimer.Elapsed += (_, _) => + { + typingIndicators.Remove(userId); + OnUsersTyping?.Invoke(typingIndicators.Keys.ToList()); + }; + typingIndicators[userId] = newTimer; + newTimer.Start(); + } + OnUsersTyping?.Invoke(typingIndicators.Keys.ToList()); + } + catch (Exception e) + { + chat.Logger.Error($"Error when trying to broadcast typing event on channel \"{Id}\": {e.Message}"); + } + } + })); + } + + /// + /// Sets whether to listen for presence events on this channel. + /// + /// True to start listening, false to stop listening. + public void SetListeningForPresence(bool listen) + { + SetListening(ref presenceEventsSubscription, SubscriptionOptions.ReceivePresenceEvents, listen, Id, chat.ListenerFactory.ProduceListener(presenceCallback: + async delegate + { + var whoIs = await WhoIsPresent().ConfigureAwait(false); + if (whoIs.Error) + { + chat.Logger.Error($"Error when trying to broadcast presence update after WhoIs(): {whoIs.Exception.Message}"); + } + else + { + OnPresenceUpdate?.Invoke(whoIs.Result); + } + })); + } + + /// + /// Forwards a message to this channel. + /// + /// The message to forward. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task ForwardMessage(Message message) + { + return await SendText(message.MessageText, new SendTextParams() + { + Meta = message.Meta + }).ConfigureAwait(false); + } + + /// + /// Emits a user mention event for this channel. + /// + /// The ID of the user being mentioned. + /// The time token of the message containing the mention. + /// The text of the mention. + /// A ChatOperationResult indicating the success or failure of the operation. + public virtual async Task EmitUserMention(string userId, string timeToken, string text) + { + var jsonDict = new Dictionary() + { + {"text",text}, + {"messageTimetoken",timeToken}, + {"channel",Id} + }; + return await chat.EmitEvent(PubnubChatEventType.Mention, userId, + chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)).ConfigureAwait(false); + } + + /// + /// Starts a typing indicator for the current user in this channel. + /// + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task StartTyping() + { + return await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":true}}").ConfigureAwait(false); + } + + /// + /// Stops the typing indicator for the current user in this channel. + /// + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task StopTyping() + { + return await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":false}}").ConfigureAwait(false); + } + + /// + /// Pins a message to this channel. + /// + /// The message to pin. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task PinMessage(Message message) + { + channelData.CustomData ??= new (); + channelData.CustomData["pinnedMessageChannelID"] = message.ChannelId; + channelData.CustomData["pinnedMessageTimetoken"] = message.TimeToken; + return (await UpdateChannelData(chat, Id, channelData).ConfigureAwait(false)).ToChatOperationResult("Channel.PinMessage()", chat); + } + + /// + /// Unpins the currently pinned message from this channel. + /// + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task UnpinMessage() + { + channelData.CustomData ??= new (); + channelData.CustomData.Remove("pinnedMessageChannelID"); + channelData.CustomData.Remove("pinnedMessageTimetoken"); + return (await UpdateChannelData(chat, Id, channelData).ConfigureAwait(false)).ToChatOperationResult("Channel.UnPinMessage()", chat); + } + + + /// + /// Asynchronously tries to get the Message pinned to this Channel. + /// + /// A ChatOperationResult containing the pinned Message object if there was one, null otherwise. + public async Task> GetPinnedMessage() + { + var result = new ChatOperationResult("Channel.GetPinnedMessage()", chat); + if (result.RegisterOperation(await Refresh().ConfigureAwait(false))) + { + return result; + } + if(!CustomData.TryGetValue("pinnedMessageChannelID", out var pinnedChannelId) + || !CustomData.TryGetValue("pinnedMessageTimetoken", out var pinnedMessageTimeToken)) + { + result.Error = true; + result.Exception = new PNException($"Channel \"{Id}\" doesn't have a pinned message."); + return result; + } + + var getMessage = await chat.GetMessage(pinnedChannelId.ToString(), pinnedMessageTimeToken.ToString()).ConfigureAwait(false); + if (result.RegisterOperation(getMessage)) + { + return result; + } + + result.Result = getMessage.Result; + return result; + } + + /// + /// Creates a new MessageDraft. + /// + /// Source of the user suggestions + /// Typing indicator trigger status. + /// User limit. + /// Channel limit. + /// Whether the MessageDraft should search for suggestions whenever the text is changed. + /// The created MessageDraft. + public MessageDraft CreateMessageDraft(UserSuggestionSource userSuggestionSource = UserSuggestionSource.GLOBAL, + bool isTypingIndicatorTriggered = true, int userLimit = 10, int channelLimit = 10, bool shouldSearchForSuggestions = false) + { + return new MessageDraft(chat, this, userSuggestionSource, isTypingIndicatorTriggered, userLimit, channelLimit, shouldSearchForSuggestions); + } + + /// + /// Disconnects from the channel. + /// + /// Disconnects from the channel and stops receiving messages. + /// Additionally, all the other listeners gets the presence update that the user has left the channel. + /// + /// + /// + /// + /// var channel = //... + /// channel.Connect(); + /// //... + /// channel.Disconnect(); + /// + /// + /// + /// + public void Disconnect() + { + SetListening(ref subscription, SubscriptionOptions.None, false, Id, null); + } + + /// + /// Leaves the channel. + /// + /// Leaves the channel and stops receiving messages. + /// Additionally, all the other listeners gets the presence update that the user has left the channel. + /// The membership is also removed from the channel. + /// + /// + /// + /// + /// var channel = //... + /// channel.Join(); + /// //... + /// channel.Leave(); + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// + public async Task Leave() + { + Disconnect(); + var currentUserId = chat.PubnubInstance.GetCurrentUserId(); + return (await chat.PubnubInstance.RemoveMemberships().Uuid(currentUserId).Include(new[] + { + PNMembershipField.TYPE, + PNMembershipField.CUSTOM, + PNMembershipField.STATUS, + PNMembershipField.CHANNEL, + PNMembershipField.CHANNEL_CUSTOM, + PNMembershipField.CHANNEL_TYPE, + PNMembershipField.CHANNEL_STATUS + }).Channels(new List() { Id }) + .ExecuteAsync().ConfigureAwait(false)).ToChatOperationResult("Channel.Leave()", chat); + } + + /// + /// Connects to the channel. + /// + /// Connects to the channel and starts receiving messages. + /// After connecting, the event is triggered when a message is received. + /// + /// + /// + /// + /// var channel = //... + /// channel.OnMessageReceived += (message) => { + /// Console.WriteLine($"Message received: {message.Text}"); + /// }; + /// channel.Connect(); + /// + /// + /// + /// + /// + public void Connect() + { + SetListening(ref subscription, SubscriptionOptions.None, true, Id, chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseMessageResult(chat, m, out var message)) + { + OnMessageReceived?.Invoke(message); + } + })); + } + + /// + /// Joins the channel. + /// + /// Joins the channel and starts receiving messages. + /// After joining, the event is triggered when a message is received. + /// Additionally, there is a possibility to add additional parameters to the join request. + /// It also adds the membership to the channel. + /// + /// + /// + /// + /// var channel = //... + /// channel.OnMessageReceived += (message) => { + /// Console.WriteLine($"Message received: {message.Text}"); + /// }; + /// channel.Join(); + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// + public async Task Join(ChatMembershipData? membershipData = null) + { + var result = new ChatOperationResult("Channel.Join()", chat); + membershipData ??= new ChatMembershipData(); + var currentUserId = chat.PubnubInstance.GetCurrentUserId(); + var setMembership = await chat.PubnubInstance.SetMemberships().Uuid(currentUserId) + .Channels(new List() + { + new PNMembership() + { + Channel = Id, + Custom = membershipData.CustomData, + Status = membershipData.Status, + Type = membershipData.Type + } + }) + .Include(new [] + { + PNMembershipField.TYPE, + PNMembershipField.CUSTOM, + PNMembershipField.STATUS, + PNMembershipField.CHANNEL, + PNMembershipField.CHANNEL_CUSTOM + }).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(setMembership)) + { + return result; + } + var joinMembership = new Membership(chat, currentUserId, Id, membershipData); + var setLast = await joinMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()).ConfigureAwait(false); + if (result.RegisterOperation(setLast)) + { + return result; + } + Connect(); + return result; + } + + /// + /// Sets the restrictions for the user. + /// + /// Sets the information about the restrictions for the user. + /// The restrictions include banning and muting the user. + /// + /// + /// The user identifier. + /// if set to true the user is banned. + /// if set to true the user is muted. + /// The reason for the restrictions. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var channel = //... + /// var result = await channel.SetRestrictions("user1", true, false, "Spamming"); + /// + /// + /// + public async Task SetRestrictions(string userId, bool banUser, bool muteUser, string reason) + { + return await chat.SetRestriction(userId, Id, banUser, muteUser, reason).ConfigureAwait(false); + } + + /// + /// Sets the restrictions for the user using a Restriction object. + /// + /// The user identifier. + /// The restriction object containing ban, mute, and reason information. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task SetRestrictions(string userId, Restriction restriction) + { + return await SetRestrictions(userId, restriction.Ban, restriction.Mute, restriction.Reason).ConfigureAwait(false); + } + + /// + /// Sends the text message. + /// + /// Sends the text message to the channel. + /// The message is sent in the form of a text. + /// + /// + /// The message to be sent. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var channel = //... + /// var result = await channel.SendText("Hello, World!"); + /// + /// + /// + public async Task SendText(string message) + { + return await SendText(message, new SendTextParams()).ConfigureAwait(false); + } + + /// + /// Sends the text message with additional parameters. + /// + /// Sends the text message to the channel with additional options such as metadata, quoted messages, and mentioned users. + /// + /// + /// The message to be sent. + /// Additional parameters for sending the message. + /// A ChatOperationResult indicating the success or failure of the operation. + public virtual async Task SendText(string message, SendTextParams sendTextParams) + { + var result = new ChatOperationResult("Channel.SendText()", chat); + + var baseInterval = Type switch + { + "public" => chat.Config.RateLimitsPerChannel.PublicConversation, + "direct" => chat.Config.RateLimitsPerChannel.DirectConversation, + "group" => chat.Config.RateLimitsPerChannel.GroupConversation, + _ => chat.Config.RateLimitsPerChannel.UnknownConversation + }; + + TaskCompletionSource completionSource = new (); + chat.RateLimiter.RunWithinLimits(Id, baseInterval, async () => + { + var messageDict = new Dictionary() + { + {"text", message}, + {"type", "text"} + }; + var meta = sendTextParams.Meta ?? new Dictionary(); + if (sendTextParams.QuotedMessage != null) + { + //TODO: may create some "ToJSON()" methods for chat entities + //TODO: what about edited messages?? + meta.Add("quotedMessage", new Dictionary() + { + {"timetoken", sendTextParams.QuotedMessage.TimeToken}, + {"text", sendTextParams.QuotedMessage.OriginalMessageText}, + {"userId", sendTextParams.QuotedMessage.UserId}, + {"channelId", sendTextParams.QuotedMessage.ChannelId}, + }); + } + if (sendTextParams.MentionedUsers.Any()) + { + meta.Add("mentionedUsers", sendTextParams.MentionedUsers); + } + + var publishResult = await chat.PubnubInstance.Publish() + .Channel(Id) + .ShouldStore(sendTextParams.StoreInHistory) + .UsePOST(sendTextParams.SendByPost) + .Message(chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(messageDict)) + .Meta(meta) + .ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(publishResult)) + { + return result; + } + foreach (var mention in sendTextParams.MentionedUsers) + { + result.RegisterOperation(await EmitUserMention(mention.Value.Id, + publishResult.Result.Timetoken.ToString(), message).ConfigureAwait(false)); + } + return result; + }, response => + { + if (result.Error) + { + chat.Logger.Error($"Error occured when trying to SendText(): {result.Exception.Message}"); + } + completionSource.SetResult(true); + }, exception => + { + chat.Logger.Error($"Error occured when trying to SendText(): {exception.Message}"); + completionSource.SetResult(true); + }); + + await completionSource.Task.ConfigureAwait(false); + + return result; + } + + /// + /// Updates the channel. + /// + /// Updates the channel with the new data. + /// The data includes the name, description, custom data, and type of the channel. + /// + /// + /// The updated data of the channel. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var channel = //... + /// var result = await channel.Update(new ChatChannelData { + /// Name = "newName", + /// Description = "newDescription", + /// CustomDataJson = "{\"key\": \"value\"}", + /// Type = "newType" + /// }); + /// + /// + /// + /// + public async Task Update(ChatChannelData updatedData) + { + return await chat.UpdateChannel(Id, updatedData).ConfigureAwait(false); + } + + /// + /// Deletes the channel. + /// + /// Deletes the channel and removes all the messages and memberships from the channel. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var channel = //... + /// var result = await channel.Delete(); + /// + /// + public async Task Delete() + { + return await chat.DeleteChannel(Id).ConfigureAwait(false); + } + + /// + /// Gets the user restrictions for a specific user. + /// + /// Gets the user restrictions that include the information about the bans and mutes for the specified user. + /// + /// + /// The user to get restrictions for. + /// A ChatOperationResult containing the Restriction object if restrictions exist for the user, error otherwise. + /// + /// + /// var channel = //... + /// var user = //... + /// var result = await channel.GetUserRestrictions(user); + /// var restriction = result.Result; + /// + /// + /// + public async Task> GetUserRestrictions(User user) + { + var result = new ChatOperationResult("Channel.GetUserRestrictions()", chat); + var membershipsResult = await chat.PubnubInstance.GetMemberships().Uuid(user.Id).Include(new[] + { + PNMembershipField.CUSTOM + }).Filter($"channel.id == \"{Chat.INTERNAL_MODERATION_PREFIX}_{Id}\"").IncludeCount(true).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(membershipsResult) || membershipsResult.Result.Memberships == null || !membershipsResult.Result.Memberships.Any()) + { + result.Error = true; + return result; + } + var membership = membershipsResult.Result.Memberships[0]; + try + { + result.Result = new Restriction() + { + Ban = (bool)membership.Custom["ban"], + Mute = (bool)membership.Custom["mute"], + Reason = (string)membership.Custom["reason"] + }; + } + catch (Exception e) + { + result.Error = true; + result.Exception = e; + } + return result; + } + + /// + /// Gets all user restrictions for this channel. + /// + /// Sort criteria for restrictions. + /// The maximum number of restrictions to retrieve. + /// Pagination object for retrieving specific page results. + /// A ChatOperationResult containing the wrapper with all user restrictions for this channel. + public async Task> GetUsersRestrictions(string sort = "", int limit = 0, + PNPageObject page = null) + { + var result = new ChatOperationResult("Channel.GetUsersRestrictions()", chat){Result = new UsersRestrictionsWrapper()}; + var operation = chat.PubnubInstance.GetChannelMembers().Channel($"{Chat.INTERNAL_MODERATION_PREFIX}_{Id}") + .Include(new[] + { + PNChannelMemberField.CUSTOM, + PNChannelMemberField.UUID + }).IncludeCount(true); + if (!string.IsNullOrEmpty(sort)) + { + operation = operation.Sort(new List() { sort }); + } + if (limit > 0) + { + operation = operation.Limit(limit); + } + if (page != null) + { + operation = operation.Page(page); + } + var membersResult = await operation.ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(membersResult)) + { + return result; + } + + result.Result.Page = membersResult.Result.Page; + result.Result.Total = membersResult.Result.TotalCount; + foreach (var member in membersResult.Result.ChannelMembers) + { + try + { + result.Result.Restrictions.Add(new UserRestriction() + { + Ban = (bool)member.Custom["ban"], + Mute = (bool)member.Custom["mute"], + Reason = (string)member.Custom["reason"], + UserId = member.UuidMetadata.Uuid + }); + } + catch (Exception e) + { + chat.Logger.Warn($"Incorrect data was encountered when parsing Channel Restriction for User \"{member.UuidMetadata.Uuid}\" in Channel \"{Id}\". Exception was: {e.Message}"); + } + } + return result; + } + + /// + /// Determines whether the user is present in the channel. + /// + /// The method checks whether the user is present in the channel. + /// + /// + /// The user identifier. + /// A ChatOperationResult containing true if the user is present in the channel; otherwise, false. + /// + /// + /// var channel = //... + /// var result = await channel.IsUserPresent("user1"); + /// var isUserPresent = result.Result; + /// Console.WriteLine($"User present: {isUserPresent}"); + /// + /// + /// + public async Task> IsUserPresent(string userId) + { + var result = new ChatOperationResult("Channel.IsUserPresent()", chat); + var wherePresent = await chat.WherePresent(userId).ConfigureAwait(false); + if (result.RegisterOperation(wherePresent)) + { + return result; + } + result.Result = wherePresent.Result.Contains(Id); + return result; + } + + /// + /// Gets the list of users present in the channel. + /// + /// Gets all the users that are present in the channel. + /// + /// + /// A ChatOperationResult containing the list of users present in the channel. + /// + /// + /// var channel = //... + /// var result = await channel.WhoIsPresent(); + /// var users = result.Result; + /// foreach (var user in users) { + /// Console.WriteLine($"User present: {user}"); + /// } + /// + /// + /// + public async Task>> WhoIsPresent() + { + var result = new ChatOperationResult>("Channel.WhoIsPresent()", chat) { Result = new List() }; + var response = await chat.PubnubInstance.HereNow().Channels(new[] { Id }).IncludeState(true) + .IncludeUUIDs(true).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(response)) + { + return result; + } + + foreach (var occupant in response.Result.Channels[Id].Occupants) + { + result.Result.Add(occupant.Uuid); + } + return result; + } + + /// + /// Gets the list of the Membership objects. + /// + /// Gets the list of the Membership objects that represent the users that are members + /// of the channel and the relationships between the users and the channel. + /// + /// + /// The filter parameter. + /// The sort parameter. + /// The maximum amount of the memberships received. + /// The page object for pagination. + /// A ChatOperationResult containing the list of the Membership objects. + /// + /// + /// var channel = //... + /// var result = await channel.GetMemberships(limit: 10); + /// var memberships = result.Result.Memberships; + /// foreach (var membership in memberships) { + /// Console.WriteLine($"Membership: {membership.UserId}"); + /// } + /// + /// + /// + public async Task> GetMemberships(string filter = "", string sort = "", int limit = 0, + PNPageObject page = null) + { + return await chat.GetChannelMemberships(Id, filter, sort, limit, page).ConfigureAwait(false); + } + + /// + /// Asynchronously gets the Message object for the given timetoken sent from this Channel. + /// + /// TimeToken of the searched-for message. + /// A ChatOperationResult containing the Message object if one was found, null otherwise. + public async Task> GetMessage(string timeToken) + { + return await chat.GetMessage(Id, timeToken).ConfigureAwait(false); + } + + /// + /// Gets the message history for this channel within a specified time range. + /// + /// The start time token for the history range. + /// The end time token for the history range. + /// The maximum number of messages to retrieve. + /// A ChatOperationResult containing the list of messages from this channel. + public async Task>> GetMessageHistory(string startTimeToken, string endTimeToken, + int count) + { + return await chat.GetChannelMessageHistory(Id, startTimeToken, endTimeToken, count).ConfigureAwait(false); + } + + /// + /// Invites a user to this channel. + /// + /// The user to invite. + /// A ChatOperationResult containing the created membership for the invited user. + public async Task> Invite(User user) + { + return await chat.InviteToChannel(Id, user.Id).ConfigureAwait(false); + } + + /// + /// Invites multiple users to this channel. + /// + /// The list of users to invite. + /// A ChatOperationResult containing a list of created memberships for the invited users. + public async Task>> InviteMultiple(List users) + { + return await chat.InviteMultipleToChannel(Id, users).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs.meta new file mode 100644 index 0000000..86d516f --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 61056492ff5c27c449ea953e0f70634b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs new file mode 100644 index 0000000..170400f --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs @@ -0,0 +1,1691 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; + +namespace PubnubChatApi +{ + //TODO: make IDisposable? + /// + /// Main class for the chat. + /// + /// Contains all the methods to interact with the chat. + /// It should be treated as a root of the chat system. + /// + /// + /// + /// The class is responsible for creating and managing channels, users, and messages. + /// + public class Chat + { + internal const string INTERNAL_MODERATION_PREFIX = "PUBNUB_INTERNAL_MODERATION"; + internal const string MESSAGE_THREAD_ID_PREFIX = "PUBNUB_INTERNAL_THREAD"; + + public Pubnub PubnubInstance { get; } + public PubnubLogModule Logger => PubnubInstance.PNConfig.Logger; + + internal ChatListenerFactory ListenerFactory { get; } + + public event Action OnAnyEvent; + + public ChatAccessManager ChatAccessManager { get; } + public PubnubChatConfig Config { get; } + internal ExponentialRateLimiter RateLimiter { get; } + + private bool storeActivity = false; + + /// + /// Initializes a new instance of the class. + /// + /// Creates a new chat instance. + /// + /// + /// Config with Chat specific parameters + /// Config with PubNub keys and values + /// Optional injectable listener factory, used in Unity to allow for dispatching Chat callbacks on main thread. + /// A ChatOperationResult containing the created Chat instance. + /// + /// The constructor initializes the Chat object with a new Pubnub instance. + /// + public static async Task> CreateInstance(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatListenerFactory? listenerFactory = null) + { + var chat = new Chat(chatConfig, pubnubConfig, listenerFactory); + if (chatConfig.StoreUserActivityTimestamp) + { + chat.StoreActivityTimeStamp(); + } + var result = new ChatOperationResult("Chat.CreateInstance()", chat){Result = chat}; + var getUser = await chat.GetCurrentUser().ConfigureAwait(false); + if (getUser.Error) + { + result.RegisterOperation(await chat.CreateUser(chat.PubnubInstance.GetCurrentUserId()).ConfigureAwait(false)); + } + return result; + } + + /// + /// Initializes a new instance of the class. + /// + /// Creates a new chat instance. + /// + /// + /// Config with Chat specific parameters + /// An existing Pubnub object instance + /// Optional injectable listener factory, used in Unity to allow for dispatching Chat callbacks on main thread. + /// A ChatOperationResult containing the created Chat instance. + /// + /// The constructor initializes the Chat object with an existing Pubnub instance. + /// + public static async Task> CreateInstance(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? listenerFactory = null) + { + var chat = new Chat(chatConfig, pubnub, listenerFactory); + if (chatConfig.StoreUserActivityTimestamp) + { + chat.StoreActivityTimeStamp(); + } + var result = new ChatOperationResult("Chat.CreateInstance()", chat){Result = chat}; + var getUser = await chat.GetCurrentUser().ConfigureAwait(false); + if (getUser.Error) + { + result.RegisterOperation(await chat.CreateUser(chat.PubnubInstance.GetCurrentUserId()).ConfigureAwait(false)); + } + return result; + } + + internal Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatListenerFactory? listenerFactory = null) + { + PubnubInstance = new Pubnub(pubnubConfig); + ListenerFactory = listenerFactory ?? new DotNetListenerFactory(); + Config = chatConfig; + ChatAccessManager = new ChatAccessManager(this); + RateLimiter = new ExponentialRateLimiter(chatConfig.RateLimitFactor); + } + + internal Chat(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? listenerFactory = null) + { + Config = chatConfig; + PubnubInstance = pubnub; + ListenerFactory = listenerFactory ?? new DotNetListenerFactory(); + ChatAccessManager = new ChatAccessManager(this); + RateLimiter = new ExponentialRateLimiter(chatConfig.RateLimitFactor); + } + + #region Channels + + /// + /// Adds a listener for channel update events on multiple channels. + /// + /// List of channel IDs to listen to. + /// The listener callback to invoke on channel updates. + public async Task AddListenerToChannelsUpdate(List channelIds, Action listener) + { + foreach (var channelId in channelIds) + { + var getResult = await GetChannel(channelId).ConfigureAwait(false); + if (!getResult.Error) + { + getResult.Result.OnChannelUpdate += listener; + } + } + } + + /// + /// Creates a new public conversation. + /// + /// Creates a new public conversation with the provided channel ID. + /// Conversation allows users to interact with each other. + /// + /// + /// The channel ID. + /// A ChatOperationResult containing the created Channel object. + /// + /// The method creates a chat channel with the provided channel ID. + /// + /// + /// + /// var chat = // ... + /// var result = await chat.CreatePublicConversation("channel_id"); + /// var channel = result.Result; + /// + /// + /// + public async Task> CreatePublicConversation(string channelId = "") + { + if (string.IsNullOrEmpty(channelId)) + { + channelId = Guid.NewGuid().ToString(); + } + + return await CreatePublicConversation(channelId, new ChatChannelData()).ConfigureAwait(false); + } + + /// + /// Creates a new public conversation. + /// + /// Creates a new public conversation with the provided channel ID. + /// Conversation allows users to interact with each other. + /// + /// + /// The channel ID. + /// The additional data for the channel. + /// A ChatOperationResult containing the created Channel object. + /// + /// The method creates a chat channel with the provided channel ID. + /// + /// + /// + /// var chat = // ... + /// var result = await chat.CreatePublicConversation("channel_id"); + /// var channel = result.Result; + /// + /// + /// + /// + public async Task> CreatePublicConversation(string channelId, ChatChannelData additionalData) + { + var result = new ChatOperationResult("Chat.CreatePublicConversation()", this); + var existingChannel = await GetChannel(channelId).ConfigureAwait(false); + if (!result.RegisterOperation(existingChannel)) + { + Logger.Debug("Trying to create a channel with ID that already exists! Returning existing one."); + result.Result = existingChannel.Result; + return result; + } + + additionalData.Type = "public"; + var updated = await Channel.UpdateChannelData(this, channelId, additionalData).ConfigureAwait(false); + if (result.RegisterOperation(updated)) + { + return result; + } + var channel = new Channel(this, channelId, additionalData); + result.Result = channel; + return result; + } + + private async Task> CreateConversation( + string type, + List users, + string channelId = "", + ChatChannelData? channelData = null, + ChatMembershipData? membershipData = null) + { + var result = new ChatOperationResult($"Chat.CreateConversation-{type}", this){Result = new CreatedChannelWrapper()}; + + if (string.IsNullOrEmpty(channelId)) + { + channelId = Guid.NewGuid().ToString(); + } + + var existingChannel = await GetChannel(channelId).ConfigureAwait(false); + if (!result.RegisterOperation(existingChannel)) + { + Logger.Debug("Trying to create a channel with ID that already exists! Returning existing one."); + result.Result.CreatedChannel = existingChannel.Result; + return result; + } + + channelData ??= new ChatChannelData(); + channelData.Type = type; + var updated = await Channel.UpdateChannelData(this, channelId, channelData).ConfigureAwait(false); + if (result.RegisterOperation(updated)) + { + return result; + } + + membershipData ??= new ChatMembershipData(); + var currentUserId = PubnubInstance.GetCurrentUserId(); + var setMembershipResult = await PubnubInstance.SetMemberships() + .Uuid(currentUserId) + .Include( + new[] + { + PNMembershipField.CHANNEL_CUSTOM, + PNMembershipField.CUSTOM, + PNMembershipField.CHANNEL, + PNMembershipField.STATUS, + PNMembershipField.TYPE, + }) + .Channels(new List() { new () + { + Channel = channelId, + Custom = membershipData.CustomData, + Status = membershipData.Status, + Type = membershipData.Type + }}) + .ExecuteAsync().ConfigureAwait(false); + + if (result.RegisterOperation(setMembershipResult)) + { + return result; + } + + var hostMembership = new Membership(this, currentUserId, channelId, membershipData); + result.Result.HostMembership = hostMembership; + + var channel = new Channel(this, channelId, channelData); + result.Result.CreatedChannel = channel; + + if (type == "direct") + { + var inviteMembership = await InviteToChannel(channelId, users[0].Id).ConfigureAwait(false); + if (result.RegisterOperation(inviteMembership)) + { + return result; + } + result.Result.InviteesMemberships = new List() { inviteMembership.Result }; + }else if (type == "group") + { + var inviteMembership = await InviteMultipleToChannel(channelId, users).ConfigureAwait(false); + if (result.RegisterOperation(inviteMembership)) + { + return result; + } + result.Result.InviteesMemberships = new List(inviteMembership.Result); + } + return result; + } + + /// + /// Creates a direct conversation between the current user and the specified user. + /// + /// The user to create a direct conversation with. + /// Optional channel ID. If not provided, a new GUID will be used. + /// Optional additional channel data. + /// Optional membership data for the conversation. + /// A ChatOperationResult containing the created channel wrapper with channel and membership information. + public async Task> CreateDirectConversation(User user, string channelId = "", + ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) + { + return await CreateConversation("direct", new List() { user }, channelId, channelData, + membershipData).ConfigureAwait(false); + } + + /// + /// Creates a group conversation with multiple users. + /// + /// The list of users to include in the group conversation. + /// Optional channel ID. If not provided, a new GUID will be used. + /// Optional additional channel data. + /// Optional membership data for the conversation. + /// A ChatOperationResult containing the created channel wrapper with channel and membership information. + public async Task> CreateGroupConversation(List users, string channelId = "", + ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) + { + return await CreateConversation("group", users, channelId, channelData, + membershipData).ConfigureAwait(false); + } + + /// + /// Invites a user to a channel. + /// + /// The ID of the channel to invite the user to. + /// The ID of the user to invite. + /// A ChatOperationResult containing the created membership for the invited user. + public async Task> InviteToChannel(string channelId, string userId) + { + var result = new ChatOperationResult("Chat.InviteToChannel()", this); + //Check if already a member first + var members = await GetChannelMemberships(channelId, filter:$"uuid.id == \"{userId}\"").ConfigureAwait(false); + if (!result.RegisterOperation(members) && members.Result.Memberships.Any()) + { + //Already a member, just return current membership + result.Result = members.Result.Memberships[0]; + return result; + } + + var channel = await GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(channel)) + { + return result; + } + + var setMemberships = await PubnubInstance.SetMemberships().Uuid(userId).Include(new[] + { + PNMembershipField.CUSTOM, + PNMembershipField.TYPE, + PNMembershipField.CHANNEL, + PNMembershipField.CHANNEL_CUSTOM, + PNMembershipField.STATUS + }).Channels(new List() + { + new() + { + Channel = channelId, + //TODO: these too here? + //TODO: again, should ChatMembershipData from Create(...)Channel also be passed here? + /*Custom = , + Status = , + Type = */ + } + }).ExecuteAsync().ConfigureAwait(false); + + if (result.RegisterOperation(setMemberships)) + { + return result; + } + + var newMataData = setMemberships.Result.Memberships?.FirstOrDefault(x => x.ChannelMetadata.Channel == channelId)? + .ChannelMetadata; + if (newMataData != null) + { + channel.Result.UpdateLocalData(newMataData); + } + + var inviteEventPayload = $"{{\"channelType\": \"{channel.Result.Type}\", \"channelId\": {channelId}}}"; + await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload).ConfigureAwait(false); + + var newMembership = new Membership(this, userId, channelId, new ChatMembershipData()); + await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()).ConfigureAwait(false); + + result.Result = newMembership; + return result; + } + + /// + /// Invites multiple users to a channel. + /// + /// The ID of the channel to invite users to. + /// The list of users to invite. + /// A ChatOperationResult containing a list of created memberships for the invited users. + public async Task>> InviteMultipleToChannel(string channelId, List users) + { + var result = new ChatOperationResult>("Chat.InviteMultipleToChannel()", this) { Result = new List() }; + var channel = await GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(channel)) + { + return result; + } + var inviteResponse = await PubnubInstance.SetChannelMembers().Channel(channelId) + .Include( + new[] { + PNChannelMemberField.UUID, + PNChannelMemberField.CUSTOM, + PNChannelMemberField.UUID_CUSTOM, + PNChannelMemberField.TYPE, + PNChannelMemberField.STATUS, + PNChannelMemberField.UUID_TYPE, + PNChannelMemberField.UUID_STATUS + }) + //TODO: again, should ChatMembershipData from Create(...)Channel also be passed here? + .Uuids(users.Select(x => new PNChannelMember() { Custom = x.CustomData, Uuid = x.Id }).ToList()) + .ExecuteAsync().ConfigureAwait(false); + + if (result.RegisterOperation(inviteResponse)) + { + return result; + } + + foreach (var channelMember in inviteResponse.Result.ChannelMembers) + { + var userId = channelMember.UuidMetadata.Uuid; + if (!users.Any(x => x.Id == userId)) + { + continue; + } + var newMembership = new Membership(this, userId, channelId, channelMember); + await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()).ConfigureAwait(false); + result.Result.Add(newMembership); + + var inviteEventPayload = $"{{\"channelType\": \"{channel.Result.Type}\", \"channelId\": {channelId}}}"; + await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload).ConfigureAwait(false); + } + + await channel.Result.Refresh().ConfigureAwait(false); + + return result; + } + + /// + /// Performs an async retrieval of a Channel object with a given ID. + /// + /// ID of the channel. + /// A ChatOperationResult containing the Channel object if it exists, null otherwise. + public async Task> GetChannel(string channelId) + { + var result = new ChatOperationResult("Chat.GetChannel()", this); + var getResult = await Channel.GetChannelData(this, channelId).ConfigureAwait(false); + if (result.RegisterOperation(getResult)) + { + return result; + } + if (channelId.Contains(MESSAGE_THREAD_ID_PREFIX) + && getResult.Result.Custom.TryGetValue("parentChannelId", out var parentChannelId) + && getResult.Result.Custom.TryGetValue("parentMessageTimetoken", out var parentMessageTimeToken)) + { + result.Result = new ThreadChannel(this, channelId, parentChannelId.ToString(), parentMessageTimeToken.ToString(), getResult.Result); + } + else + { + result.Result = new Channel(this, channelId, getResult.Result); + } + return result; + } + + /// + /// Gets the list of channels with the provided parameters. + /// + /// Filter criteria for channels. + /// Sort criteria for channels. + /// The maximum number of channels to get. + /// Pagination object for retrieving specific page results. + /// A wrapper containing the list of channels and pagination information. + public async Task GetChannels(string filter = "", string sort = "", int limit = 0, + PNPageObject page = null) + { + var operation = PubnubInstance.GetAllChannelMetadata().IncludeCustom(true).IncludeCount(true).IncludeStatus(true).IncludeType(true); + if (!string.IsNullOrEmpty(filter)) + { + operation = operation.Filter(filter); + } + if (!string.IsNullOrEmpty(sort)) + { + operation = operation.Sort(new List(){sort}); + } + if (limit > 0) + { + operation = operation.Limit(limit); + } + if (page != null) + { + operation = operation.Page(page); + } + var response = await operation.ExecuteAsync().ConfigureAwait(false); + + if (response.Status.Error) + { + Logger.Error($"Error when trying to GetChannels(): {response.Status.ErrorData.Information}"); + return default; + } + var wrapper = new ChannelsResponseWrapper() + { + Channels = new List(), + Total = response.Result.TotalCount, + Page = response.Result.Page + }; + foreach (var resultMetadata in response.Result.Channels) + { + var channel = new Channel(this, resultMetadata.Channel, resultMetadata); + wrapper.Channels.Add(channel); + } + return wrapper; + } + + /// + /// Updates the channel with the provided channel ID. + /// + /// Updates the channel with the provided channel ID with the provided data. + /// + /// + /// The channel ID. + /// The updated data for the channel. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var chat = // ... + /// var result = await chat.UpdateChannel("channel_id", new ChatChannelData { + /// ChannelName = "new_name" + /// // ... + /// }); + /// + /// + /// + public async Task UpdateChannel(string channelId, ChatChannelData updatedData) + { + var result = new ChatOperationResult("Chat.UpdateChannel()", this); + result.RegisterOperation(await Channel.UpdateChannelData(this, channelId, updatedData).ConfigureAwait(false)); + return result; + } + + /// + /// Deletes the channel with the provided channel ID. + /// + /// The channel is deleted with all the messages and users. + /// + /// + /// The channel ID. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var chat = // ... + /// var result = await chat.DeleteChannel("channel_id"); + /// + /// + public async Task DeleteChannel(string channelId) + { + var result = new ChatOperationResult("Chat.DeleteChannel()", this); + result.RegisterOperation(await PubnubInstance.RemoveChannelMetadata().Channel(channelId).ExecuteAsync().ConfigureAwait(false)); + return result; + } + + #endregion + + #region Users + + internal async void StoreActivityTimeStamp() + { + var currentUserId = PubnubInstance.GetCurrentUserId(); + while (storeActivity) + { + var getResult = await User.GetUserData(this, currentUserId).ConfigureAwait(false); + var data = (ChatUserData)getResult.Result; + if (getResult.Status.Error) + { + Logger.Error($"Error when trying to store user activity timestamp: {getResult.Status.ErrorData}"); + await Task.Delay(Config.StoreUserActivityInterval).ConfigureAwait(false); + continue; + } + data.CustomData ??= new Dictionary(); + data.CustomData["lastActiveTimestamp"] = ChatUtils.TimeTokenNow(); + var setData = await User.UpdateUserData(this, currentUserId, data).ConfigureAwait(false); + if (setData.Status.Error) + { + Logger.Error($"Error when trying to store user activity timestamp: {setData.Status.ErrorData}"); + } + await Task.Delay(Config.StoreUserActivityInterval).ConfigureAwait(false); + } + } + + /// + /// Gets the current user's mentions within a specified time range. + /// + /// The start time token for the search range. + /// The end time token for the search range. + /// The maximum number of mentions to retrieve. + /// A ChatOperationResult containing the user mentions wrapper with mention data. + public async Task> GetCurrentUserMentions(string startTimeToken, string endTimeToken, + int count) + { + var result = new ChatOperationResult("Chat.GetCurrentUserMentions()", this); + var id = PubnubInstance.GetCurrentUserId(); + var getEventHistory = await GetEventsHistory(id, startTimeToken, endTimeToken, count).ConfigureAwait(false); + if (result.RegisterOperation(getEventHistory)) + { + return result; + } + var wrapper = new UserMentionsWrapper() + { + IsMore = getEventHistory.Result.IsMore, + Mentions = new List() + }; + var mentionEvents = getEventHistory.Result.Events.Where(x => x.Type == PubnubChatEventType.Mention); + foreach (var mentionEvent in mentionEvents) + { + var payloadDict = + PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(mentionEvent.Payload); + if (!payloadDict.TryGetValue("text", out var mentionText) + || !payloadDict.TryGetValue("messageTimetoken", out var messageTimeToken) + || !payloadDict.TryGetValue("channel", out var mentionChannel)) + { + continue; + } + var getMessage = await GetMessage(mentionChannel.ToString(), messageTimeToken.ToString()).ConfigureAwait(false); + if (getMessage.Error) + { + Logger.Warn($"Could not find message with ID/Timetoken from mention event. Event payload: {mentionEvent.Payload}"); + continue; + } + + var mention = new UserMentionData() + { + ChannelId = mentionChannel.ToString(), + Event = mentionEvent, + Message = getMessage.Result, + UserId = mentionEvent.UserId + }; + if (payloadDict.TryGetValue("parentChannel", out var parentChannelId)) + { + mention.ParentChannelId = parentChannelId.ToString(); + } + wrapper.Mentions.Add(mention); + } + result.Result = wrapper; + return result; + } + + /// + /// Asynchronously tries to retrieve the current User object for this chat. + /// + /// A ChatOperationResult containing the current User object if there is one, null otherwise. + public async Task> GetCurrentUser() + { + var result = new ChatOperationResult("Chat.GetCurrentUser()", this); + var userId = PubnubInstance.GetCurrentUserId(); + var getUser = await GetUser(userId).ConfigureAwait(false); + if (result.RegisterOperation(getUser)) + { + return result; + } + result.Result = getUser.Result; + return result; + } + + /// + /// Sets the restrictions for the user with the provided user ID. + /// + /// Sets the restrictions for the user with the provided user ID in the provided channel. + /// + /// + /// The user ID. + /// The channel ID. + /// The ban user flag. + /// The mute user flag. + /// The reason for the restrictions. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var result = await chat.SetRestriction("user_id", "channel_id", true, true, "Spamming"); + /// + /// + public async Task SetRestriction(string userId, string channelId, bool banUser, bool muteUser, string reason) + { + var result = new ChatOperationResult("Chat.SetRestriction()", this); + var restrictionsChannelId = $"{INTERNAL_MODERATION_PREFIX}_{channelId}"; + var getResult = await Channel.GetChannelData(this, restrictionsChannelId).ConfigureAwait(false); + if (result.RegisterOperation(getResult)) + { + if (result.RegisterOperation(await Channel.UpdateChannelData(this, restrictionsChannelId, + new ChatChannelData()).ConfigureAwait(false))) + { + return result; + } + } + var moderationEventsChannelId = INTERNAL_MODERATION_PREFIX + userId; + //Lift restrictions + if (!banUser && !muteUser) + { + if (result.RegisterOperation(await PubnubInstance.RemoveChannelMembers().Channel(restrictionsChannelId) + .Uuids(new List() { userId }).ExecuteAsync().ConfigureAwait(false))) + { + return result; + } + result.RegisterOperation(await EmitEvent(PubnubChatEventType.Moderation, moderationEventsChannelId, + $"{{\"channelId\": \"{channelId}\", \"restriction\": \"lifted\", \"reason\": \"{reason}\"}}").ConfigureAwait(false)); + return result; + } + //Ban or mute + if (result.RegisterOperation(await PubnubInstance.SetChannelMembers().Channel(restrictionsChannelId).Uuids(new List() + { + new PNChannelMember() + { + Uuid = userId, + Custom = new Dictionary() + { + { "ban", banUser }, + { "mute", muteUser }, + { "reason", reason } + } + } + }).Include(new PNChannelMemberField[] + { + PNChannelMemberField.UUID, + PNChannelMemberField.CUSTOM + }).ExecuteAsync().ConfigureAwait(false))) + { + return result; + } + result.RegisterOperation(await EmitEvent(PubnubChatEventType.Moderation, moderationEventsChannelId, + $"{{\"channelId\": \"{channelId}\", \"restriction\": \"{(banUser ? "banned" : "muted")}\", \"reason\": \"{reason}\"}}").ConfigureAwait(false)); + return result; + } + + /// + /// Sets the restrictions for the user with the provided user ID. + /// + /// Sets the restrictions for the user with the provided user ID in the provided channel. + /// + /// + /// The user ID. + /// The channel ID. + /// The Restriction object to be applied. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var result = await chat.SetRestriction("user_id", "channel_id", new Restriction(){Ban = true, Mute = true, Reason = "Spamming"}); + /// + /// + public async Task SetRestriction(string userId, string channelId, Restriction restriction) + { + return await SetRestriction(userId, channelId, restriction.Ban, restriction.Mute, restriction.Reason).ConfigureAwait(false); + } + + /// + /// Adds a listener for user update events on multiple users. + /// + /// List of user IDs to listen to. + /// The listener callback to invoke on user updates. + public async void AddListenerToUsersUpdate(List userIds, Action listener) + { + foreach (var userId in userIds) + { + var getUser = await GetUser(userId).ConfigureAwait(false); + if (!getUser.Error) + { + getUser.Result.OnUserUpdated += listener; + } + } + } + + /// + /// Creates a new user with the provided user ID. + /// + /// Creates a new user with the empty data and the provided user ID. + /// + /// + /// The user ID. + /// A ChatOperationResult containing the created User object. + /// + /// The data for user is empty. + /// + /// + /// + /// var chat = // ... + /// var result = await chat.CreateUser("user_id"); + /// var user = result.Result; + /// + /// + /// + public async Task> CreateUser(string userId) + { + return await CreateUser(userId, new ChatUserData()).ConfigureAwait(false); + } + + /// + /// Creates a new user with the provided user ID. + /// + /// Creates a new user with the provided data and the provided user ID. + /// + /// + /// The user ID. + /// The additional data for the user. + /// A ChatOperationResult containing the created User object. + /// + /// + /// var chat = // ... + /// var result = await chat.CreateUser("user_id"); + /// var user = result.Result; + /// + /// + /// + public async Task> CreateUser(string userId, ChatUserData additionalData) + { + var result = new ChatOperationResult("Chat.CreateUser()", this); + var existingUser = await GetUser(userId).ConfigureAwait(false); + if (!result.RegisterOperation(existingUser)) + { + result.Result = existingUser.Result; + return result; + } + + var update = await User.UpdateUserData(this, userId, additionalData).ConfigureAwait(false); + if (result.RegisterOperation(update)) + { + return result; + } + var user = new User(this, userId, additionalData); + result.Result = user; + return result; + } + + /// + /// Checks if the user with the provided user ID is present in the provided channel. + /// + /// Checks if the user with the provided user ID is present in the provided channel. + /// + /// + /// The user ID. + /// The channel ID. + /// A ChatOperationResult containing true if the user is present, false otherwise. + /// + /// + /// var chat = // ... + /// var result = await chat.IsPresent("user_id", "channel_id"); + /// if (result.Result) { + /// // User is present + /// } + /// + /// + /// + /// + public async Task> IsPresent(string userId, string channelId) + { + var result = new ChatOperationResult("Chat.IsPresent()", this); + var getChannel = await GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) + { + return result; + } + var isPresent = await getChannel.Result.IsUserPresent(userId).ConfigureAwait(false); + if (result.RegisterOperation(isPresent)) + { + return result; + } + result.Result = isPresent.Result; + return result; + } + + /// + /// Gets the list of users present in the provided channel. + /// + /// Gets all the users as a list of the strings present in the provided channel. + /// + /// + /// The channel ID. + /// A ChatOperationResult containing the list of user IDs present in the channel. + /// + /// + /// var chat = // ... + /// var result = await chat.WhoIsPresent("channel_id"); + /// foreach (var userId in result.Result) { + /// // User is present on the channel + /// } + /// + /// + /// + /// + public async Task>> WhoIsPresent(string channelId) + { + var result = new ChatOperationResult>("Chat.WhoIsPresent()", this) { Result = new List() }; + var getChannel = await GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) + { + return result; + } + var whoIs = await getChannel.Result.WhoIsPresent().ConfigureAwait(false); + if (result.RegisterOperation(whoIs)) + { + return result; + } + result.Result = whoIs.Result; + return result; + } + + /// + /// Gets the list of channels where the user with the provided user ID is present. + /// + /// Gets all the channels as a list of the strings where the user with the provided user ID is present. + /// + /// + /// The user ID. + /// A ChatOperationResult containing the list of channel IDs where the user is present. + /// + /// + /// var chat = // ... + /// var result = await chat.WherePresent("user_id"); + /// foreach (var channelId in result.Result) { + /// // Channel where User is present + /// }; + /// + /// + /// + /// + public async Task>> WherePresent(string userId) + { + var result = new ChatOperationResult>("Chat.WherePresent()", this) { Result = new List() }; + var getUser = await GetUser(userId).ConfigureAwait(false); + if (result.RegisterOperation(getUser)) + { + return result; + } + var wherePresent = await getUser.Result.WherePresent().ConfigureAwait(false); + if (result.RegisterOperation(wherePresent)) + { + return result; + } + result.Result = wherePresent.Result; + return result; + } + + /// + /// Asynchronously gets the user with the provided user ID. + /// + /// ID of the User to get. + /// A ChatOperationResult containing the User object if one with given ID is found, null otherwise. + public async Task> GetUser(string userId) + { + var result = new ChatOperationResult("Chat.GetUser()", this); + var getData = await User.GetUserData(this, userId).ConfigureAwait(false); + if (result.RegisterOperation(getData)) + { + return result; + } + var user = new User(this, userId, getData.Result); + result.Result = user; + return result; + } + + /// + /// Gets the list of users with the provided parameters. + /// + /// Gets all the users that matches the provided parameters. + /// + /// + /// Filter criteria for users. + /// Sort criteria for users. + /// The maximum number of users to get. + /// Pagination object for retrieving specific page results. + /// The list of the users that matches the provided parameters. + /// + /// + /// var chat = // ... + /// var users = await chat.GetUsers( + /// filter: "status == 'admin'", + /// limit: 10 + /// ); + /// foreach (var user in users.Users) { + /// // User found + /// }; + /// + /// + /// + public async Task> GetUsers(string filter = "", string sort = "", int limit = 0, + PNPageObject page = null) + { + var result = new ChatOperationResult("Chat.GetUsers()", this); + var operation = PubnubInstance.GetAllUuidMetadata().IncludeCustom(true).IncludeStatus(true).IncludeType(true); + if (!string.IsNullOrEmpty(filter)) + { + operation = operation.Filter(filter); + } + if (!string.IsNullOrEmpty(sort)) + { + operation = operation.Sort(new List(){sort}); + } + if (limit > 0) + { + operation = operation.Limit(limit); + } + if (page != null) + { + operation = operation.Page(page); + } + var getUuidMetadata = await operation.ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getUuidMetadata)) + { + return result; + } + var response = new UsersResponseWrapper() + { + Users = new List(), + Total = getUuidMetadata.Result.TotalCount, + Page = result.Result.Page + }; + foreach (var resultMetadata in getUuidMetadata.Result.Uuids) + { + var user = new User(this, resultMetadata.Uuid, resultMetadata); + response.Users.Add(user); + } + result.Result = response; + return result; + } + + /// + /// Updates the user with the provided user ID. + /// + /// Updates the user with the provided user ID with the provided data. + /// + /// + /// The user ID. + /// The updated data for the user. + /// A ChatOperationResult with information on the Update's success. + /// + /// + /// var chat = // ... + /// var result = await chat.UpdateUser("user_id", new ChatUserData { + /// Username = "new_name" + /// // ... + /// }); + /// + /// + /// + public async Task UpdateUser(string userId, ChatUserData updatedData) + { + return (await User.UpdateUserData(this, userId, updatedData).ConfigureAwait(false)).ToChatOperationResult("Chat.UpdateUser()", this); + } + + /// + /// Deletes the user with the provided user ID. + /// + /// The user is deleted with all the messages and channels. + /// + /// + /// The user ID. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var chat = // ... + /// var result = await chat.DeleteUser("user_id"); + /// + /// + public async Task DeleteUser(string userId) + { + var result = new ChatOperationResult("Chat.DeleteUser()", this); + result.RegisterOperation(await PubnubInstance.RemoveUuidMetadata().Uuid(userId).ExecuteAsync().ConfigureAwait(false)); + return result; + } + + #endregion + + #region Memberships + + /// + /// Gets the memberships of the user with the provided user ID. + /// + /// Gets all the memberships of the user with the provided user ID. + /// The memberships can be filtered, sorted, and paginated. + /// + /// + /// The user ID. + /// Filter criteria for memberships. + /// Sort criteria for memberships. + /// The maximum number of memberships to retrieve. + /// Pagination object for retrieving specific page results. + /// A ChatOperationResult containing the list of the memberships of the user. + /// + /// + /// var chat = // ... + /// var result = await chat.GetUserMemberships( + /// "user_id", + /// limit: 10 + /// ); + /// foreach (var membership in result.Result.Memberships) { + /// // Membership found + /// }; + /// + /// + /// + public async Task> GetUserMemberships(string userId, string filter = "", + string sort = "", + int limit = 0, PNPageObject page = null) + { + var result = new ChatOperationResult("Chat.GetUserMemberships()", this); + var operation = PubnubInstance.GetMemberships().Include( + new[] + { + PNMembershipField.CHANNEL_CUSTOM, + PNMembershipField.CHANNEL_TYPE, + PNMembershipField.CHANNEL_STATUS, + PNMembershipField.CUSTOM, + PNMembershipField.TYPE, + PNMembershipField.STATUS, + }).Uuid(userId); + if (!string.IsNullOrEmpty(filter)) + { + operation = operation.Filter(filter); + } + if (!string.IsNullOrEmpty(sort)) + { + operation = operation.Sort(new List() { sort }); + } + if (limit > 0) + { + operation = operation.Limit(limit); + } + if (page != null) + { + operation.Page(page); + } + var getMemberships = await operation.ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getMemberships)) + { + return result; + } + + var memberships = new List(); + foreach (var membershipResult in getMemberships.Result.Memberships) + { + memberships.Add(new Membership(this, userId, membershipResult.ChannelMetadata.Channel, new ChatMembershipData() + { + CustomData = membershipResult.Custom, + Status = membershipResult.Status, + Type = membershipResult.Type + })); + } + result.Result = new MembersResponseWrapper() + { + Memberships = memberships, + Page = getMemberships.Result.Page, + Total = getMemberships.Result.TotalCount + }; + return result; + } + + /// + /// Gets the memberships of the channel with the provided channel ID. + /// + /// Gets all the memberships of the channel with the provided channel ID. + /// The memberships can be filtered, sorted, and paginated. + /// + /// + /// The channel ID. + /// Filter criteria for memberships. + /// Sort criteria for memberships. + /// The maximum number of memberships to retrieve. + /// Pagination object for retrieving specific page results. + /// A ChatOperationResult containing the list of the memberships of the channel. + /// + /// + /// var chat = // ... + /// var result = await chat.GetChannelMemberships( + /// "channel_id", + /// limit: 10 + /// ); + /// foreach (var membership in result.Result.Memberships) { + /// // Membership found + /// }; + /// + /// + /// + public async Task> GetChannelMemberships(string channelId, string filter = "", + string sort = "", + int limit = 0, PNPageObject page = null) + { + var result = new ChatOperationResult("Chat.GetChannelMemberships()", this); + var operation = PubnubInstance.GetChannelMembers().Include( + new[] + { + PNChannelMemberField.UUID_CUSTOM, + PNChannelMemberField.UUID_TYPE, + PNChannelMemberField.UUID_STATUS, + PNChannelMemberField.CUSTOM, + PNChannelMemberField.TYPE, + PNChannelMemberField.STATUS, + }).Channel(channelId); + if (!string.IsNullOrEmpty(filter)) + { + operation = operation.Filter(filter); + } + if (!string.IsNullOrEmpty(sort)) + { + operation = operation.Sort(new List() { sort }); + } + if (limit > 0) + { + operation = operation.Limit(limit); + } + if (page != null) + { + operation = operation.Page(page); + } + + var getResult = await operation.ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getResult)) + { + return result; + } + + var memberships = new List(); + foreach (var channelMemberResult in getResult.Result.ChannelMembers) + { + memberships.Add(new Membership(this, channelMemberResult.UuidMetadata.Uuid, channelId, new ChatMembershipData() + { + CustomData = channelMemberResult.Custom, + Status = channelMemberResult.Status, + Type = channelMemberResult.Type + })); + } + result.Result = new MembersResponseWrapper() + { + Memberships = memberships, + Page = getResult.Result.Page, + Total = getResult.Result.TotalCount + }; + return result; + } + + #endregion + + #region Messages + + /// + /// Gets the message reports history for a specific channel. + /// + /// The ID of the channel to get reports for. + /// The start time token for the history range. + /// The end time token for the history range. + /// The maximum number of reports to retrieve. + /// A ChatOperationResult containing the events history wrapper with report events. + public async Task> GetMessageReportsHistory(string channelId, string startTimeToken, + string endTimeToken, int count) + { + return await GetEventsHistory($"PUBNUB_INTERNAL_MODERATION_{channelId}", startTimeToken, endTimeToken, + count).ConfigureAwait(false); + } + + /// + /// Asynchronously gets the Message object for the given timetoken. + /// + /// ID of the channel on which the message was sent. + /// TimeToken of the searched-for message. + /// A ChatOperationResult containing the Message object if one was found, null otherwise. + public async Task> GetMessage(string channelId, string messageTimeToken) + { + var result = new ChatOperationResult("Chat.GetMessage()", this); + var startTimeToken = (long.Parse(messageTimeToken) + 1).ToString(); + var getHistory = await GetChannelMessageHistory(channelId, startTimeToken, messageTimeToken, 1).ConfigureAwait(false); + if (result.RegisterOperation(getHistory)) + { + return result; + } + if (!getHistory.Result.Any()) + { + result.Error = true; + result.Exception = new PNException($"Didn't find any message with timetoken {messageTimeToken} on channel {channelId}"); + return result; + } + result.Result = getHistory.Result[0]; + return result; + } + + /// + /// Marks all messages as read for the current user across all their channels. + /// + /// Optional filter to apply when getting user memberships. + /// Optional sort criteria for memberships. + /// Maximum number of memberships to process (0-100). + /// Optional pagination object. + /// A ChatOperationResult containing the wrapper with updated memberships and status information. + public async Task> MarkAllMessagesAsRead(string filter = "", string sort = "", + int limit = 0, + PNPageObject page = null) + { + var result = new ChatOperationResult("Chat.MarkAllMessagesAsRead()", this); + if (limit < 0 || limit > 100) + { + result.Error = true; + result.Exception = new PNException("For marking messages as read limit has to be between 0 and 100"); + return result; + } + var currentUserId = PubnubInstance.GetCurrentUserId(); + var getCurrentUser = await GetCurrentUser().ConfigureAwait(false); + if (result.RegisterOperation(getCurrentUser)) + { + return result; + } + var getCurrentMemberships = await getCurrentUser.Result.GetMemberships(filter, sort, limit, page).ConfigureAwait(false); + if (result.RegisterOperation(getCurrentMemberships)) + { + return result; + } + if (getCurrentMemberships.Result.Memberships == null || !getCurrentMemberships.Result.Memberships.Any()) + { + result.Result = new MarkMessagesAsReadWrapper() + { + Memberships = new List() + }; + return result; + } + var timeToken = ChatUtils.TimeTokenNow(); + var memberships = getCurrentMemberships.Result.Memberships; + foreach (var membership in memberships) + { + membership.MembershipData.CustomData ??= new(); + membership.MembershipData.CustomData["lastReadMessageTimetoken"] = timeToken; + } + if (result.RegisterOperation(await Membership.UpdateMembershipsData(this, currentUserId, memberships).ConfigureAwait(false))) + { + return result; + } + foreach (var membership in memberships) + { + await EmitEvent(PubnubChatEventType.Receipt, membership.ChannelId, + $"{{\"messageTimetoken\": \"{timeToken}\"}}").ConfigureAwait(false); + } + result.Result = new MarkMessagesAsReadWrapper() + { + Memberships = memberships, + Page = getCurrentMemberships.Result.Page, + Status = getCurrentMemberships.Result.Status, + Total = getCurrentMemberships.Result.Total + }; + return result; + } + + /// + /// Gets unread message counts for the current user's channels. + /// + /// Optional filter to apply when getting user memberships. + /// Optional sort criteria for memberships. + /// Maximum number of memberships to process (0-100). + /// Optional pagination object. + /// A ChatOperationResult containing a list of unread message wrappers with count information per channel. + public async Task>> GetUnreadMessagesCounts(string filter = "", string sort = "", + int limit = 0, + PNPageObject page = null) + { + var result = new ChatOperationResult>("Chat.GetUnreadMessagesCounts()", this); + if (limit < 0 || limit > 100) + { + result.Error = true; + result.Exception = new PNException("For getting message counts limit has to be between 0 and 100"); + return result; + } + var getCurrentUser = await GetCurrentUser().ConfigureAwait(false); + if (result.RegisterOperation(getCurrentUser)) + { + return result; + } + var getCurrentMemberships = await getCurrentUser.Result.GetMemberships(filter, sort, limit, page).ConfigureAwait(false); + if (result.RegisterOperation(getCurrentMemberships)) + { + return result; + } + if (getCurrentMemberships.Result.Memberships == null || !getCurrentMemberships.Result.Memberships.Any()) + { + result.Result = new List(); + return result; + } + var memberships = getCurrentMemberships.Result.Memberships; + var channelIds = new List(); + var timeTokens = new List(); + foreach (var membership in memberships) + { + channelIds.Add(membership.ChannelId); + var lastRead = string.IsNullOrEmpty(membership.LastReadMessageTimeToken) ? Membership.EMPTY_TIMETOKEN : long.Parse(membership.LastReadMessageTimeToken); + timeTokens.Add(lastRead); + } + var getCounts = await PubnubInstance.MessageCounts().Channels(channelIds.ToArray()).ChannelsTimetoken(timeTokens.ToArray()) + .ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getCounts)) + { + return result; + } + var wrapperList = new List(); + foreach (var channelMessagesCount in getCounts.Result.Channels) + { + wrapperList.Add(new UnreadMessageWrapper() + { + ChannelId = channelMessagesCount.Key, + Count = Convert.ToInt32(channelMessagesCount.Value), + Membership = memberships.First(x => x.ChannelId == channelMessagesCount.Key) + }); + } + result.Result = wrapperList; + return result; + } + + /// + /// Creates a thread channel for a specific message. + /// + /// The time token of the message to create a thread for. + /// The ID of the channel where the message was sent. + /// A ChatOperationResult containing the created ThreadChannel. + public async Task> CreateThreadChannel(string messageTimeToken, string messageChannelId) + { + var result = new ChatOperationResult("Chat.CreateThreadChannel()", this); + var getMessage = await GetMessage(messageChannelId, messageTimeToken).ConfigureAwait(false); + if (result.RegisterOperation(getMessage)) + { + return result; + } + var createThread = getMessage.Result.CreateThread(); + if (result.RegisterOperation(createThread)) + { + return result; + } + result.Result = createThread.Result; + return result; + } + + /// + /// Removes a thread channel associated with a specific message. + /// + /// The time token of the message whose thread to remove. + /// The ID of the channel where the message was sent. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task RemoveThreadChannel(string messageTimeToken, string messageChannelId) + { + var result = new ChatOperationResult("Chat.RemoveThreadChannel()", this); + var getMessage = await GetMessage(messageChannelId, messageTimeToken).ConfigureAwait(false); + if (result.RegisterOperation(getMessage)) + { + return result; + } + result.RegisterOperation(await getMessage.Result.RemoveThread().ConfigureAwait(false)); + return result; + } + + /// + /// Asynchronously tries to retrieve a ThreadChannel object from a Message object if there is one. + /// + /// Message on which the ThreadChannel is supposed to be. + /// A ChatOperationResult containing the ThreadChannel object if one was found, null otherwise. + public async Task> GetThreadChannel(Message message) + { + var result = new ChatOperationResult("Chat.GetThreadChannel()", this); + var getChannel = await GetChannel(message.GetThreadId()).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) + { + return result; + } + if (getChannel.Result is not ThreadChannel threadChannel) + { + result.Error = true; + result.Exception = new PNException("Retrieved channel wasn't a thread channel"); + return result; + } + result.Result = threadChannel; + return result; + } + + /// + /// Forwards a message to a different channel. + /// + /// The time token of the message to forward. + /// The ID of the channel to forward the message to. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task ForwardMessage(string messageTimeToken, string channelId) + { + var result = new ChatOperationResult("Chat.ForwardMessage()", this); + var getMessage = await GetMessage(channelId, messageTimeToken).ConfigureAwait(false); + if (result.RegisterOperation(getMessage)) + { + return result; + } + result.RegisterOperation(await getMessage.Result.Forward(channelId).ConfigureAwait(false)); + return result; + } + + /// + /// Adds a listener for message update events on specific messages in a channel. + /// + /// The ID of the channel containing the messages. + /// List of message time tokens to listen to for updates. + /// The listener callback to invoke on message updates. + public async void AddListenerToMessagesUpdate(string channelId, List messageTimeTokens, + Action listener) + { + foreach (var messageTimeToken in messageTimeTokens) + { + var getMessage = await GetMessage(channelId, messageTimeToken).ConfigureAwait(false); + if (!getMessage.Error) + { + getMessage.Result.OnMessageUpdated += listener; + } + } + } + + /// + /// Pins a message to a channel. + /// + /// The ID of the channel to pin the message to. + /// The message to pin. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task PinMessageToChannel(string channelId, Message message) + { + var result = new ChatOperationResult("Chat.PinMessageToChannel()", this); + var getChannel = await GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) + { + return result; + } + var pin = await getChannel.Result.PinMessage(message).ConfigureAwait(false); + result.RegisterOperation(pin); + return result; + } + + /// + /// Unpins the currently pinned message from a channel. + /// + /// The ID of the channel to unpin the message from. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task UnpinMessageFromChannel(string channelId) + { + var result = new ChatOperationResult("Chat.UnPinMessageFromChannel()", this); + var getChannel = await GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) + { + return result; + } + var unpin = await getChannel.Result.UnpinMessage().ConfigureAwait(false); + result.RegisterOperation(unpin); + return result; + } + + /// + /// Gets the channel message history. + /// + /// Gets the list of the messages that were sent in the channel with the provided parameters. + /// The history is limited by the provided count of messages, start time token, and end time token. + /// + /// + /// The channel ID. + /// The start time token of the messages. + /// The end time token of the messages. + /// The maximum amount of the messages. + /// A ChatOperationResult containing the list of messages that were sent in the channel. + /// + /// + /// var chat = // ... + /// var result = await chat.GetChannelMessageHistory("channel_id", "start_time_token", "end_time_token", 10); + /// foreach (var message in result.Result) { + /// // Message found + /// }; + /// + /// + /// + public async Task>> GetChannelMessageHistory(string channelId, string startTimeToken, + string endTimeToken, + int count) + { + var result = new ChatOperationResult>("Chat.GetChannelMessageHistory()", this) + { + Result = new List() + }; + var getHistory = await PubnubInstance.FetchHistory().Channels(new[] { channelId }) + .Start(long.Parse(startTimeToken)).End(long.Parse(endTimeToken)).MaximumPerChannel(count).IncludeMessageActions(true) + .IncludeMeta(true).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getHistory) || getHistory.Result.Messages == null || !getHistory.Result.Messages.ContainsKey(channelId)) + { + return result; + } + + //TODO: should be in "MessageHistoryWrapper" object? + var isMore = getHistory.Result.More != null; + foreach (var historyItem in getHistory.Result.Messages[channelId]) + { + if (ChatParsers.TryParseMessageFromHistory(this, channelId, historyItem, out var message)) + { + result.Result.Add(message); + } + } + return result; + } + + #endregion + + #region Events + + internal void BroadcastAnyEvent(ChatEvent chatEvent) + { + OnAnyEvent?.Invoke(chatEvent); + } + + /// + /// Gets the events history for a specific channel within a time range. + /// + /// The ID of the channel to get events for. + /// The start time token for the history range. + /// The end time token for the history range. + /// The maximum number of events to retrieve. + /// A ChatOperationResult containing the events history wrapper with chat events. + public async Task> GetEventsHistory(string channelId, string startTimeToken, + string endTimeToken, + int count) + { + var result = new ChatOperationResult("Chat.GetEventsHistory()", this) + { + Result = new EventsHistoryWrapper() + { + Events = new List() + } + }; + var getHistory = await PubnubInstance.FetchHistory().Channels(new[] { channelId }) + .Start(long.Parse(startTimeToken)).End(long.Parse(endTimeToken)).MaximumPerChannel(count) + .ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getHistory) || !getHistory.Result.Messages.ContainsKey(channelId)) + { + return result; + } + + var isMore = getHistory.Result.More != null; + var events = new List(); + foreach (var message in getHistory.Result.Messages[channelId]) + { + if (ChatParsers.TryParseEventFromHistory(this, channelId, message, out var chatEvent)) + { + events.Add(chatEvent); + } + } + result.Result = new EventsHistoryWrapper() + { + Events = events, + IsMore = isMore + }; + return result; + } + + /// + /// Emits a chat event on the specified channel. + /// + /// The type of event to emit. + /// The channel ID where to emit the event. + /// The JSON payload of the event. + /// A ChatOperationResult indicating the success or failure of the operation. + public async Task EmitEvent(PubnubChatEventType type, string channelId, string jsonPayload) + { + var result = new ChatOperationResult("Chat.EmitEvent()", this); + jsonPayload = jsonPayload.Remove(0, 1); + jsonPayload = jsonPayload.Remove(jsonPayload.Length - 1); + var fullPayload = $"{{{jsonPayload}, \"type\": \"{ChatEnumConverters.ChatEventTypeToString(type)}\"}}"; + var emitOperation = PubnubInstance.Publish().Channel(channelId).Message(fullPayload); + if (type is PubnubChatEventType.Receipt or PubnubChatEventType.Typing) + { + emitOperation.ShouldStore(false); + } + result.RegisterOperation(await emitOperation.ExecuteAsync().ConfigureAwait(false)); + return result; + } + + #endregion + + /// + /// Destroys the chat instance and cleans up resources. + /// + /// Stops user activity tracking, destroys the PubNub instance, and disposes the rate limiter. + /// + /// + public void Destroy() + { + storeActivity = false; + PubnubInstance.Destroy(); + RateLimiter.Dispose(); + } + + ~Chat() + { + Destroy(); + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs.meta new file mode 100644 index 0000000..311d94b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d23c8c643efe4034b8c283acf081403a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ChatAccessManager.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ChatAccessManager.cs new file mode 100644 index 0000000..15211e3 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ChatAccessManager.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; + +namespace PubnubChatApi +{ + public class ChatAccessManager + { + + private Chat chat; + + internal ChatAccessManager(Chat chat) + { + this.chat = chat; + } + + public async Task CanI(PubnubAccessPermission permission, PubnubAccessResourceType resourceType, string resourceName) + { + var parsed = chat.PubnubInstance.ParseToken(chat.PubnubInstance.PNConfig.AuthKey); + Dictionary mapping = resourceType switch + { + PubnubAccessResourceType.Uuids => parsed.Resources.Uuids, + PubnubAccessResourceType.Channels => parsed.Resources.Channels, + _ => throw new ArgumentOutOfRangeException(nameof(resourceType), resourceType, null) + }; + var authValues = mapping[resourceName]; + switch (permission) + { + case PubnubAccessPermission.Read: + return authValues.Read; + case PubnubAccessPermission.Write: + return authValues.Write; + case PubnubAccessPermission.Manage: + return authValues.Manage; + case PubnubAccessPermission.Delete: + return authValues.Delete; + case PubnubAccessPermission.Get: + return authValues.Get; + case PubnubAccessPermission.Join: + return authValues.Join; + case PubnubAccessPermission.Update: + return authValues.Update; + default: + throw new ArgumentOutOfRangeException(nameof(permission), permission, null); + } + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ChatAccessManager.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ChatAccessManager.cs.meta new file mode 100644 index 0000000..150ca5c --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ChatAccessManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bf6a2a76177e6424b9da947075c699cb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data.meta new file mode 100644 index 0000000..ca3d461 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ab7523594d795e74dbba14517a890a13 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs new file mode 100644 index 0000000..e6c71ba --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi +{ + public struct ChannelsResponseWrapper + { + public List Channels; + public PNPageObject Page; + public int Total; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs.meta new file mode 100644 index 0000000..e210c83 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 18820f966b1188648b16a7535cadab3e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs new file mode 100644 index 0000000..b501a56 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi +{ + public class ChannelsRestrictionsWrapper + { + public List Restrictions = new (); + public PNPageObject Page = new (); + public int Total; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs.meta new file mode 100644 index 0000000..127b596 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8d81f1b96973b55429089e184e33eb31 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatChannelData.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatChannelData.cs new file mode 100644 index 0000000..48babc9 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatChannelData.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi +{ + /// + /// Data class for the chat channel. + /// + /// Contains all the data related to the chat channel. + /// + /// + /// + /// By default, all the properties are set to empty strings. + /// + public class ChatChannelData + { + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public Dictionary CustomData { get; set; } = new (); + public string Updated { get; internal set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + + public static implicit operator ChatChannelData(PNChannelMetadataResult metadataResult) + { + return new ChatChannelData() + { + Name = metadataResult.Name, + Description = metadataResult.Description, + CustomData = metadataResult.Custom, + Status = metadataResult.Status, + Updated = metadataResult.Updated, + Type = metadataResult.Type + }; + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatChannelData.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatChannelData.cs.meta new file mode 100644 index 0000000..03a4921 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatChannelData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7bcd12877e1458443b0c806844b62008 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatMembershipData.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatMembershipData.cs new file mode 100644 index 0000000..4f2b629 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatMembershipData.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi +{ + /// + /// Data class for a chat membership. + /// + /// Contains all the additional data related to the chat membership. + /// + /// + /// + /// By default, all the properties are set to empty strings. + /// + public class ChatMembershipData + { + public Dictionary CustomData { get; set; } = new(); + public string Status { get; set; } + public string Type { get; set; } + + public static implicit operator ChatMembershipData(PNChannelMembersItemResult membersItem) + { + return new ChatMembershipData() + { + CustomData = membersItem.Custom, + Status = membersItem.Status, + Type = membersItem.Type + }; + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatMembershipData.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatMembershipData.cs.meta new file mode 100644 index 0000000..e77cb10 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatMembershipData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 43ff0732605b6234d81b7cf47779eda2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatOperationResult.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatOperationResult.cs new file mode 100644 index 0000000..cd4a001 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatOperationResult.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi +{ + public class ChatOperationResult + { + public bool Error { get; internal set; } + public List InternalStatuses { get; internal set; } = new(); + public Exception Exception { get; internal set; } + + internal string OperationName { get; } + protected Chat chat; + + internal ChatOperationResult(string operationName, Chat chat) + { + OperationName = operationName; + this.chat = chat; + } + + /// + /// Registers a single PNResult to this overall Chat Operation Result. + /// Returns pubnubResult.Status.Error + /// + internal bool RegisterOperation(PNResult pubnubResult) + { + InternalStatuses.Add(pubnubResult.Status); + chat.Logger.Debug($"Chat operation \"{OperationName}\" registered PN Status: {chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(pubnubResult.Status)}"); + Error = pubnubResult.Status.Error; + if (Error) + { + chat.Logger.Debug($"Chat operation \"{OperationName}\" registered PN Status with error: {pubnubResult.Status.ErrorData.Information}"); + Exception = pubnubResult.Status.ErrorData.Throwable; + } + return Error; + } + + /// + /// Registers another ChatOperationResult to this ChatOperationResult. + /// Returns otherChatResult.Error + /// + internal bool RegisterOperation(ChatOperationResult otherChatResult) + { + foreach (var status in otherChatResult.InternalStatuses) + { + chat.Logger.Debug($"Chat operation \"{OperationName}\" registered PN Status from operation \"{otherChatResult.OperationName}\": {chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(status)}"); + InternalStatuses.Add(status); + + } + if (otherChatResult.Error) + { + chat.Logger.Debug($"Chat operation \"{OperationName}\" registered PN Status from operation \"{otherChatResult.OperationName}\" with error: {otherChatResult.Exception.Message}"); + } + Exception = otherChatResult.Exception; + Error = otherChatResult.Error; + return Error; + } + } + + public class ChatOperationResult : ChatOperationResult + { + internal ChatOperationResult(string operationName, Chat chat) : base(operationName, chat) + { + } + + public T Result { get; internal set; } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatOperationResult.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatOperationResult.cs.meta new file mode 100644 index 0000000..51db597 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatOperationResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1b46efed0a3994e4da30c569cc8eae5a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatUserData.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatUserData.cs new file mode 100644 index 0000000..774952e --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatUserData.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi +{ + /// + /// Data class for the chat user. + /// + /// Contains all the data related to the chat user. + /// + /// + /// + /// By default, all the properties are set to empty strings. + /// + public class ChatUserData + { + public string Username { get; set; } = string.Empty; + public string ExternalId { get; set; } = string.Empty; + public string ProfileUrl { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public Dictionary CustomData { get; set; } = new (); + public string Status { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + + public static implicit operator ChatUserData(PNUuidMetadataResult metadataResult) + { + return new ChatUserData() + { + ExternalId = metadataResult.ExternalId, + Email = metadataResult.Email, + ProfileUrl = metadataResult.ProfileUrl, + Username = metadataResult.Name, + Status = metadataResult.Status, + Type = metadataResult.Type, + CustomData = metadataResult.Custom + }; + } + + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatUserData.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatUserData.cs.meta new file mode 100644 index 0000000..cb08608 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatUserData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d05e8d74739fd0b49be010631fddf2a8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs new file mode 100644 index 0000000..8ca515b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace PubnubChatApi +{ + public class CreatedChannelWrapper + { + public Channel CreatedChannel; + public Membership HostMembership; + public List InviteesMemberships; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs.meta new file mode 100644 index 0000000..6edb4bb --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7f3a95f85c597ab469540d486d08b8e8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/EventsHistoryWrapper.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/EventsHistoryWrapper.cs new file mode 100644 index 0000000..3af98d2 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/EventsHistoryWrapper.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace PubnubChatApi +{ + public struct EventsHistoryWrapper + { + public List Events; + public bool IsMore; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/EventsHistoryWrapper.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/EventsHistoryWrapper.cs.meta new file mode 100644 index 0000000..0bd1c96 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/EventsHistoryWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 553a8d1ca2ca3fa4d8ec04d8cc3e1c3a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs new file mode 100644 index 0000000..2b3bcb7 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi +{ + public struct MarkMessagesAsReadWrapper + { + public PNPageObject Page; + public int Total; + public string Status; + public List Memberships; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs.meta new file mode 100644 index 0000000..0e193dd --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: df10985a1b7f4364782a50dee2537fbf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs new file mode 100644 index 0000000..54357fb --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi +{ + public class MembersResponseWrapper + { + public List Memberships = new (); + public PNPageObject Page = new (); + public int Total; + public string Status; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs.meta new file mode 100644 index 0000000..2c80cd0 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 446442ac11faf82409cf2f5be99757fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MentionedUser.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MentionedUser.cs new file mode 100644 index 0000000..e2446b0 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MentionedUser.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace PubnubChatApi +{ + public struct MentionedUser + { + [JsonProperty("id")] + public string Id; + [JsonProperty("name")] + public string Name; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MentionedUser.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MentionedUser.cs.meta new file mode 100644 index 0000000..6814e36 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MentionedUser.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 832e381621f66f44aa4c8d564cf59a9e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MessageAction.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MessageAction.cs new file mode 100644 index 0000000..26e9357 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MessageAction.cs @@ -0,0 +1,10 @@ +namespace PubnubChatApi +{ + public struct MessageAction + { + public PubnubMessageActionType Type; + public string Value; + public string TimeToken; + public string UserId; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MessageAction.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MessageAction.cs.meta new file mode 100644 index 0000000..9a0fa23 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MessageAction.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1fdc7a8ea2210084e889ea3100a6a199 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/PubnubChatConfig.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/PubnubChatConfig.cs new file mode 100644 index 0000000..1bb27e1 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/PubnubChatConfig.cs @@ -0,0 +1,35 @@ +using PubnubApi; + +namespace PubnubChatApi +{ + public class PubnubChatConfig + { + [System.Serializable] + public class RateLimitPerChannel + { + public int DirectConversation; + public int GroupConversation; + public int PublicConversation; + public int UnknownConversation; + } + + public int TypingTimeout { get; } + public int TypingTimeoutDifference { get; } + public int RateLimitFactor { get; } + public RateLimitPerChannel RateLimitsPerChannel { get; } + public bool StoreUserActivityTimestamp { get; } + public int StoreUserActivityInterval { get; } + + public PubnubChatConfig(int typingTimeout = 5000, int typingTimeoutDifference = 1000, int rateLimitFactor = 2, + RateLimitPerChannel rateLimitPerChannel = null, bool storeUserActivityTimestamp = false, + int storeUserActivityInterval = 60000) + { + RateLimitsPerChannel = rateLimitPerChannel ?? new RateLimitPerChannel(); + RateLimitFactor = rateLimitFactor; + StoreUserActivityTimestamp = storeUserActivityTimestamp; + StoreUserActivityInterval = storeUserActivityInterval; + TypingTimeout = typingTimeout; + TypingTimeoutDifference = typingTimeoutDifference; + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/PubnubChatConfig.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/PubnubChatConfig.cs.meta new file mode 100644 index 0000000..52e3d15 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/PubnubChatConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 19628923b9ef0f74b9a266014db16a97 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ReferencedChannel.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ReferencedChannel.cs new file mode 100644 index 0000000..0834889 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ReferencedChannel.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace PubnubChatApi +{ + public struct ReferencedChannel + { + [JsonProperty("id")] + public string Id; + [JsonProperty("name")] + public string Name; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ReferencedChannel.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ReferencedChannel.cs.meta new file mode 100644 index 0000000..042970c --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ReferencedChannel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 206d82799af58664aaa0fdd9099c228c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/Restriction.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/Restriction.cs new file mode 100644 index 0000000..ebacc6b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/Restriction.cs @@ -0,0 +1,22 @@ +namespace PubnubChatApi +{ + /// + /// Data struct for restriction. + /// + public class Restriction + { + public bool Ban; + public bool Mute; + public string Reason = string.Empty; + } + + public class UserRestriction : Restriction + { + public string UserId = string.Empty; + } + + public class ChannelRestriction : Restriction + { + public string ChannelId = string.Empty; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/Restriction.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/Restriction.cs.meta new file mode 100644 index 0000000..c10164a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/Restriction.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5615d40db96a4174883d34a53cd93428 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/SendTextParams.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/SendTextParams.cs new file mode 100644 index 0000000..abf4951 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/SendTextParams.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace PubnubChatApi +{ + public class SendTextParams + { + public bool StoreInHistory = true; + public bool SendByPost = false; + public Dictionary Meta = new(); + public Dictionary MentionedUsers = new(); + public Message QuotedMessage = null; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/SendTextParams.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/SendTextParams.cs.meta new file mode 100644 index 0000000..a822ea8 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/SendTextParams.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 54e6089c1fc106b4ab3a3980ce3eb827 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/TextLink.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/TextLink.cs new file mode 100644 index 0000000..8d819c1 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/TextLink.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace PubnubChatApi +{ + public class TextLink + { + [JsonProperty("start_index")] + public int StartIndex; + [JsonProperty("end_index")] + public int EndIndex; + [JsonProperty("link")] + public string Link = string.Empty; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/TextLink.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/TextLink.cs.meta new file mode 100644 index 0000000..c7fa9d1 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/TextLink.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e260f9bba821eb94e9894cee4d9c78cb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs new file mode 100644 index 0000000..599d081 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs @@ -0,0 +1,9 @@ +namespace PubnubChatApi +{ + public struct UnreadMessageWrapper + { + public string ChannelId; + public Membership Membership; + public int Count; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs.meta new file mode 100644 index 0000000..632bdb4 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cabc39e56033b34498eca5b9d3dbfde3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionData.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionData.cs new file mode 100644 index 0000000..4df4b2f --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionData.cs @@ -0,0 +1,11 @@ +namespace PubnubChatApi +{ + public class UserMentionData + { + public string ChannelId; + public string ParentChannelId; + public string UserId; + public ChatEvent Event; + public Message Message; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionData.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionData.cs.meta new file mode 100644 index 0000000..dd3d42d --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 42299b66424d2094dacb2305ff687891 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs new file mode 100644 index 0000000..5568745 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace PubnubChatApi +{ + public class UserMentionsWrapper + { + public List Mentions = new(); + public bool IsMore; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs.meta new file mode 100644 index 0000000..c5e9dc8 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 027b6f7d831b6b44dbc7123f70a8da61 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs new file mode 100644 index 0000000..88b1525 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi +{ + public struct UsersResponseWrapper + { + public List Users; + public PNPageObject Page; + public int Total; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs.meta new file mode 100644 index 0000000..b98fcdd --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 751738a961e19ac4da0f4c51efc02bd3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersRestrictionsWrapper.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersRestrictionsWrapper.cs new file mode 100644 index 0000000..cc54980 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersRestrictionsWrapper.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi +{ + public class UsersRestrictionsWrapper + { + public List Restrictions = new (); + public PNPageObject Page = new (); + public int Total; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersRestrictionsWrapper.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersRestrictionsWrapper.cs.meta new file mode 100644 index 0000000..cf8600f --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersRestrictionsWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 115d1365f8e31e94cbefd34b949ca5d8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Events.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Events.meta new file mode 100644 index 0000000..55ecd80 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Events.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d548fe7845a66d44d867f858f56c1f8d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Events/ChatEvent.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Events/ChatEvent.cs new file mode 100644 index 0000000..8d85bfa --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Events/ChatEvent.cs @@ -0,0 +1,11 @@ +namespace PubnubChatApi +{ + public struct ChatEvent + { + public string TimeToken; + public PubnubChatEventType Type; + public string ChannelId; + public string UserId; + public string Payload; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Events/ChatEvent.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Events/ChatEvent.cs.meta new file mode 100644 index 0000000..6e7c117 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Events/ChatEvent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 03255aea2de03b2478c4b1c75ba6a112 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Membership.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Membership.cs new file mode 100644 index 0000000..2f9e1d6 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Membership.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; + +namespace PubnubChatApi +{ + /// + /// Represents a membership of a user in a channel. + /// + /// Memberships are relations between users and channels. They are used to determine + /// which users are allowed to send messages to which channels. + /// + /// + /// + /// Memberships are created when a user joins a channel and are deleted when a user leaves a channel. + /// + /// + /// + /// + public class Membership : UniqueChatEntity + { + //Message counts requires a valid timetoken, so this one will be like "0", from beginning of the channel + internal const long EMPTY_TIMETOKEN = 17000000000000000; + + /// + /// The user ID of the user that this membership belongs to. + /// + public string UserId { get; } + + /// + /// The channel ID of the channel that this membership belongs to. + /// + public string ChannelId { get; } + + /// + /// The string time token of last read message on the membership channel. + /// + public string LastReadMessageTimeToken => MembershipData.CustomData != null && MembershipData.CustomData.TryGetValue("lastReadMessageTimetoken", out var timeToken) ? timeToken.ToString() : ""; + + public ChatMembershipData MembershipData { get; private set; } + + /// + /// Event that is triggered when the membership is updated. + /// + /// This event is triggered when the membership is updated by the server. + /// Every time the membership is updated, this event is triggered. + /// + /// + /// + /// + /// membership.OnMembershipUpdated += (membership) => + /// { + /// Console.WriteLine("Membership updated!"); + /// }; + /// + /// + /// + public event Action OnMembershipUpdated; + + protected override string UpdateChannelId => ChannelId; + + internal Membership(Chat chat, string userId, string channelId, ChatMembershipData membershipData) : base(chat, userId+channelId) + { + UserId = userId; + ChannelId = channelId; + UpdateLocalData(membershipData); + } + + internal void UpdateLocalData(ChatMembershipData newData) + { + MembershipData = newData; + } + + protected override SubscribeCallback CreateUpdateListener() + { + return chat.ListenerFactory.ProduceListener(objectEventCallback: delegate(Pubnub pn, PNObjectEventResult e) + { + if (ChatParsers.TryParseMembershipUpdate(chat, this, e, out var updatedData)) + { + UpdateLocalData(updatedData); + OnMembershipUpdated?.Invoke(this); + } + }); + } + + /// + /// Updates the membership with a ChatMembershipData object. + /// + /// This method updates the membership with a ChatMembershipData object. This object can be used to store + /// additional information about the membership. + /// + /// + /// The ChatMembershipData object to update the membership with. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + public async Task Update(ChatMembershipData membershipData) + { + var result = (await UpdateMembershipData(membershipData).ConfigureAwait(false)).ToChatOperationResult("Membership.Update()", chat); + if (!result.Error) + { + UpdateLocalData(membershipData); + } + return result; + } + + internal async Task> UpdateMembershipData(ChatMembershipData membershipData) + { + return await chat.PubnubInstance.ManageMemberships().Uuid(UserId).Set(new List() + { + new() + { + Channel = ChannelId, + Custom = membershipData.CustomData, + Status = membershipData.Status, + Type = membershipData.Type + } + }).Include(new[] + { + PNMembershipField.TYPE, + PNMembershipField.CUSTOM, + PNMembershipField.STATUS, + PNMembershipField.CHANNEL, + PNMembershipField.CHANNEL_CUSTOM, + PNMembershipField.CHANNEL_TYPE, + PNMembershipField.CHANNEL_STATUS, + }).ExecuteAsync().ConfigureAwait(false); + } + + internal static async Task> UpdateMembershipsData(Chat chat, string userId, List memberships) + { + var pnMemberships = memberships.Select(membership => new PNMembership() + { + Channel = membership.ChannelId, + Custom = membership.MembershipData.CustomData, + Status = membership.MembershipData.Status, + Type = membership.MembershipData.Type + }).ToList(); + return await chat.PubnubInstance.SetMemberships().Uuid(userId).Channels(pnMemberships).Include(new[] + { + PNMembershipField.TYPE, + PNMembershipField.CUSTOM, + PNMembershipField.STATUS, + PNMembershipField.CHANNEL, + PNMembershipField.CHANNEL_CUSTOM, + PNMembershipField.CHANNEL_TYPE, + PNMembershipField.CHANNEL_STATUS + }).ExecuteAsync().ConfigureAwait(false); + } + + /// + /// Sets the last read message for this membership. + /// + /// Updates the membership to mark the specified message as the last one read by the user. + /// This is used for tracking read receipts and unread message counts. + /// + /// + /// The message to mark as last read. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + public async Task SetLastReadMessage(Message message) + { + return await SetLastReadMessageTimeToken(message.TimeToken).ConfigureAwait(false); + } + + /// + /// Sets the last read message time token for this membership. + /// + /// Updates the membership to mark the message with the specified time token as the last one read by the user. + /// This is used for tracking read receipts and unread message counts. + /// + /// + /// The time token of the message to mark as last read. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + public async Task SetLastReadMessageTimeToken(string timeToken) + { + var result = new ChatOperationResult("Membership.SetLastReadMessageTimeToken()", chat); + MembershipData.CustomData ??= new Dictionary(); + MembershipData.CustomData["lastReadMessageTimetoken"] = timeToken; + var update = await UpdateMembershipData(MembershipData).ConfigureAwait(false); + if (result.RegisterOperation(update)) + { + return result; + } + result.RegisterOperation(await chat.EmitEvent(PubnubChatEventType.Receipt, ChannelId, + $"{{\"messageTimetoken\": \"{timeToken}\"}}").ConfigureAwait(false)); + return result; + } + + /// + /// Gets the count of unread messages for this membership. + /// + /// Calculates the number of messages that have been sent to the channel since the last read message time token. + /// + /// + /// A ChatOperationResult with the number of unread messages + /// + /// + public async Task> GetUnreadMessagesCount() + { + var result = new ChatOperationResult("Membership.GetUnreadMessagesCount()", chat); + if (!long.TryParse(LastReadMessageTimeToken, out var lastRead)) + { + result.Error = true; + result.Exception = new PNException("LastReadMessageTimeToken is not a valid time token!"); + return result; + } + lastRead = lastRead == 0 ? EMPTY_TIMETOKEN : lastRead; + var countsResponse = await chat.PubnubInstance.MessageCounts().Channels(new[] { ChannelId }) + .ChannelsTimetoken(new[] { lastRead }).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(countsResponse)) + { + return result; + } + result.Result = countsResponse.Result.Channels[ChannelId]; + return result; + } + + /// + /// Refreshes the membership data from the server. + /// + /// Fetches the latest membership information from the server and updates the local data. + /// This is useful when you want to ensure you have the most up-to-date membership information. + /// + /// + /// A ChatOperationResult indicating the success or failure of the refresh operation. + public override async Task Refresh() + { + return await chat.GetChannelMemberships(ChannelId, filter:$"uuid.id == \"{UserId}\"").ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Membership.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Membership.cs.meta new file mode 100644 index 0000000..4e878f7 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Membership.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d216e3a523f02f342ac99f6df9c2b8fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs new file mode 100644 index 0000000..9a33367 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs @@ -0,0 +1,754 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; + +namespace PubnubChatApi +{ + /// + /// Represents a message in a chat channel. + /// + /// Messages are sent by users to chat channels. They can contain text + /// and other data, such as metadata or message actions. + /// + /// + /// + /// + public class Message : UniqueChatEntity + { + /// + /// The text content of the message. + /// + /// This is the main content of the message. It can be any text that the user wants to send. + /// + /// + public string MessageText { + get + { + var edits = MessageActions.Where(x => x.Type == PubnubMessageActionType.Edited).ToList(); + return edits.Any() ? edits[0].Value : OriginalMessageText; + } + } + + /// + /// The original, un-edited text of the message. + /// + public string OriginalMessageText { get; internal set; } + + /// + /// The time token of the message. + /// + /// The time token is a unique identifier for the message. + /// It is used to identify the message in the chat. + /// + /// + public string TimeToken { get; internal set; } + + /// + /// The channel ID of the channel that the message belongs to. + /// + /// This is the ID of the channel that the message was sent to. + /// + /// + public string ChannelId { get; internal set; } + + /// + /// The user ID of the user that sent the message. + /// + /// This is the unique ID of the user that sent the message. + /// Do not confuse this with the username of the user. + /// + /// + public string UserId { get; internal set; } + + /// + /// The metadata of the message. + /// + /// The metadata is additional data that can be attached to the message. + /// It can be used to store additional information about the message. + /// + /// + public Dictionary Meta { get; internal set; } = new (); + + /// + /// Whether the message has been deleted. + /// + /// This property indicates whether the message has been deleted. + /// If the message has been deleted, this property will be true. + /// It means that all the deletions are soft deletions. + /// + /// + public bool IsDeleted => MessageActions.Any(x => x.Type == PubnubMessageActionType.Deleted); + + /// + /// Gets the list of users mentioned in this message. + /// + /// Extracts user mentions from the message metadata and returns them as a list of MentionedUser objects. + /// + /// + /// A list of users that were mentioned in this message. + public List MentionedUsers { + get + { + var mentioned = new List(); + if (!Meta.TryGetValue("mentionedUsers", out var rawMentionedUsers)) + { + return mentioned; + } + if (rawMentionedUsers is Dictionary mentionedDict) + { + foreach (var kvp in mentionedDict) + { + if (kvp.Value is Dictionary mentionedUser) + { + mentioned.Add(new MentionedUser() + { + Id = (string)mentionedUser["id"], + Name = (string)mentionedUser["name"] + }); + } + } + } + return mentioned; + } + } + + /// + /// Gets the list of channels referenced in this message. + /// + /// Extracts channel references from the message metadata and returns them as a list of ReferencedChannel objects. + /// + /// + /// A list of channels that were referenced in this message. + public List ReferencedChannels { + get + { + var referenced = new List(); + if (!Meta.TryGetValue("referencedChannels", out var rawReferenced)) + { + return referenced; + } + if (rawReferenced is Dictionary referencedDict) + { + foreach (var kvp in referencedDict) + { + if (kvp.Value is Dictionary referencedChannel) + { + referenced.Add(new ReferencedChannel() + { + Id = (string)referencedChannel["id"], + Name = (string)referencedChannel["name"] + }); + } + } + } + return referenced; + } + } + + /// + /// Gets the list of text links found in this message. + /// + /// Extracts text links from the message metadata and returns them as a list of TextLink objects + /// containing start index, end index, and link URL information. + /// + /// + /// A list of text links found in this message. + public List TextLinks { + get + { + var links = new List(); + if (!Meta.TryGetValue("textLinks", out var rawLinks)) + { + return links; + } + if (rawLinks is Dictionary linksDick) + { + foreach (var kvp in linksDick) + { + if (kvp.Value is Dictionary link) + { + links.Add(new TextLink() + { + StartIndex = Convert.ToInt32(link["start_index"]), + EndIndex = Convert.ToInt32(link["end_index"]), + Link = (string)link["link"] + }); + } + } + } + return links; + } + } + + /// + /// Gets or sets the list of message actions applied to this message. + /// + /// Message actions include reactions, edits, deletions, and other modifications to the message. + /// + /// + /// A list of all message actions applied to this message. + public List MessageActions { get; internal set; } = new(); + + /// + /// Gets the list of reactions added to this message. + /// + /// Filters the message actions to return only reactions (like emojis or other reaction types). + /// + /// + /// A list of reaction message actions for this message. + public List Reactions => + MessageActions.Where(x => x.Type == PubnubMessageActionType.Reaction).ToList(); + + /// + /// The data type of the message. + /// + /// This is the type of the message data. + /// It can be used to determine the type of the message. + /// + /// + /// + public PubnubChatMessageType Type { get; internal set; } + + + /// + /// Event that is triggered when the message is updated. + /// + /// This event is triggered when the message is updated by the server. + /// Every time the message is updated, this event is triggered. + /// + /// + /// + /// + /// var message = // ...; + /// message.OnMessageUpdated += (message) => + /// { + /// Console.WriteLine("Message updated!"); + /// }; + /// + /// + /// + /// + public event Action OnMessageUpdated; + + protected override string UpdateChannelId => ChannelId; + + internal Message(Chat chat, string timeToken,string originalMessageText, string channelId, string userId, PubnubChatMessageType type, Dictionary meta, List messageActions) : base(chat, timeToken) + { + TimeToken = timeToken; + OriginalMessageText = originalMessageText; + ChannelId = channelId; + UserId = userId; + Type = type; + Meta = meta; + MessageActions = messageActions; + } + + protected override SubscribeCallback CreateUpdateListener() + { + return chat.ListenerFactory.ProduceListener(messageActionCallback: delegate(Pubnub pn, PNMessageActionEventResult e) + { + if (ChatParsers.TryParseMessageUpdate(chat, this, e)) + { + OnMessageUpdated?.Invoke(this); + } + }); + } + + /// + /// Edits the text of the message. + /// + /// This method edits the text of the message. + /// It changes the text of the message to the new text provided. + /// + /// + /// The new text of the message. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// var result = await message.EditMessageText("New text"); + /// + /// + /// + public async Task EditMessageText(string newText) + { + var result = new ChatOperationResult("Message.EditMessageText()", chat); + if (string.IsNullOrEmpty(newText)) + { + result.Error = true; + result.Exception = new PNException("Failed to edit text, new text is empty or null"); + return result; + } + result.RegisterOperation(await chat.PubnubInstance.AddMessageAction() + .Action(new PNMessageAction() { Type = "edited", Value = newText }) + .Channel(ChannelId) + .MessageTimetoken(long.Parse(TimeToken)).Channel(ChannelId).ExecuteAsync().ConfigureAwait(false)); + return result; + } + + /// + /// Gets the quoted message if this message quotes another message. + /// + /// A ChatOperationResult containing the quoted Message object if one exists, null otherwise. + public async Task> GetQuotedMessage() + { + var result = new ChatOperationResult("Message.GetQuotedMessage()", chat); + if (!Meta.TryGetValue("quotedMessage", out var quotedMessage)) + { + result.Error = true; + result.Exception = new PNException("No quoted message was found."); + return result; + } + if (quotedMessage is not Dictionary quotedMessageDict || + !quotedMessageDict.TryGetValue("timetoken", out var timetoken) || + !quotedMessageDict.TryGetValue("channelId", out var channelId)) + { + result.Error = true; + result.Exception = new PNException("Quoted message data has incorrect format."); + return result; + } + var getMessage = await chat.GetMessage(channelId.ToString(), timetoken.ToString()).ConfigureAwait(false); + if (result.RegisterOperation(getMessage)) + { + return result; + } + result.Result = getMessage.Result; + return result; + } + + /// + /// Checks if this message has a thread associated with it. + /// + /// Determines whether a thread channel has been created for this message by checking for thread-related message actions. + /// + /// + /// True if this message has a thread, false otherwise. + /// + /// + public bool HasThread() + { + return MessageActions.Any(x => x.Type == PubnubMessageActionType.ThreadRootId); + } + + internal string GetThreadId() + { + return $"{Chat.MESSAGE_THREAD_ID_PREFIX}_{ChannelId}_{TimeToken}"; + } + + /// + /// Creates a thread channel for this message. + /// + /// Creates a new thread channel that is associated with this message. Users can send messages + /// to the thread to have conversations related to this specific message. + /// + /// + /// A ChatOperationResult containing the created ThreadChannel object. + /// + /// + /// var message = // ...; + /// var result = message.CreateThread(); + /// if (!result.Error) { + /// var threadChannel = result.Result; + /// // Use the thread channel + /// } + /// + /// + /// + /// + /// + public ChatOperationResult CreateThread() + { + var result = new ChatOperationResult("Message.CreateThread()", chat); + if (ChannelId.Contains(Chat.MESSAGE_THREAD_ID_PREFIX)) + { + result.Error = true; + result.Exception = new PNException("Only one level of thread nesting is allowed."); + return result; + } + if (IsDeleted) + { + result.Error = true; + result.Exception = new PNException("You cannot create threads on deleted messages."); + return result; + } + if (HasThread()) + { + result.Error = true; + result.Exception = new PNException("Thread for this message already exist."); + return result; + } + var threadId = GetThreadId(); + var description = $"Thread on message with timetoken {TimeToken} on channel {ChannelId}"; + var data = new ChatChannelData() + { + Description = description + }; + result.Result = new ThreadChannel(chat, threadId, ChannelId, TimeToken, data); + return result; + } + + /// + /// Asynchronously tries to get the ThreadChannel started on this Message. + /// + /// A ChatOperationResult containing the retrieved ThreadChannel object, null if one wasn't found. + public async Task> GetThread() + { + return await chat.GetThreadChannel(this).ConfigureAwait(false); + } + + /// + /// Removes the thread channel associated with this message. + /// + /// Deletes the thread channel that was created for this message, including all messages in the thread. + /// This action cannot be undone. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// if (message.HasThread()) { + /// var result = await message.RemoveThread(); + /// if (!result.Error) { + /// // Thread has been removed + /// } + /// } + /// + /// + /// + /// + /// + public async Task RemoveThread() + { + var result = new ChatOperationResult("Message.RemoveThread()", chat); + if (!HasThread()) + { + result.Error = true; + result.Exception = new PNException("There is no thread to be deleted"); + return result; + } + var threadMessageAction = MessageActions.First(x => x.Type == PubnubMessageActionType.ThreadRootId); + var getThread = await GetThread().ConfigureAwait(false); + if (result.RegisterOperation(getThread)) + { + return result; + } + if (result.RegisterOperation(await chat.PubnubInstance.RemoveMessageAction().Channel(ChannelId) + .MessageTimetoken(long.Parse(TimeToken)).ActionTimetoken(long.Parse(threadMessageAction.TimeToken)) + .ExecuteAsync().ConfigureAwait(false))) + { + return result; + } + MessageActions = MessageActions.Where(x => x.Type != PubnubMessageActionType.ThreadRootId).ToList(); + result.RegisterOperation(await getThread.Result.Delete().ConfigureAwait(false)); + return result; + } + + /// + /// Pins this message to its channel. + /// + /// Marks this message as the pinned message for the channel it belongs to. + /// Only one message can be pinned per channel. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// var result = await message.Pin(); + /// if (!result.Error) { + /// // Message has been pinned to the channel + /// } + /// + /// + /// + /// + public async Task Pin() + { + var result = new ChatOperationResult("Message.Pin()", chat); + var getChannel = await chat.GetChannel(ChannelId).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) + { + return result; + } + result.RegisterOperation(await getChannel.Result.PinMessage(this).ConfigureAwait(false)); + return result; + } + + /// + /// Reports this message for inappropriate content or behavior. + /// + /// Submits a report about this message to the moderation system with the specified reason. + /// This helps maintain community guidelines and content standards. + /// + /// + /// The reason for reporting this message. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// var result = await message.Report("Spam content"); + /// if (!result.Error) { + /// // Message has been reported + /// } + /// + /// + public async Task Report(string reason) + { + var jsonDict = new Dictionary() + { + {"text",MessageText}, + {"reason",reason}, + {"reportedMessageChannelId",ChannelId}, + {"reportedMessageTimetoken",TimeToken}, + {"reportedUserId",UserId} + }; + return await chat.EmitEvent(PubnubChatEventType.Report, $"{Chat.INTERNAL_MODERATION_PREFIX}_{ChannelId}", + chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)).ConfigureAwait(false); + } + + /// + /// Forwards this message to another channel. + /// + /// Sends a copy of this message to the specified channel, preserving the original text and metadata. + /// + /// + /// The ID of the channel to forward the message to. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// var result = await message.Forward("target-channel-id"); + /// if (!result.Error) { + /// // Message has been forwarded + /// } + /// + /// + /// + public async Task Forward(string channelId) + { + var result = new ChatOperationResult("Message.Forward()", chat); + var getChannel = await chat.GetChannel(channelId).ConfigureAwait(false); + if (result.RegisterOperation(getChannel)) + { + return result; + } + result.RegisterOperation(await getChannel.Result.ForwardMessage(this).ConfigureAwait(false)); + return result; + } + + /// + /// Checks if this message has a specific reaction. + /// + /// Determines whether the specified reaction value (like an emoji) has been added to this message. + /// + /// + /// The reaction value to check for (e.g., "👍", "❤️"). + /// True if the message has the specified reaction, false otherwise. + /// + /// + /// var message = // ...; + /// if (message.HasUserReaction("👍")) { + /// // Message has a thumbs up reaction + /// } + /// + /// + /// + /// + public bool HasUserReaction(string reactionValue) + { + return Reactions.Any(x => x.Value == reactionValue); + } + + /// + /// Toggles a reaction on this message. + /// + /// Adds the specified reaction if it doesn't exist, or removes it if it already exists. + /// This allows users to react to messages with emojis or other reaction types. + /// + /// + /// The reaction value to toggle (e.g., "👍", "❤️"). + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// var result = await message.ToggleReaction("👍"); + /// if (!result.Error) { + /// // Reaction has been toggled + /// } + /// + /// + /// + /// + public async Task ToggleReaction(string reactionValue) + { + var result = new ChatOperationResult("Message.ToggleReaction()", chat); + var currentUserId = chat.PubnubInstance.GetCurrentUserId(); + for (var i = 0; i < MessageActions.Count; i++) + { + var reaction = MessageActions[i]; + if (reaction.Type == PubnubMessageActionType.Reaction && reaction.UserId == currentUserId && reaction.Value == reactionValue) + { + //Removing old one + var remove = await chat.PubnubInstance.RemoveMessageAction().MessageTimetoken(long.Parse(TimeToken)) + .ActionTimetoken(long.Parse(reaction.TimeToken)).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(remove)) + { + return result; + } + MessageActions.RemoveAt(i); + break; + } + } + var add = await chat.PubnubInstance.AddMessageAction().Action(new PNMessageAction() + { + Type = "reaction", Value = reactionValue + }).MessageTimetoken(long.Parse(TimeToken)).Channel(ChannelId).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(add)) + { + return result; + } + MessageActions.Add(new MessageAction() + { + UserId = currentUserId, + TimeToken = add.Result.MessageTimetoken.ToString(), + Type = PubnubMessageActionType.Reaction, + Value = reactionValue + }); + return result; + } + + /// + /// Restores a previously deleted message. + /// + /// Undoes the soft deletion of this message, making it visible again to all users. + /// This only works for messages that were soft deleted. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// if (message.IsDeleted) { + /// var result = await message.Restore(); + /// if (!result.Error) { + /// // Message has been restored + /// } + /// } + /// + /// + /// + /// + public async Task Restore() + { + var result = new ChatOperationResult("Message.Restore()", chat); + if (!IsDeleted) + { + result.Error = true; + result.Exception = new PNException("Can't restore a message that wasn't deleted!"); + return result; + } + var deleteAction = MessageActions.First(x => x.Type == PubnubMessageActionType.Deleted); + var restore = await chat.PubnubInstance.RemoveMessageAction().MessageTimetoken(long.Parse(TimeToken)) + .ActionTimetoken(long.Parse(deleteAction.TimeToken)).Channel(ChannelId).ExecuteAsync().ConfigureAwait(false); + result.RegisterOperation(restore); + MessageActions.RemoveAt(MessageActions.IndexOf(deleteAction)); + return result; + } + + /// + /// Deletes the message. + /// + /// A soft-deleted message can be restored, a hard-deleted one is permanently gone. + /// + /// + /// Whether to perform a soft delete (true) or hard delete (false). + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var message = // ...; + /// var result = await message.Delete(soft: true); + /// + /// + /// + /// + public async Task Delete(bool soft) + { + var result = new ChatOperationResult("Message.Delete()", chat); + if (soft) + { + var add = await chat.PubnubInstance.AddMessageAction() + .MessageTimetoken(long.Parse(TimeToken)).Action(new PNMessageAction() + { + Type = "deleted", + Value = "deleted" + }).Channel(ChannelId).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(add)) + { + return result; + } + MessageActions.Add(new MessageAction() + { + TimeToken = add.Result.ActionTimetoken.ToString(), + UserId = chat.PubnubInstance.GetCurrentUserId(), + Type = PubnubMessageActionType.Deleted, + Value = "deleted" + }); + } + else + { + if (HasThread()) + { + var getThread = await GetThread().ConfigureAwait(false); + if (result.RegisterOperation(getThread)) + { + return result; + } + var deleteThread = await getThread.Result.Delete().ConfigureAwait(false); + if (result.RegisterOperation(deleteThread)) + { + return result; + } + } + var startTimeToken = long.Parse(TimeToken) + 1; + var deleteMessage = await chat.PubnubInstance.DeleteMessages().Start(startTimeToken) + .End(long.Parse(TimeToken)).ExecuteAsync().ConfigureAwait(false); + result.RegisterOperation(deleteMessage); + } + return result; + } + + /// + /// Refreshes the message data from the server. + /// + /// Fetches the latest message information from the server and updates the local data, + /// including message actions, metadata, and other properties that may have changed. + /// + /// + /// A ChatOperationResult indicating the success or failure of the refresh operation. + /// + /// + /// var message = // ...; + /// var result = await message.Refresh(); + /// if (!result.Error) { + /// // Message data has been refreshed + /// Console.WriteLine($"Updated text: {message.MessageText}"); + /// } + /// + /// + public override async Task Refresh() + { + var result = new ChatOperationResult("Message.Refresh()", chat); + var get = await chat.GetMessage(ChannelId, TimeToken).ConfigureAwait(false); + if (result.RegisterOperation(get)) + { + return result; + } + MessageActions = get.Result.MessageActions; + Meta = get.Result.Meta; + return result; + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs.meta new file mode 100644 index 0000000..37c0fa6 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b594b5d330bc21340abf13e9fd84c5b5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MessageDraft.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MessageDraft.cs new file mode 100644 index 0000000..7e33207 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MessageDraft.cs @@ -0,0 +1,730 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Newtonsoft.Json; +using DiffMatchPatch; + +namespace PubnubChatApi +{ + public enum MentionType + { + User, + Channel, + Url + } + + public class MentionTarget + { + [JsonProperty("type")] + public MentionType Type { get; set; } + [JsonProperty("target")] + public string Target { get; set; } + } + + public class SuggestedMention + { + public int Offset { get; set; } + public string ReplaceFrom { get; set; } + public string ReplaceTo { get; set; } + public MentionTarget Target { get; set; } + } + + /// + /// Internal class to track mentions within the draft text + /// + internal class InternalMention + { + public int Start { get; set; } + public int Length { get; set; } + public MentionTarget Target { get; set; } + + public int EndExclusive => Start + Length; + + public InternalMention(int start, int length, MentionTarget target) + { + Start = start; + Length = length; + Target = target; + } + } + + public class MessageElement + { + public string Text { get; set; } + public MentionTarget? MentionTarget { get; set; } = null; + } + + /// + /// Enum describing the source for getting user suggestions for mentions. + /// + public enum UserSuggestionSource + { + /// + /// Search for users globally. + /// + GLOBAL, + + /// + /// Search only for users that are members of this channel. + /// + CHANNEL + } + + public class MessageDraft + { + private class DraftCallbackDataHelper + { + public List MessageElements; + public List SuggestedMentions; + } + + // Static regex patterns for mention detection + private static readonly Regex UserMentionRegex = new Regex(@"((?=\s?)@[a-zA-Z0-9_]+)", RegexOptions.Compiled); + private static readonly Regex ChannelReferenceRegex = new Regex(@"((?=\s?)#[a-zA-Z0-9_]+)", RegexOptions.Compiled); + + // Schema prefixes for rendering mentions + private static readonly string SchemaUser = "pn-user://"; + private static readonly string SchemaChannel = "pn-channel://"; + + // Internal state + private string _value = string.Empty; + private List _mentions = new (); + private diff_match_patch _diffMatchPatch = new (); + + public event Action, List> OnDraftUpdated; + + /// + /// Gets the current text of the draft + /// + public string Text => _value; + + /// + /// Gets the current message elements + /// + public List MessageElements => GetMessageElements(); + + public bool ShouldSearchForSuggestions { get; set; } + + private Channel channel; + private Chat chat; + + private bool isTypingIndicatorTriggered; + private UserSuggestionSource userSuggestionSource; + private int userLimit; + private int channelLimit; + + internal MessageDraft(Chat chat, Channel channel, UserSuggestionSource userSuggestionSource, bool isTypingIndicatorTriggered, int userLimit, int channelLimit, bool shouldSearchForSuggestions) + { + this.chat = chat; + this.channel = channel; + this.isTypingIndicatorTriggered = isTypingIndicatorTriggered; + this.userSuggestionSource = userSuggestionSource; + this.userLimit = userLimit; + this.channelLimit = channelLimit; + ShouldSearchForSuggestions = shouldSearchForSuggestions; + } + + private async void BroadcastDraftUpdate() + { + try + { + var messageElements = GetMessageElements(); + var suggestedMentions = ShouldSearchForSuggestions ? await GenerateSuggestedMentions().ConfigureAwait(false) : new List(); + OnDraftUpdated?.Invoke(messageElements, suggestedMentions); + } + catch (Exception e) + { + chat.Logger.Error($"Error has occured when trying to broadcast MessageDraft update: {e.Message}"); + } + } + + /// + /// Generates suggested mentions based on current text patterns + /// + private async Task> GenerateSuggestedMentions() + { + var suggestions = new List(); + var rawMentions = SuggestRawMentions(); + + foreach (var rawMention in rawMentions) + { + var suggestion = new SuggestedMention + { + Offset = rawMention.Start, + ReplaceFrom = _value.Substring(rawMention.Start, rawMention.Length), + }; + switch (rawMention.Target.Type) + { + case MentionType.User: + var usersWrapper = + await chat.GetUsers(filter: $"name LIKE \"{rawMention.Target.Target}*\"", limit:userLimit).ConfigureAwait(false); + if (!usersWrapper.Error && usersWrapper.Result.Users.Any()) + { + var user = usersWrapper.Result.Users[0]; + suggestion.Target = new MentionTarget() { Target = user.Id, Type = rawMention.Target.Type }; + suggestion.ReplaceTo = user.UserName; + if (userSuggestionSource == UserSuggestionSource.CHANNEL && + !(await user.IsPresentOn(channel.Id).ConfigureAwait(false)).Result) + { + continue; + } + } + else + { + continue; + } + break; + case MentionType.Channel: + var channelsWrapper = await chat.GetChannels(filter: $"name LIKE \"{rawMention.Target.Target}*\"", + limit: channelLimit).ConfigureAwait(false); + if (channelsWrapper.Channels != null && channelsWrapper.Channels.Any()) + { + var mentionedChannel = channelsWrapper.Channels[0]; + suggestion.Target = new MentionTarget() { Target = channel.Id, Type = rawMention.Target.Type }; + suggestion.ReplaceTo = mentionedChannel.Name; + } + else + { + continue; + } + break; + case MentionType.Url: + break; + default: + throw new ArgumentOutOfRangeException(); + } + suggestions.Add(suggestion); + } + + return suggestions; + } + + /// + /// Suggests raw mentions based on regex patterns in the text + /// + internal List SuggestRawMentions() + { + var allMentions = new List(); + + // Find user mentions (@username) + var userMatches = UserMentionRegex.Matches(_value); + foreach (Match match in userMatches) + { + bool alreadyMentioned = _mentions.Any(mention => mention.Start == match.Index); + if (!alreadyMentioned) + { + var target = new MentionTarget { Type = MentionType.User, Target = match.Value[1..] }; + allMentions.Add(new InternalMention(match.Index, match.Length, target)); + } + } + + // Find channel mentions (#channel) + var channelMatches = ChannelReferenceRegex.Matches(_value); + foreach (Match match in channelMatches) + { + bool alreadyMentioned = _mentions.Any(mention => mention.Start == match.Index); + if (!alreadyMentioned) + { + var target = new MentionTarget { Type = MentionType.Channel, Target = match.Value[1..] }; + allMentions.Add(new InternalMention(match.Index, match.Length, target)); + } + } + + // Sort by start position + allMentions.Sort((a, b) => a.Start.CompareTo(b.Start)); + + return allMentions; + } + + /// + /// Inserts a suggested mention into the draft at the appropriate position. + /// + /// Insert mention into the MessageDraft according to SuggestedMention.Offset, SuggestedMention.ReplaceFrom and + /// SuggestedMention.target. + /// + /// + /// A SuggestedMention that can be obtained from OnDraftUpdated when ShouldSearchForSuggestions is set to true + /// The text to replace SuggestedMention.ReplaceFrom with. SuggestedMention.ReplaceTo can be used for example. + public void InsertSuggestedMention(SuggestedMention mention, string text) + { + if (mention == null || string.IsNullOrEmpty(text) || mention.Target == null) + { + return; + } + if (!ValidateSuggestedMention(mention)) + { + return; + } + + TriggerTypingIndicator(); + + // Remove the text that should be replaced + ApplyRemoveTextInternal(mention.Offset, mention.ReplaceFrom.Length); + + // Insert the new text + ApplyInsertTextInternal(mention.Offset, text); + + // Add mention for the inserted text + _mentions.Add(new InternalMention(mention.Offset, text.Length, mention.Target)); + + // Sort mentions by start position + _mentions.Sort((a, b) => a.Start.CompareTo(b.Start)); + + BroadcastDraftUpdate(); + } + + /// + /// Insert some text into the MessageDraft text at the given offset. + /// + /// The position from the start of the message draft where insertion will occur + /// Text the text to insert at the given offset + public void InsertText(int offset, string text) + { + if (string.IsNullOrEmpty(text) || offset < 0 || offset > _value.Length) + { + return; + } + + TriggerTypingIndicator(); + + // Insert text at the specified position + _value = _value.Insert(offset, text); + + // Filter out mentions that overlap with the insertion point and adjust positions + var newMentions = new List(); + + foreach (var mention in _mentions) + { + // Only keep mentions that don't overlap with the insertion point + if (offset <= mention.Start || offset >= mention.EndExclusive) + { + var newMention = new InternalMention(mention.Start, mention.Length, mention.Target); + + // Adjust start position if the mention comes after the insertion point + if (offset <= mention.Start) + { + newMention.Start += text.Length; + } + + newMentions.Add(newMention); + } + } + + _mentions = newMentions; + BroadcastDraftUpdate(); + } + + /// + /// Remove a number of characters from the MessageDraft text at the given offset. + /// + /// The position from the start of the message draft where removal will occur + /// Length the number of characters to remove, starting at the given offset + public void RemoveText(int offset, int length) + { + if (offset < 0 || offset >= _value.Length || length <= 0) + { + return; + } + + TriggerTypingIndicator(); + + // Clamp length to not exceed the text bounds + length = Math.Min(length, _value.Length - offset); + + // Remove text from the specified position + _value = _value.Remove(offset, length); + + // Filter out mentions that overlap with the removal range and adjust positions + var newMentions = new List(); + + foreach (var mention in _mentions) + { + // Only keep mentions that don't overlap with the removal range + if (offset > mention.EndExclusive || offset + length <= mention.Start) + { + var newMention = new InternalMention(mention.Start, mention.Length, mention.Target); + + // Adjust start position if the mention comes after the removal range + if (offset < mention.Start) + { + newMention.Start -= Math.Min(length, mention.Start - offset); + } + + newMentions.Add(newMention); + } + } + + _mentions = newMentions; + BroadcastDraftUpdate(); + } + + /// + /// Add a mention to a user, channel or link specified by target at the given offset. + /// + /// The start of the mention + /// The number of characters (length) of the mention + /// The target of the mention + public void AddMention(int offset, int length, MentionTarget target) + { + if (target == null || offset < 0 || length <= 0 || offset + length > _value.Length) + { + return; + } + + // Add the mention to the list + _mentions.Add(new InternalMention(offset, length, target)); + + // Sort mentions by start position + _mentions.Sort((a, b) => a.Start.CompareTo(b.Start)); + + BroadcastDraftUpdate(); + } + + /// + /// Remove a mention starting at the given offset, if any. + /// + /// Offset the start of the mention to remove + public void RemoveMention(int offset) + { + // Remove mentions that start at the specified offset + _mentions.RemoveAll(mention => mention.Start == offset); + + // Sort mentions by start position + _mentions.Sort((a, b) => a.Start.CompareTo(b.Start)); + + BroadcastDraftUpdate(); + } + + /// + /// Update the whole message draft text with a new value. + /// Internally MessageDraft will try to calculate the most + /// optimal set of insertions and removals that will convert the current text to the provided text, in order to + /// preserve any mentions. This is a best effort operation, and if any mention text is found to be modified, + /// the mention will be invalidated and removed. + /// + /// + public void Update(string text) + { + text ??= string.Empty; + + TriggerTypingIndicator(); + + // Use diff-match-patch to compute differences + var diffs = _diffMatchPatch.diff_main(_value, text); + _diffMatchPatch.diff_cleanupSemantic(diffs); + + int consumed = 0; + + // Apply each diff operation + foreach (var diff in diffs) + { + switch (diff.operation) + { + case Operation.DELETE: + // Apply removal without broadcasting + ApplyRemoveTextInternal(consumed, diff.text.Length); + break; + + case Operation.INSERT: + // Apply insertion without broadcasting + ApplyInsertTextInternal(consumed, diff.text); + consumed += diff.text.Length; + break; + + case Operation.EQUAL: + consumed += diff.text.Length; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + BroadcastDraftUpdate(); + } + + /// + /// Internal method to apply text insertion without triggering broadcasts + /// + private void ApplyInsertTextInternal(int offset, string text) + { + text ??= string.Empty; + if (offset < 0 || offset > _value.Length) + { + return; + } + + // Insert text at the specified position + _value = _value.Insert(offset, text); + + // Filter out mentions that overlap with the insertion point and adjust positions + var newMentions = new List(); + + foreach (var mention in _mentions) + { + // Only keep mentions that don't overlap with the insertion point + if (offset <= mention.Start || offset >= mention.EndExclusive) + { + var newMention = new InternalMention(mention.Start, mention.Length, mention.Target); + + // Adjust start position if the mention comes after the insertion point + if (offset <= mention.Start) + { + newMention.Start += text.Length; + } + + newMentions.Add(newMention); + } + } + + _mentions = newMentions; + } + + /// + /// Internal method to apply text removal without triggering broadcasts + /// + private void ApplyRemoveTextInternal(int offset, int length) + { + if (offset < 0 || offset >= _value.Length || length <= 0) + { + return; + } + + // Clamp length to not exceed the text bounds + length = Math.Min(length, _value.Length - offset); + + // Remove text from the specified position + _value = _value.Remove(offset, length); + + // Filter out mentions that overlap with the removal range and adjust positions + var newMentions = new List(); + + foreach (var mention in _mentions) + { + // Only keep mentions that don't overlap with the removal range + if (offset > mention.EndExclusive || offset + length <= mention.Start) + { + var newMention = new InternalMention(mention.Start, mention.Length, mention.Target); + + // Adjust start position if the mention comes after the removal range + if (offset < mention.Start) + { + newMention.Start -= Math.Min(length, mention.Start - offset); + } + + newMentions.Add(newMention); + } + } + + _mentions = newMentions; + } + + /// + /// Send the MessageDraft, along with its quotedMessage if any, on the channel. + /// + public async Task Send() + { + return await Send(new SendTextParams()).ConfigureAwait(false); + } + + /// + /// Send the rendered MessageDraft on the channel. + /// + /// Additional parameters for sending the message. + public async Task Send(SendTextParams sendTextParams) + { + var mentions = new Dictionary(); + //TODO: revisit if this is the final data format and how to solve that we don't include name anywhere + var userMentionIndex = 0; + var channelReferenceIndex = 0; + var textLinkIndex = 0; + foreach (var internalMention in _mentions) + { + switch (internalMention.Target.Type) + { + case MentionType.User: + mentions.Add(userMentionIndex++, new MentionedUser(){Id = internalMention.Target.Target}); + break; + case MentionType.Channel: + var reference = new ReferencedChannel() { Id = internalMention.Target.Target }; + if (sendTextParams.Meta.TryGetValue("referencedChannels", out var refs)) + { + if (refs is Dictionary referencedChannels) + { + referencedChannels.Add(channelReferenceIndex++, reference); + } + } + else + { + sendTextParams.Meta.Add("referencedChannels", new Dictionary(){{channelReferenceIndex++, reference}}); + } + break; + case MentionType.Url: + var link = new TextLink() { StartIndex = internalMention.Start, EndIndex = internalMention.EndExclusive, Link = internalMention.Target.Target }; + if (sendTextParams.Meta.TryGetValue("textLinks", out var linkObjects)) + { + if (linkObjects is Dictionary links) + { + links.Add(textLinkIndex++, link); + } + } + else + { + sendTextParams.Meta.Add("textLinks", new Dictionary(){{textLinkIndex++, link}}); + } + break; + default: + break; + } + } + sendTextParams.MentionedUsers = mentions; + return await channel.SendText(Render(), sendTextParams).ConfigureAwait(false); + } + + /// + /// Validates that a suggested mention is valid for the current text + /// + private bool ValidateSuggestedMention(SuggestedMention suggestedMention) + { + if (suggestedMention.Offset < 0 || suggestedMention.Offset >= _value.Length) + { + return false; + } + if (string.IsNullOrEmpty(suggestedMention.ReplaceFrom)) + { + return false; + } + if (suggestedMention.Offset + suggestedMention.ReplaceFrom.Length > _value.Length) + { + return false; + } + + var substring = _value.Substring(suggestedMention.Offset, suggestedMention.ReplaceFrom.Length); + return substring == suggestedMention.ReplaceFrom; + } + + /// + /// Validates that mentions don't overlap and are within valid text bounds. + /// + /// True if all mentions are valid, false otherwise. + public bool ValidateMentions() + { + for (int i = 0; i < _mentions.Count; i++) + { + if (i > 0 && _mentions[i].Start < _mentions[i - 1].EndExclusive) + { + return false; + } + } + return true; + } + + /// + /// Gets message elements with plain text and links + /// + public List GetMessageElements() + { + var elements = new List(); + int lastPosition = 0; + + foreach (var mention in _mentions) + { + // Add plain text before the mention + if (lastPosition < mention.Start) + { + var plainText = _value.Substring(lastPosition, mention.Start - lastPosition); + if (!string.IsNullOrEmpty(plainText)) + { + elements.Add(new MessageElement { Text = plainText, MentionTarget = null }); + } + } + + // Add the mention element + var mentionText = _value.Substring(mention.Start, mention.Length); + elements.Add(new MessageElement { Text = mentionText, MentionTarget = mention.Target }); + + lastPosition = mention.EndExclusive; + } + + // Add remaining text after last mention + if (lastPosition < _value.Length) + { + var remainingText = _value.Substring(lastPosition); + if (!string.IsNullOrEmpty(remainingText)) + { + elements.Add(new MessageElement { Text = remainingText, MentionTarget = null }); + } + } + + return elements; + } + + /// + /// Renders the draft text with mentions converted to their appropriate schema format. + /// + /// Renders the message with markdown-style links. + /// + /// + /// The rendered text with schema-formatted mentions. + public string Render() + { + var elements = GetMessageElements(); + var result = new System.Text.StringBuilder(); + + foreach (var element in elements) + { + if (element.MentionTarget == null) + { + result.Append(element.Text); + } + else + { + var escapedText = EscapeLinkText(element.Text); + var escapedUrl = EscapeLinkUrl(element.MentionTarget.Target); + + switch (element.MentionTarget.Type) + { + case MentionType.User: + result.Append($"[{escapedText}]({SchemaUser}{escapedUrl})"); + break; + case MentionType.Channel: + result.Append($"[{escapedText}]({SchemaChannel}{escapedUrl})"); + break; + case MentionType.Url: + result.Append($"[{escapedText}]({escapedUrl})"); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + return result.ToString(); + } + + private async void TriggerTypingIndicator() + { + if (isTypingIndicatorTriggered && channel.Type == "public") + { + await channel.StartTyping().ConfigureAwait(false); + } + } + + /// + /// Escapes text for use in markdown links + /// + private static string EscapeLinkText(string text) + { + return text?.Replace("\\", "\\\\").Replace("]", "\\]") ?? string.Empty; + } + + /// + /// Escapes URLs for use in markdown links + /// + private static string EscapeLinkUrl(string url) + { + return url?.Replace("\\", "\\\\").Replace(")", "\\)") ?? string.Empty; + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MessageDraft.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MessageDraft.cs.meta new file mode 100644 index 0000000..1f55898 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MessageDraft.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2d58e4d14a244254594c19b176319d88 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadChannel.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadChannel.cs new file mode 100644 index 0000000..cca5b1a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadChannel.cs @@ -0,0 +1,165 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; + +namespace PubnubChatApi +{ + public class ThreadChannel : Channel + { + public string ParentChannelId { get; } + public string ParentMessageTimeToken { get; } + + private bool initialised; + + internal ThreadChannel(Chat chat, string channelId, string parentChannelId, string parentMessageTimeToken, + ChatChannelData data) : base(chat, channelId, data) + { + ParentChannelId = parentChannelId; + ParentMessageTimeToken = parentMessageTimeToken; + data.CustomData["parentChannelId"] = ParentChannelId; + data.CustomData["parentMessageTimetoken"] = ParentMessageTimeToken; + } + + private async Task InitThreadChannel() + { + var result = new ChatOperationResult("ThreadChannel.InitThreadChannel()", chat); + var channelUpdate = await UpdateChannelData(chat, Id, channelData).ConfigureAwait(false); + if (result.RegisterOperation(channelUpdate)) + { + return result; + } + result.RegisterOperation(await chat.PubnubInstance.AddMessageAction() + .Action(new PNMessageAction() { Type = "threadRootId", Value = Id }).Channel(ParentChannelId) + .MessageTimetoken(long.Parse(ParentMessageTimeToken)).ExecuteAsync().ConfigureAwait(false)); + return result; + } + + public override async Task SendText(string message, SendTextParams sendTextParams) + { + var result = new ChatOperationResult("ThreadChannel.SendText()", chat); + if (!initialised) + { + if (result.RegisterOperation(await InitThreadChannel().ConfigureAwait(false))) + { + return result; + } + + initialised = true; + } + + return await base.SendText(message, sendTextParams).ConfigureAwait(false); + } + + /// + /// Gets the message history for this thread channel. + /// + /// Retrieves the list of messages from this thread within the specified time range and + /// returns them as ThreadMessage objects that contain additional context about the parent channel. + /// + /// + /// The start time token for the history range. + /// The end time token for the history range. + /// The maximum number of messages to retrieve. + /// A ChatOperationResult containing the list of ThreadMessage objects from this thread. + /// + /// + /// var threadChannel = // ...; + /// var result = await threadChannel.GetThreadHistory("start_token", "end_token", 50); + /// if (!result.Error) { + /// foreach (var threadMessage in result.Result) { + /// Console.WriteLine($"Thread message: {threadMessage.MessageText}"); + /// Console.WriteLine($"Parent channel: {threadMessage.ParentChannelId}"); + /// } + /// } + /// + /// + /// + /// + public async Task>> GetThreadHistory(string startTimeToken, + string endTimeToken, int count) + { + var result = new ChatOperationResult>("ThreadChannel.GetThreadHistory()", chat) + { + Result = new List() + }; + var getHistory = await GetMessageHistory(startTimeToken, endTimeToken, count).ConfigureAwait(false); + if (result.RegisterOperation(getHistory)) + { + return result; + } + + foreach (var message in getHistory.Result) + { + result.Result.Add(new ThreadMessage(chat, message.TimeToken, message.OriginalMessageText, + message.ChannelId, ParentChannelId, message.UserId, PubnubChatMessageType.Text, message.Meta, + message.MessageActions)); + } + + return result; + } + + public override async Task EmitUserMention(string userId, string timeToken, string text) + { + var jsonDict = new Dictionary() + { + {"text",text}, + {"messageTimetoken",timeToken}, + {"channel",Id}, + {"parentChannel", ParentChannelId} + }; + return await chat.EmitEvent(PubnubChatEventType.Mention, userId, + chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)).ConfigureAwait(false); + } + + /// + /// Pins a thread message to the parent channel. + /// + /// Takes a message from this thread and pins it to the parent channel where the thread originated. + /// This allows important thread messages to be highlighted in the main channel. + /// + /// + /// The thread message to pin to the parent channel. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var threadChannel = // ...; + /// var threadMessage = // ... get a thread message + /// var result = await threadChannel.PinMessageToParentChannel(threadMessage); + /// if (!result.Error) { + /// // Thread message has been pinned to the parent channel + /// } + /// + /// + /// + /// + /// + public async Task PinMessageToParentChannel(ThreadMessage message) + { + return await chat.PinMessageToChannel(ParentChannelId, message).ConfigureAwait(false); + } + + /// + /// Unpins the currently pinned message from the parent channel. + /// + /// Removes the pinned message from the parent channel where this thread originated. + /// This undoes a previous pin operation performed by PinMessageToParentChannel. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var threadChannel = // ...; + /// var result = await threadChannel.UnPinMessageFromParentChannel(); + /// if (!result.Error) { + /// // Message has been unpinned from the parent channel + /// } + /// + /// + /// + /// + public async Task UnPinMessageFromParentChannel() + { + return await chat.UnpinMessageFromChannel(ParentChannelId).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadChannel.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadChannel.cs.meta new file mode 100644 index 0000000..890d296 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadChannel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 52460abf78731944cb1b9aff62a94bd0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadMessage.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadMessage.cs new file mode 100644 index 0000000..781e99f --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadMessage.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; + +namespace PubnubChatApi +{ + public class ThreadMessage : Message + { + public event Action OnThreadMessageUpdated; + + public string ParentChannelId { get; } + + internal ThreadMessage(Chat chat, string timeToken, string originalMessageText, string channelId, + string parentChannelId, string userId, PubnubChatMessageType type, Dictionary meta, + List messageActions) : base(chat, timeToken, originalMessageText, channelId, userId, type, + meta, messageActions) + { + ParentChannelId = parentChannelId; + } + + protected override SubscribeCallback CreateUpdateListener() + { + return chat.ListenerFactory.ProduceListener( + messageActionCallback: delegate(Pubnub pn, PNMessageActionEventResult e) + { + if (ChatParsers.TryParseMessageUpdate(chat, this, e)) + { + OnThreadMessageUpdated?.Invoke(this); + } + }); + } + + /// + /// Pins this thread message to the parent channel. + /// + /// Takes this message from the thread and pins it to the parent channel where the thread originated. + /// This allows important thread messages to be highlighted in the main channel for all users to see. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var threadMessage = // ... get a thread message + /// var result = await threadMessage.PinMessageToParentChannel(); + /// if (!result.Error) { + /// // This thread message has been pinned to the parent channel + /// } + /// + /// + /// + /// + /// + /// + public async Task PinMessageToParentChannel() + { + return await chat.PinMessageToChannel(ParentChannelId, this).ConfigureAwait(false); + } + + /// + /// Unpins the currently pinned message from the parent channel. + /// + /// Removes the pinned message from the parent channel where this thread originated. + /// This is typically used when this thread message was previously pinned to the parent channel + /// and now needs to be unpinned. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var threadMessage = // ... get a thread message + /// var result = await threadMessage.UnPinMessageFromParentChannel(); + /// if (!result.Error) { + /// // Message has been unpinned from the parent channel + /// } + /// + /// + /// + /// + /// + public async Task UnPinMessageFromParentChannel() + { + return await chat.UnpinMessageFromChannel(ParentChannelId).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadMessage.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadMessage.cs.meta new file mode 100644 index 0000000..274c24d --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c6b0c32ff5f20647b5ca58d13801902 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/User.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/User.cs new file mode 100644 index 0000000..12d4707 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/User.cs @@ -0,0 +1,598 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; + +namespace PubnubChatApi +{ + /// + /// Represents a user in the chat. + /// + /// You can get information about the user, update the user's data, delete the user, set restrictions on the user, + /// + /// + public class User : UniqueChatEntity + { + private ChatUserData userData; + + /// + /// The user's user name. + /// + /// This might be user's display name in the chat. + /// + /// + public string UserName => userData.Username; + + /// + /// The user's external id. + /// + /// This might be user's id in the external system (e.g. Database, CRM, etc.) + /// + /// + public string ExternalId => userData.ExternalId; + + /// + /// The user's profile url. + /// + /// This might be user's profile url to download the profile picture. + /// + /// + public string ProfileUrl => userData.ProfileUrl; + + /// + /// The user's email. + /// + /// This should be user's email address. + /// + /// + public string Email => userData.Email; + + /// + /// The user's custom data. + /// + /// This might be any custom data that you want to store for the user. + /// + /// + public Dictionary CustomData => userData.CustomData; + + /// + /// The user's status. + /// + /// This is a string that represents the user's status. + /// + /// + public string Status => userData.Status; + + /// + /// The user's data type. + /// + /// This is a string that represents the user's data type. + /// + /// + public string DataType => userData.Type; + + public bool Active + { + get + { + if (CustomData == null || !CustomData.TryGetValue("lastActiveTimestamp", out var lastActiveTimestamp)) + { + return false; + } + var currentTimeStamp = ChatUtils.TimeTokenNowLong(); + var interval = chat.Config.StoreUserActivityInterval; + var lastActive = Convert.ToInt64(lastActiveTimestamp); + return currentTimeStamp - lastActive <= interval * 1000000; + } + } + + public string LastActiveTimeStamp + { + get + { + if (CustomData == null || !CustomData.TryGetValue("lastActiveTimestamp", out var lastActiveTimestamp)) + { + return string.Empty; + } + return lastActiveTimestamp.ToString(); + } + } + + /// + /// Event that is triggered when the user is updated. + /// + /// This event is triggered when the user's data is updated. + /// You can subscribe to this event to get notified when the user is updated. + /// + /// + /// + /// + /// // var user = // ...; + /// user.OnUserUpdated += (user) => + /// { + /// Console.WriteLine($"User {user.UserName} is updated."); + /// }; + /// + /// + /// + /// + public event Action OnUserUpdated; + + private Subscription mentionsSubscription; + private Subscription invitesSubscription; + private Subscription moderationSubscription; + public event Action OnMentionEvent; + public event Action OnInviteEvent; + public event Action OnModerationEvent; + + protected override string UpdateChannelId => Id; + + internal User(Chat chat, string userId, ChatUserData chatUserData) : base(chat, userId) + { + UpdateLocalData(chatUserData); + } + + protected override SubscribeCallback CreateUpdateListener() + { + return chat.ListenerFactory.ProduceListener(objectEventCallback: delegate(Pubnub pn, PNObjectEventResult e) + { + if (ChatParsers.TryParseUserUpdate(chat, this, e, out var updatedData)) + { + UpdateLocalData(updatedData); + OnUserUpdated?.Invoke(this); + } + }); + } + + /// + /// Sets whether to listen for mention events for this user. + /// + /// When enabled, the user will receive mention events when they are mentioned in messages. + /// + /// + /// True to start listening, false to stop listening. + /// + public void SetListeningForMentionEvents(bool listen) + { + SetListening(ref mentionsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Mention, out var mentionEvent)) + { + OnMentionEvent?.Invoke(mentionEvent); + chat.BroadcastAnyEvent(mentionEvent); + } + })); + } + + /// + /// Sets whether to listen for invite events for this user. + /// + /// When enabled, the user will receive invite events when they are invited to channels. + /// + /// + /// True to start listening, false to stop listening. + /// + public void SetListeningForInviteEvents(bool listen) + { + SetListening(ref invitesSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Invite, out var inviteEvent)) + { + OnInviteEvent?.Invoke(inviteEvent); + chat.BroadcastAnyEvent(inviteEvent); + } + })); + } + + /// + /// Sets whether to listen for moderation events for this user. + /// + /// When enabled, the user will receive moderation events such as bans, mutes, and other restrictions. + /// + /// + /// True to start listening, false to stop listening. + /// + public void SetListeningForModerationEvents(bool listen) + { + SetListening(ref moderationSubscription, SubscriptionOptions.None, listen, Chat.INTERNAL_MODERATION_PREFIX+Id, chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Moderation, out var moderationEvent)) + { + OnModerationEvent?.Invoke(moderationEvent); + chat.BroadcastAnyEvent(moderationEvent); + } + })); + } + + /// + /// Updates the user. + /// + /// This method updates the user's data. + /// + /// + /// The updated data for the user. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var user = // ...; + /// var result = await user.Update(new ChatUserData + /// { + /// UserName = "New User Name", + /// }); + /// + /// + /// + public async Task Update(ChatUserData updatedData) + { + UpdateLocalData(updatedData); + var result = new ChatOperationResult("User.Update()", chat); + result.RegisterOperation(await UpdateUserData(chat, Id, updatedData).ConfigureAwait(false)); + return result; + } + + internal static async Task> UpdateUserData(Chat chat, string userId, ChatUserData chatUserData) + { + var operation = chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true).IncludeStatus(true).IncludeType(true).Uuid(userId); + if (!string.IsNullOrEmpty(chatUserData.Username)) + { + operation = operation.Name(chatUserData.Username); + } + if (!string.IsNullOrEmpty(chatUserData.Email)) + { + operation = operation.Email(chatUserData.Email); + } + if (!string.IsNullOrEmpty(chatUserData.ExternalId)) + { + operation = operation.ExternalId(chatUserData.ExternalId); + } + if (!string.IsNullOrEmpty(chatUserData.ProfileUrl)) + { + operation = operation.ProfileUrl(chatUserData.ProfileUrl); + } + if (!string.IsNullOrEmpty(chatUserData.Type)) + { + operation = operation.Type(chatUserData.Type); + } + if (!string.IsNullOrEmpty(chatUserData.Status)) + { + operation = operation.Status(chatUserData.Status); + } + if (chatUserData.CustomData != null) + { + operation = operation.Custom(chatUserData.CustomData); + } + return await operation.ExecuteAsync().ConfigureAwait(false); + } + + internal static async Task> GetUserData(Chat chat, string userId) + { + return await chat.PubnubInstance.GetUuidMetadata().Uuid(userId).IncludeCustom(true).ExecuteAsync().ConfigureAwait(false); + } + + internal void UpdateLocalData(ChatUserData? newData) + { + if (newData == null) + { + return; + } + userData = newData; + } + + /// + /// Refreshes the user data from the server. + /// + /// Fetches the latest user information from the server and updates the local data. + /// This is useful when you want to ensure you have the most up-to-date user information. + /// + /// + /// A ChatOperationResult indicating the success or failure of the refresh operation. + /// + /// + /// var user = // ...; + /// var result = await user.Refresh(); + /// if (!result.Error) { + /// // User data has been refreshed + /// Console.WriteLine($"User name: {user.UserName}"); + /// } + /// + /// + public override async Task Refresh() + { + var result = new ChatOperationResult("User.Refresh()", chat); + var getUserData = await GetUserData(chat, Id).ConfigureAwait(false); + if (result.RegisterOperation(getUserData)) + { + return result; + } + UpdateLocalData(getUserData.Result); + return result; + } + + /// + /// Deletes the user. + /// + /// This method deletes the user from the chat. + /// It will remove the user from all the channels and delete the user's data. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var user = // ...; + /// await user.DeleteUser(); + /// + /// + public async Task DeleteUser() + { + return await chat.DeleteUser(Id).ConfigureAwait(false); + } + + /// + /// Sets restrictions on the user. + /// + /// This method sets the restrictions on the user. + /// You can ban the user from a channel, mute the user on the channel, or set the restrictions on the user. + /// + /// + /// The channel id on which the restrictions are set. + /// If set to true, the user is banned from the channel. + /// If set to true, the user is muted on the channel. + /// The reason for setting the restrictions on the user. + /// + /// + /// var user = // ...; + /// user.SetRestrictions("channel_id", true, false, "Banned from the channel"); + /// + /// + public async Task SetRestriction(string channelId, bool banUser, bool muteUser, string reason) + { + return await chat.SetRestriction(Id, channelId, banUser, muteUser, reason).ConfigureAwait(false); + } + + /// + /// Sets restrictions on the user using a Restriction object. + /// + /// This method sets the restrictions on the user using a structured Restriction object + /// that contains ban, mute, and reason information. + /// + /// + /// The channel id on which the restrictions are set. + /// The restriction object containing ban, mute, and reason information. + /// A ChatOperationResult indicating the success or failure of the operation. + /// + public async Task SetRestriction(string channelId, Restriction restriction) + { + return await chat.SetRestriction(Id, channelId, restriction).ConfigureAwait(false); + } + + /// + /// Gets the restrictions on the user for a specific channel. + /// + /// This method gets the restrictions (bans and mutes) that have been applied to this user + /// on the specified channel. + /// + /// + /// The channel for which the restrictions are to be fetched. + /// A ChatOperationResult containing the Restriction object if restrictions exist for this user on the channel, error otherwise. + /// + /// + /// var user = // ...; + /// var channel = // ...; + /// var result = await user.GetChannelRestrictions(channel); + /// var restriction = result.Result; + /// + /// + /// + public async Task> GetChannelRestrictions(Channel channel) + { + var result = new ChatOperationResult("User.GetChannelRestrictions()", chat); + var membersResult = await chat.PubnubInstance.GetChannelMembers().Channel($"{Chat.INTERNAL_MODERATION_PREFIX}_{channel.Id}").Include(new[] + { + PNChannelMemberField.CUSTOM + }).Filter($"uuid.id == \"{Id}\"").IncludeCount(true).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(membersResult) || membersResult.Result.ChannelMembers == null || !membersResult.Result.ChannelMembers.Any()) + { + result.Error = true; + return result; + } + var member = membersResult.Result.ChannelMembers[0]; + try + { + result.Result = new Restriction() + { + Ban = (bool)member.Custom["ban"], + Mute = (bool)member.Custom["mute"], + Reason = (string)member.Custom["reason"] + }; + } + catch (Exception e) + { + result.Error = true; + result.Exception = e; + } + return result; + } + + /// + /// Gets all channel restrictions for this user across all channels. + /// + /// This method retrieves all restrictions (bans and mutes) that have been applied to this user + /// across all channels where they have restrictions. + /// + /// + /// Sort criteria for restrictions. + /// The maximum number of restrictions to retrieve. + /// Pagination object for retrieving specific page results. + /// A ChatOperationResult containing the wrapper with all channel restrictions for this user. + /// + /// + /// var user = // ...; + /// var result = await user.GetChannelsRestrictions(limit: 10); + /// var restrictions = result.Result.Restrictions; + /// foreach (var restriction in restrictions) { + /// Console.WriteLine($"Channel: {restriction.ChannelId}, Ban: {restriction.Ban}, Mute: {restriction.Mute}"); + /// } + /// + /// + /// + /// + public async Task> GetChannelsRestrictions(string sort = "", int limit = 0, + PNPageObject page = null) + { + var result = new ChatOperationResult("User.GetChannelsRestrictions()", chat){Result = new ChannelsRestrictionsWrapper()}; + var operation = chat.PubnubInstance.GetMemberships().Uuid(Id) + .Include(new[] + { + PNMembershipField.CUSTOM, + PNMembershipField.CHANNEL + }).Filter($"channel.id LIKE \"{Chat.INTERNAL_MODERATION_PREFIX}_*\"").IncludeCount(true); + if (!string.IsNullOrEmpty(sort)) + { + operation = operation.Sort(new List() { sort }); + } + if (limit > 0) + { + operation = operation.Limit(limit); + } + if (page != null) + { + operation = operation.Page(page); + } + var membershipsResult = await operation.ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(membershipsResult)) + { + return result; + } + + result.Result.Page = membershipsResult.Result.Page; + result.Result.Total = membershipsResult.Result.TotalCount; + foreach (var membership in membershipsResult.Result.Memberships) + { + try + { + var internalChannelId = membership.ChannelMetadata.Channel; + var removeString = $"{Chat.INTERNAL_MODERATION_PREFIX}_"; + var index = internalChannelId.IndexOf(removeString, StringComparison.Ordinal); + var channelId = (index < 0) + ? internalChannelId + : internalChannelId.Remove(index, removeString.Length); + result.Result.Restrictions.Add(new ChannelRestriction() + { + Ban = (bool)membership.Custom["ban"], + Mute = (bool)membership.Custom["mute"], + Reason = (string)membership.Custom["reason"], + ChannelId = channelId + }); + } + catch (Exception e) + { + chat.Logger.Warn($"Incorrect data was encountered when parsing Channel Restriction for User \"{Id}\" in Channel \"{membership.ChannelMetadata.Channel}\". Exception was: {e.Message}"); + } + } + return result; + } + + /// + /// Checks if the user is present on the channel. + /// + /// This method checks if the user is present on the channel. + /// + /// + /// The channel id on which the user's presence is to be checked. + /// + /// A ChatOperationResult with true if the user is present on the channel; otherwise, false. + /// + /// + /// + /// var user = // ...; + /// if (user.IsPresentOn("channel_id")) { + /// // User is present on the channel + /// } + /// + /// + public async Task> IsPresentOn(string channelId) + { + var result = new ChatOperationResult("User.IsPresentOn()", chat); + var response = await chat.PubnubInstance.WhereNow().Uuid(Id).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(response)) + { + return result; + } + result.Result = response.Result.Channels.Contains(channelId); + return result; + } + + /// + /// Gets the list of channels where the user is present. + /// + /// This method gets the list of channels where the user is present. + /// + /// + /// + /// A ChatOperationResult containing the list of channels where the user is present. + /// + /// + /// The list is kept as a list of channel ids. + /// + /// + /// + /// var user = // ...; + /// var result = await user.WherePresent(); + /// var channels = result.Result; + /// foreach (var channel in channels) { + /// Console.WriteLine(channel); + /// } + /// + /// + public async Task>> WherePresent() + { + var result = new ChatOperationResult>("User.WherePresent()", chat); + var where = await chat.PubnubInstance.WhereNow().Uuid(Id).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(where)) + { + return result; + } + result.Result = new List(); + if (where.Result != null) + { + result.Result.AddRange(where.Result.Channels); + } + return result; + } + + /// + /// Gets the list of memberships of the user. + /// + /// This methods gets the list of memberships of the user. + /// All the relationships of the user with the channels are considered as memberships. + /// + /// + /// The filter parameter. + /// The sort parameter. + /// The limit on the number of memberships to be fetched. + /// The page object for pagination. + /// + /// A ChatOperationResult containing the list of memberships of the user. + /// + /// + /// + /// var user = // ...; + /// var result = await user.GetMemberships(limit: 50); + /// var memberships = result.Result.Memberships; + /// foreach (var membership in memberships) { + /// Console.WriteLine(membership.ChannelId); + /// } + /// + /// + /// + public async Task> GetMemberships(string filter = "", string sort = "", int limit = 0, + PNPageObject page = null) + { + return await chat.GetUserMemberships(Id, filter, sort, limit, page).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/User.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/User.cs.meta new file mode 100644 index 0000000..5b2c673 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/User.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3e5694b922163044aac1718881cd3421 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums.meta new file mode 100644 index 0000000..f15b598 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5471dbc83d410a64e95a5de975ab81e9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessPermission.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessPermission.cs new file mode 100644 index 0000000..e85e0fa --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessPermission.cs @@ -0,0 +1,13 @@ +namespace PubnubChatApi +{ + public enum PubnubAccessPermission + { + Read, + Write, + Manage, + Delete, + Get, + Join, + Update + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessPermission.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessPermission.cs.meta new file mode 100644 index 0000000..9381966 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessPermission.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bd342eb3f69d49748bc7736a277d02ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessResourceType.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessResourceType.cs new file mode 100644 index 0000000..0bf0e79 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessResourceType.cs @@ -0,0 +1,8 @@ +namespace PubnubChatApi +{ + public enum PubnubAccessResourceType + { + Uuids, + Channels + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessResourceType.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessResourceType.cs.meta new file mode 100644 index 0000000..4254b25 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessResourceType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8dd9d0418469a364f9453fc93ce0a62e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChannelType.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChannelType.cs new file mode 100644 index 0000000..a73a6af --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChannelType.cs @@ -0,0 +1,9 @@ +namespace PubnubChatApi +{ + public enum PubnubChannelType + { + Direct, + Public, + Group + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChannelType.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChannelType.cs.meta new file mode 100644 index 0000000..80d7281 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChannelType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2e2667b6980124840a1eb500a1d0201d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatEventType.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatEventType.cs new file mode 100644 index 0000000..3a2cdfc --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatEventType.cs @@ -0,0 +1,13 @@ +namespace PubnubChatApi +{ + public enum PubnubChatEventType + { + Typing, + Report, + Receipt, + Mention, + Invite, + Custom, + Moderation + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatEventType.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatEventType.cs.meta new file mode 100644 index 0000000..a936a60 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatEventType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fa8ba5dac82c79343b64f7417be9d547 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatMessageType.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatMessageType.cs new file mode 100644 index 0000000..56369c4 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatMessageType.cs @@ -0,0 +1,13 @@ +namespace PubnubChatApi +{ + /// + /// Represents the type of a chat message. + /// + /// Chat messages can have different types, such as text messages or message actions. + /// + /// + public enum PubnubChatMessageType + { + Text + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatMessageType.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatMessageType.cs.meta new file mode 100644 index 0000000..a698f2c --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatMessageType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 791302bb209f0874eb3532f1adbacbf9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubMessageActionType.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubMessageActionType.cs new file mode 100644 index 0000000..d060b03 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubMessageActionType.cs @@ -0,0 +1,12 @@ +namespace PubnubChatApi +{ + public enum PubnubMessageActionType + { + Reaction, + Receipt, + Custom, + Edited, + Deleted, + ThreadRootId + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubMessageActionType.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubMessageActionType.cs.meta new file mode 100644 index 0000000..96fe273 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubMessageActionType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1f4c963a1b262b54d8ac847e0ed05731 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities.meta new file mode 100644 index 0000000..0a83864 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a0cb7a41bc2983a4ab3f37c92578670e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatEnumConverters.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatEnumConverters.cs new file mode 100644 index 0000000..37c05cd --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatEnumConverters.cs @@ -0,0 +1,75 @@ +using System; + +namespace PubnubChatApi +{ + internal static class ChatEnumConverters + { + internal static string ChatEventTypeToString(PubnubChatEventType eventType) + { + switch(eventType) + { + case PubnubChatEventType.Typing: + return "typing"; + case PubnubChatEventType.Report: + return "report"; + case PubnubChatEventType.Receipt: + return "receipt"; + case PubnubChatEventType.Mention: + return "mention"; + case PubnubChatEventType.Invite: + return "invite"; + case PubnubChatEventType.Custom: + return "custom"; + case PubnubChatEventType.Moderation: + return "moderation"; + default: + return "incorrect_chat_event_type"; + break; + } + } + + internal static PubnubChatEventType StringToEventType(string eventString) + { + switch (eventString) + { + case "typing": + return PubnubChatEventType.Typing; + case "report": + return PubnubChatEventType.Report; + case "receipt": + return PubnubChatEventType.Receipt; + case "mention": + return PubnubChatEventType.Mention; + case "invite": + return PubnubChatEventType.Invite; + case "custom": + return PubnubChatEventType.Custom; + case "moderation": + return PubnubChatEventType.Moderation; + default: + throw new ArgumentOutOfRangeException(); + } + } + + internal static PubnubMessageActionType StringToActionType(string actionString) + { + switch (actionString) + { + case "reaction": + return PubnubMessageActionType.Reaction; + case "receipt": + return PubnubMessageActionType.Receipt; + case "custom": + return PubnubMessageActionType.Custom; + case "edited": + return PubnubMessageActionType.Edited; + case "deleted": + return PubnubMessageActionType.Deleted; + case "threadRootId": + return PubnubMessageActionType.ThreadRootId; + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatEnumConverters.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatEnumConverters.cs.meta new file mode 100644 index 0000000..4b759ee --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatEnumConverters.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 13c53aa3f74cba549a04389529d0b2cd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatListenerFactory.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatListenerFactory.cs new file mode 100644 index 0000000..4fd35d7 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatListenerFactory.cs @@ -0,0 +1,16 @@ +using System; +using PubnubApi; + +namespace PubnubChatApi +{ + public abstract class ChatListenerFactory + { + public abstract SubscribeCallback ProduceListener(Action>? messageCallback = null, + Action? presenceCallback = null, + Action>? signalCallback = null, + Action? objectEventCallback = null, + Action? messageActionCallback = null, + Action? fileCallback = null, + Action? statusCallback = null); + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatListenerFactory.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatListenerFactory.cs.meta new file mode 100644 index 0000000..f58d3c7 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatListenerFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 371386995ca14cc4ea7d9e46bad37bb5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs new file mode 100644 index 0000000..94248cf --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PubnubApi; + +namespace PubnubChatApi +{ + internal static class ChatParsers + { + internal static bool TryParseMessageResult(Chat chat, PNMessageResult messageResult, out Message message) + { + try + { + var messageDict = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(messageResult.Message + .ToString()); + + if (!messageDict.TryGetValue("type", out var typeValue) || typeValue.ToString() != "text") + { + message = null; + return false; + } + + //TODO: later more types I guess? + var type = PubnubChatMessageType.Text; + var text = messageDict["text"].ToString(); + var meta = messageResult.UserMetadata ?? new Dictionary(); + + message = new Message(chat, messageResult.Timetoken.ToString(), text, messageResult.Channel, messageResult.Publisher, type, meta, new List()); + return true; + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNMessageResult with payload: {messageResult.Message} into chat Message entity. Exception was: {e.Message}"); + message = null; + return false; + } + } + + internal static bool TryParseMessageFromHistory(Chat chat, string channelId, PNHistoryItemResult historyItem, out Message message) + { + try + { + var messageDict = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(historyItem.Entry.ToString()); + + if (!messageDict.TryGetValue("type", out var typeValue) || typeValue.ToString() != "text") + { + message = null; + return false; + } + + //TODO: later more types I guess? + var type = PubnubChatMessageType.Text; + var text = messageDict["text"].ToString(); + + var actions = new List(); + if (historyItem.ActionItems != null) + { + foreach (var kvp in historyItem.ActionItems) + { + var actionType = ChatEnumConverters.StringToActionType(kvp.Key); + foreach (var actionEntry in kvp.Value) + { + actions.Add(new MessageAction() + { + TimeToken = actionEntry.ActionTimetoken.ToString(), + UserId = actionEntry.Uuid, + Type = actionType, + Value = actionEntry.Action.Value + }); + } + } + } + message = new Message(chat, historyItem.Timetoken.ToString(), text, channelId, historyItem.Uuid, type, historyItem.Meta, actions); + return true; + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNHistoryItemResult with payload: {historyItem.Entry} into chat Message entity. Exception was: {e.Message}"); + message = null; + return false; + } + } + + internal static bool TryParseMembershipUpdate(Chat chat, Membership membership, PNObjectEventResult objectEvent, out ChatMembershipData updatedData) + { + try + { + var channel = objectEvent.MembershipMetadata.Channel; + var user = objectEvent.MembershipMetadata.Uuid; + var type = objectEvent.Type; + if (type == "membership" && channel == membership.ChannelId && user == membership.UserId) + { + updatedData = new ChatMembershipData() + { + Status = objectEvent.MembershipMetadata.Status, + CustomData = objectEvent.MembershipMetadata.Custom, + Type = objectEvent.MembershipMetadata.Type + }; + return true; + } + else + { + updatedData = null; + return false; + } + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into Membership update. Exception was: {e.Message}"); + updatedData = null; + return false; + } + } + + internal static bool TryParseUserUpdate(Chat chat, User user, PNObjectEventResult objectEvent, out ChatUserData updatedData) + { + try + { + var uuid = objectEvent.UuidMetadata.Uuid; + var type = objectEvent.Type; + if (type == "uuid" && uuid == user.Id) + { + updatedData = objectEvent.UuidMetadata; + return true; + } + else + { + updatedData = null; + return false; + } + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into User update. Exception was: {e.Message}"); + updatedData = null; + return false; + } + } + + internal static bool TryParseChannelUpdate(Chat chat, Channel channel, PNObjectEventResult objectEvent, out ChatChannelData updatedData) + { + try + { + var channelId = objectEvent.Channel; + var type = objectEvent.Type; + if (type == "channel" && channelId == channel.Id) + { + updatedData = objectEvent.ChannelMetadata; + return true; + } + else + { + updatedData = null; + return false; + } + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into Channel update. Exception was: {e.Message}"); + updatedData = null; + return false; + } + } + + internal static bool TryParseMessageUpdate(Chat chat, Message message, PNMessageActionEventResult actionEvent) + { + try + { + if (actionEvent.MessageTimetoken.ToString() == message.TimeToken && actionEvent.Uuid == message.UserId && actionEvent.Channel == message.ChannelId) + { + if (actionEvent.Event != "removed") + { + //already has it + if (message.MessageActions.Any(x => x.TimeToken == actionEvent.ActionTimetoken.ToString())) + { + return true; + } + message.MessageActions.Add(new MessageAction() + { + TimeToken = actionEvent.ActionTimetoken.ToString(), + Type = ChatEnumConverters.StringToActionType(actionEvent.Action.Type), + Value = actionEvent.Action.Value, + UserId = actionEvent.Uuid + }); + } + else + { + var dict = message.MessageActions.ToDictionary(x => x.TimeToken, y => y); + dict.Remove(actionEvent.ActionTimetoken.ToString()); + message.MessageActions = dict.Values.ToList(); + } + return true; + } + else + { + return false; + } + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNMessageActionEventResult into Message update. Exception was: {e.Message}"); + return false; + } + } + + internal static bool TryParseEvent(Chat chat, PNMessageResult messageResult, PubnubChatEventType eventType, out ChatEvent chatEvent) + { + try + { + var jsonDict = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(messageResult.Message + .ToString()); + if (!jsonDict.TryGetValue("type", out var typeString)) + { + chatEvent = default; + return false; + } + var receivedEventType = ChatEnumConverters.StringToEventType(typeString.ToString()); + if (receivedEventType != eventType) + { + chatEvent = default; + return false; + } + chatEvent = new ChatEvent() + { + TimeToken = messageResult.Timetoken.ToString(), + Type = eventType, + ChannelId = messageResult.Channel, + UserId = messageResult.Publisher, + Payload = messageResult.Message.ToString() + }; + return true; + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNMessageResult into ChatEvent of type \"{eventType}\". Exception was: {e.Message}"); + chatEvent = default; + return false; + } + } + + internal static bool TryParseEventFromHistory(Chat chat, string channelId, PNHistoryItemResult historyItem, out ChatEvent chatEvent) + { + try + { + var jsonDict = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(historyItem.Entry.ToString()); + if (!jsonDict.TryGetValue("type", out var typeString)) + { + chatEvent = default; + return false; + } + var receivedEventType = ChatEnumConverters.StringToEventType(typeString.ToString()); + chatEvent = new ChatEvent() + { + TimeToken = historyItem.Timetoken.ToString(), + Type = receivedEventType, + ChannelId = channelId, + UserId = historyItem.Uuid, + Payload = historyItem.Entry.ToString() + }; + return true; + } + catch (Exception e) + { + chat.Logger.Debug($"Failed to parse PNHistoryItemResult into ChatEvent. Exception was: {e.Message}"); + chatEvent = default; + return false; + } + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs.meta new file mode 100644 index 0000000..b2f84bf --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 968b8fe3028f24043af8dc6dc1cbc8bf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatUtils.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatUtils.cs new file mode 100644 index 0000000..3ab1e40 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatUtils.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using PubnubApi; + +namespace PubnubChatApi +{ + internal static class ChatUtils + { + internal static string TimeTokenNow() + { + var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var timeStamp = Convert.ToInt64(timeSpan.TotalSeconds * 10000000); + return timeStamp.ToString(CultureInfo.InvariantCulture); + } + + internal static long TimeTokenNowLong() + { + var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var timeStamp = Convert.ToInt64(timeSpan.TotalSeconds * 10000000); + return timeStamp; + } + + internal static ChatOperationResult ToChatOperationResult(this PNResult result, string operationName, Chat chat) + { + var operationResult = new ChatOperationResult(operationName, chat); + operationResult.RegisterOperation(result); + return operationResult; + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatUtils.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatUtils.cs.meta new file mode 100644 index 0000000..2be11ef --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 226453a558b58fd46b321fc71f8d6c69 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DiffMatchPatch.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DiffMatchPatch.cs new file mode 100644 index 0000000..b158ff4 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DiffMatchPatch.cs @@ -0,0 +1,2298 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Web; + +namespace DiffMatchPatch { + internal static class CompatibilityExtensions { + // JScript splice function + public static List Splice(this List input, int start, int count, + params T[] objects) { + List deletedRange = input.GetRange(start, count); + input.RemoveRange(start, count); + input.InsertRange(start, objects); + + return deletedRange; + } + + // Java substring function + public static string JavaSubstring(this string s, int begin, int end) { + return s.Substring(begin, end - begin); + } + } + + /**- + * The data structure representing a diff is a List of Diff objects: + * {Diff(Operation.DELETE, "Hello"), Diff(Operation.INSERT, "Goodbye"), + * Diff(Operation.EQUAL, " world.")} + * which means: delete "Hello", add "Goodbye" and keep " world." + */ + public enum Operation { + DELETE, INSERT, EQUAL + } + + + /** + * Class representing one diff operation. + */ + public class Diff { + public Operation operation; + // One of: INSERT, DELETE or EQUAL. + public string text; + // The text associated with this diff operation. + + /** + * Constructor. Initializes the diff with the provided values. + * @param operation One of INSERT, DELETE or EQUAL. + * @param text The text being applied. + */ + public Diff(Operation operation, string text) { + // Construct a diff with the specified operation and text. + this.operation = operation; + this.text = text; + } + + /** + * Display a human-readable version of this Diff. + * @return text version. + */ + public override string ToString() { + string prettyText = this.text.Replace('\n', '\u00b6'); + return "Diff(" + this.operation + ",\"" + prettyText + "\")"; + } + + /** + * Is this Diff equivalent to another Diff? + * @param d Another Diff to compare against. + * @return true or false. + */ + public override bool Equals(Object obj) { + // If parameter is null return false. + if (obj == null) { + return false; + } + + // If parameter cannot be cast to Diff return false. + Diff p = obj as Diff; + if ((System.Object)p == null) { + return false; + } + + // Return true if the fields match. + return p.operation == this.operation && p.text == this.text; + } + + public bool Equals(Diff obj) { + // If parameter is null return false. + if (obj == null) { + return false; + } + + // Return true if the fields match. + return obj.operation == this.operation && obj.text == this.text; + } + + public override int GetHashCode() { + return text.GetHashCode() ^ operation.GetHashCode(); + } + } + + + /** + * Class representing one patch operation. + */ + public class Patch { + public List diffs = new List(); + public int start1; + public int start2; + public int length1; + public int length2; + + /** + * Emulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indices are printed as 1-based, not 0-based. + * @return The GNU diff string. + */ + public override string ToString() { + string coords1, coords2; + if (this.length1 == 0) { + coords1 = this.start1 + ",0"; + } else if (this.length1 == 1) { + coords1 = Convert.ToString(this.start1 + 1); + } else { + coords1 = (this.start1 + 1) + "," + this.length1; + } + if (this.length2 == 0) { + coords2 = this.start2 + ",0"; + } else if (this.length2 == 1) { + coords2 = Convert.ToString(this.start2 + 1); + } else { + coords2 = (this.start2 + 1) + "," + this.length2; + } + StringBuilder text = new StringBuilder(); + text.Append("@@ -").Append(coords1).Append(" +").Append(coords2) + .Append(" @@\n"); + // Escape the body of the patch with %xx notation. + foreach (Diff aDiff in this.diffs) { + switch (aDiff.operation) { + case Operation.INSERT: + text.Append('+'); + break; + case Operation.DELETE: + text.Append('-'); + break; + case Operation.EQUAL: + text.Append(' '); + break; + } + + text.Append(diff_match_patch.encodeURI(aDiff.text)).Append("\n"); + } + return text.ToString(); + } + } + + + /** + * Class containing the diff, match and patch methods. + * Also Contains the behaviour settings. + */ + public class diff_match_patch { + // Defaults. + // Set these on your diff_match_patch instance to override the defaults. + + // Number of seconds to map a diff before giving up (0 for infinity). + public float Diff_Timeout = 1.0f; + // Cost of an empty edit operation in terms of edit characters. + public short Diff_EditCost = 4; + // At what point is no match declared (0.0 = perfection, 1.0 = very loose). + public float Match_Threshold = 0.5f; + // How far to search for a match (0 = exact location, 1000+ = broad match). + // A match this many characters away from the expected location will add + // 1.0 to the score (0.0 is a perfect match). + public int Match_Distance = 1000; + // When deleting a large block of text (over ~64 characters), how close + // do the contents have to be to match the expected contents. (0.0 = + // perfection, 1.0 = very loose). Note that Match_Threshold controls + // how closely the end points of a delete need to match. + public float Patch_DeleteThreshold = 0.5f; + // Chunk size for context length. + public short Patch_Margin = 4; + + // The number of bits in an int. + private short Match_MaxBits = 32; + + + // DIFF FUNCTIONS + + + /** + * Find the differences between two texts. + * Run a faster, slightly less optimal diff. + * This method allows the 'checklines' of diff_main() to be optional. + * Most of the time checklines is wanted, so default to true. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @return List of Diff objects. + */ + public List diff_main(string text1, string text2) { + return diff_main(text1, text2, true); + } + + /** + * Find the differences between two texts. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @return List of Diff objects. + */ + public List diff_main(string text1, string text2, bool checklines) { + // Set a deadline by which time the diff must be complete. + DateTime deadline; + if (this.Diff_Timeout <= 0) { + deadline = DateTime.MaxValue; + } else { + deadline = DateTime.Now + + new TimeSpan(((long)(Diff_Timeout * 1000)) * 10000); + } + return diff_main(text1, text2, checklines, deadline); + } + + /** + * Find the differences between two texts. Simplifies the problem by + * stripping any common prefix or suffix off the texts before diffing. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. Used + * internally for recursive calls. Users should set DiffTimeout + * instead. + * @return List of Diff objects. + */ + private List diff_main(string text1, string text2, bool checklines, + DateTime deadline) { + // Check for null inputs not needed since null can't be passed in C#. + + // Check for equality (speedup). + List diffs; + if (text1 == text2) { + diffs = new List(); + if (text1.Length != 0) { + diffs.Add(new Diff(Operation.EQUAL, text1)); + } + return diffs; + } + + // Trim off common prefix (speedup). + int commonlength = diff_commonPrefix(text1, text2); + string commonprefix = text1.Substring(0, commonlength); + text1 = text1.Substring(commonlength); + text2 = text2.Substring(commonlength); + + // Trim off common suffix (speedup). + commonlength = diff_commonSuffix(text1, text2); + string commonsuffix = text1.Substring(text1.Length - commonlength); + text1 = text1.Substring(0, text1.Length - commonlength); + text2 = text2.Substring(0, text2.Length - commonlength); + + // Compute the diff on the middle block. + diffs = diff_compute(text1, text2, checklines, deadline); + + // Restore the prefix and suffix. + if (commonprefix.Length != 0) { + diffs.Insert(0, (new Diff(Operation.EQUAL, commonprefix))); + } + if (commonsuffix.Length != 0) { + diffs.Add(new Diff(Operation.EQUAL, commonsuffix)); + } + + diff_cleanupMerge(diffs); + return diffs; + } + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. + * @return List of Diff objects. + */ + private List diff_compute(string text1, string text2, + bool checklines, DateTime deadline) { + List diffs = new List(); + + if (text1.Length == 0) { + // Just add some text (speedup). + diffs.Add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + if (text2.Length == 0) { + // Just delete some text (speedup). + diffs.Add(new Diff(Operation.DELETE, text1)); + return diffs; + } + + string longtext = text1.Length > text2.Length ? text1 : text2; + string shorttext = text1.Length > text2.Length ? text2 : text1; + int i = longtext.IndexOf(shorttext, StringComparison.Ordinal); + if (i != -1) { + // Shorter text is inside the longer text (speedup). + Operation op = (text1.Length > text2.Length) ? + Operation.DELETE : Operation.INSERT; + diffs.Add(new Diff(op, longtext.Substring(0, i))); + diffs.Add(new Diff(Operation.EQUAL, shorttext)); + diffs.Add(new Diff(op, longtext.Substring(i + shorttext.Length))); + return diffs; + } + + if (shorttext.Length == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + diffs.Add(new Diff(Operation.DELETE, text1)); + diffs.Add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + // Check to see if the problem can be split in two. + string[] hm = diff_halfMatch(text1, text2); + if (hm != null) { + // A half-match was found, sort out the return data. + string text1_a = hm[0]; + string text1_b = hm[1]; + string text2_a = hm[2]; + string text2_b = hm[3]; + string mid_common = hm[4]; + // Send both pairs off for separate processing. + List diffs_a = diff_main(text1_a, text2_a, checklines, deadline); + List diffs_b = diff_main(text1_b, text2_b, checklines, deadline); + // Merge the results. + diffs = diffs_a; + diffs.Add(new Diff(Operation.EQUAL, mid_common)); + diffs.AddRange(diffs_b); + return diffs; + } + + if (checklines && text1.Length > 100 && text2.Length > 100) { + return diff_lineMode(text1, text2, deadline); + } + + return diff_bisect(text1, text2, deadline); + } + + /** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time when the diff should be complete by. + * @return List of Diff objects. + */ + private List diff_lineMode(string text1, string text2, + DateTime deadline) { + // Scan the text on a line-by-line basis first. + Object[] a = diff_linesToChars(text1, text2); + text1 = (string)a[0]; + text2 = (string)a[1]; + List linearray = (List)a[2]; + + List diffs = diff_main(text1, text2, false, deadline); + + // Convert the diff back to original text. + diff_charsToLines(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + diff_cleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.Add(new Diff(Operation.EQUAL, string.Empty)); + int pointer = 0; + int count_delete = 0; + int count_insert = 0; + string text_delete = string.Empty; + string text_insert = string.Empty; + while (pointer < diffs.Count) { + switch (diffs[pointer].operation) { + case Operation.INSERT: + count_insert++; + text_insert += diffs[pointer].text; + break; + case Operation.DELETE: + count_delete++; + text_delete += diffs[pointer].text; + break; + case Operation.EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + diffs.RemoveRange(pointer - count_delete - count_insert, + count_delete + count_insert); + pointer = pointer - count_delete - count_insert; + List subDiff = + this.diff_main(text_delete, text_insert, false, deadline); + diffs.InsertRange(pointer, subDiff); + pointer = pointer + subDiff.Count; + } + count_insert = 0; + count_delete = 0; + text_delete = string.Empty; + text_insert = string.Empty; + break; + default: + throw new ArgumentOutOfRangeException(); + } + pointer++; + } + diffs.RemoveAt(diffs.Count - 1); // Remove the dummy entry at the end. + + return diffs; + } + + /** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time at which to bail if not yet complete. + * @return List of Diff objects. + */ + protected List diff_bisect(string text1, string text2, + DateTime deadline) { + // Cache the text lengths to prevent multiple calls. + int text1_length = text1.Length; + int text2_length = text2.Length; + int max_d = (text1_length + text2_length + 1) / 2; + int v_offset = max_d; + int v_length = 2 * max_d; + int[] v1 = new int[v_length]; + int[] v2 = new int[v_length]; + for (int x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + int delta = text1_length - text2_length; + // If the total number of characters is odd, then the front path will + // collide with the reverse path. + bool front = (delta % 2 != 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + int k1start = 0; + int k1end = 0; + int k2start = 0; + int k2end = 0; + for (int d = 0; d < max_d; d++) { + // Bail out if deadline is reached. + if (DateTime.Now > deadline) { + break; + } + + // Walk the front path one step. + for (int k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + int k1_offset = v_offset + k1; + int x1; + if (k1 == -d || k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1]) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + int y1 = x1 - k1; + while (x1 < text1_length && y1 < text2_length + && text1[x1] == text2[y1]) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + int k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + int x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + + // Walk the reverse path one step. + for (int k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + int k2_offset = v_offset + k2; + int x2; + if (k2 == -d || k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1]) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + int y2 = x2 - k2; + while (x2 < text1_length && y2 < text2_length + && text1[text1_length - x2 - 1] + == text2[text2_length - y2 - 1]) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + int k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + int x1 = v1[k1_offset]; + int y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + List diffs = new List(); + diffs.Add(new Diff(Operation.DELETE, text1)); + diffs.Add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + /** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param x Index of split point in text1. + * @param y Index of split point in text2. + * @param deadline Time at which to bail if not yet complete. + * @return LinkedList of Diff objects. + */ + private List diff_bisectSplit(string text1, string text2, + int x, int y, DateTime deadline) { + string text1a = text1.Substring(0, x); + string text2a = text2.Substring(0, y); + string text1b = text1.Substring(x); + string text2b = text2.Substring(y); + + // Compute both diffs serially. + List diffs = diff_main(text1a, text2a, false, deadline); + List diffsb = diff_main(text1b, text2b, false, deadline); + + diffs.AddRange(diffsb); + return diffs; + } + + /** + * Split two texts into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text1 First string. + * @param text2 Second string. + * @return Three element Object array, containing the encoded text1, the + * encoded text2 and the List of unique strings. The zeroth element + * of the List of unique strings is intentionally blank. + */ + protected Object[] diff_linesToChars(string text1, string text2) { + List lineArray = new List(); + Dictionary lineHash = new Dictionary(); + // e.g. linearray[4] == "Hello\n" + // e.g. linehash.get("Hello\n") == 4 + + // "\x00" is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray.Add(string.Empty); + + // Allocate 2/3rds of the space for text1, the rest for text2. + string chars1 = diff_linesToCharsMunge(text1, lineArray, lineHash, 40000); + string chars2 = diff_linesToCharsMunge(text2, lineArray, lineHash, 65535); + return new Object[] { chars1, chars2, lineArray }; + } + + /** + * Split a text into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text String to encode. + * @param lineArray List of unique strings. + * @param lineHash Map of strings to indices. + * @param maxLines Maximum length of lineArray. + * @return Encoded string. + */ + private string diff_linesToCharsMunge(string text, List lineArray, + Dictionary lineHash, int maxLines) { + int lineStart = 0; + int lineEnd = -1; + string line; + StringBuilder chars = new StringBuilder(); + // Walk the text, pulling out a Substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + while (lineEnd < text.Length - 1) { + lineEnd = text.IndexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = text.Length - 1; + } + line = text.JavaSubstring(lineStart, lineEnd + 1); + + if (lineHash.ContainsKey(line)) { + chars.Append(((char)(int)lineHash[line])); + } else { + if (lineArray.Count == maxLines) { + // Bail out at 65535 because char 65536 == char 0. + line = text.Substring(lineStart); + lineEnd = text.Length; + } + lineArray.Add(line); + lineHash.Add(line, lineArray.Count - 1); + chars.Append(((char)(lineArray.Count - 1))); + } + lineStart = lineEnd + 1; + } + return chars.ToString(); + } + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines + * of text. + * @param diffs List of Diff objects. + * @param lineArray List of unique strings. + */ + protected void diff_charsToLines(ICollection diffs, + IList lineArray) { + StringBuilder text; + foreach (Diff diff in diffs) { + text = new StringBuilder(); + for (int j = 0; j < diff.text.Length; j++) { + text.Append(lineArray[diff.text[j]]); + } + diff.text = text.ToString(); + } + } + + /** + * Determine the common prefix of two strings. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the start of each string. + */ + public int diff_commonPrefix(string text1, string text2) { + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + int n = Math.Min(text1.Length, text2.Length); + for (int i = 0; i < n; i++) { + if (text1[i] != text2[i]) { + return i; + } + } + return n; + } + + /** + * Determine the common suffix of two strings. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of each string. + */ + public int diff_commonSuffix(string text1, string text2) { + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + int text1_length = text1.Length; + int text2_length = text2.Length; + int n = Math.Min(text1.Length, text2.Length); + for (int i = 1; i <= n; i++) { + if (text1[text1_length - i] != text2[text2_length - i]) { + return i - 1; + } + } + return n; + } + + /** + * Determine if the suffix of one string is the prefix of another. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of the first + * string and the start of the second string. + */ + protected int diff_commonOverlap(string text1, string text2) { + // Cache the text lengths to prevent multiple calls. + int text1_length = text1.Length; + int text2_length = text2.Length; + // Eliminate the null case. + if (text1_length == 0 || text2_length == 0) { + return 0; + } + // Truncate the longer string. + if (text1_length > text2_length) { + text1 = text1.Substring(text1_length - text2_length); + } else if (text1_length < text2_length) { + text2 = text2.Substring(0, text1_length); + } + int text_length = Math.Min(text1_length, text2_length); + // Quick check for the worst case. + if (text1 == text2) { + return text_length; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: https://neil.fraser.name/news/2010/11/04/ + int best = 0; + int length = 1; + while (true) { + string pattern = text1.Substring(text_length - length); + int found = text2.IndexOf(pattern, StringComparison.Ordinal); + if (found == -1) { + return best; + } + length += found; + if (found == 0 || text1.Substring(text_length - length) == + text2.Substring(0, length)) { + best = length; + length++; + } + } + } + + /** + * Do the two texts share a Substring which is at least half the length of + * the longer text? + * This speedup can produce non-minimal diffs. + * @param text1 First string. + * @param text2 Second string. + * @return Five element String array, containing the prefix of text1, the + * suffix of text1, the prefix of text2, the suffix of text2 and the + * common middle. Or null if there was no match. + */ + + protected string[] diff_halfMatch(string text1, string text2) { + if (this.Diff_Timeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return null; + } + string longtext = text1.Length > text2.Length ? text1 : text2; + string shorttext = text1.Length > text2.Length ? text2 : text1; + if (longtext.Length < 4 || shorttext.Length * 2 < longtext.Length) { + return null; // Pointless. + } + + // First check if the second quarter is the seed for a half-match. + string[] hm1 = diff_halfMatchI(longtext, shorttext, + (longtext.Length + 3) / 4); + // Check again based on the third quarter. + string[] hm2 = diff_halfMatchI(longtext, shorttext, + (longtext.Length + 1) / 2); + string[] hm; + if (hm1 == null && hm2 == null) { + return null; + } else if (hm2 == null) { + hm = hm1; + } else if (hm1 == null) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].Length > hm2[4].Length ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + if (text1.Length > text2.Length) { + return hm; + //return new string[]{hm[0], hm[1], hm[2], hm[3], hm[4]}; + } else { + return new string[] { hm[2], hm[3], hm[0], hm[1], hm[4] }; + } + } + + /** + * Does a Substring of shorttext exist within longtext such that the + * Substring is at least half the length of longtext? + * @param longtext Longer string. + * @param shorttext Shorter string. + * @param i Start index of quarter length Substring within longtext. + * @return Five element string array, containing the prefix of longtext, the + * suffix of longtext, the prefix of shorttext, the suffix of shorttext + * and the common middle. Or null if there was no match. + */ + private string[] diff_halfMatchI(string longtext, string shorttext, int i) { + // Start with a 1/4 length Substring at position i as a seed. + string seed = longtext.Substring(i, longtext.Length / 4); + int j = -1; + string best_common = string.Empty; + string best_longtext_a = string.Empty, best_longtext_b = string.Empty; + string best_shorttext_a = string.Empty, best_shorttext_b = string.Empty; + while (j < shorttext.Length && (j = shorttext.IndexOf(seed, j + 1, + StringComparison.Ordinal)) != -1) { + int prefixLength = diff_commonPrefix(longtext.Substring(i), + shorttext.Substring(j)); + int suffixLength = diff_commonSuffix(longtext.Substring(0, i), + shorttext.Substring(0, j)); + if (best_common.Length < suffixLength + prefixLength) { + best_common = shorttext.Substring(j - suffixLength, suffixLength) + + shorttext.Substring(j, prefixLength); + best_longtext_a = longtext.Substring(0, i - suffixLength); + best_longtext_b = longtext.Substring(i + prefixLength); + best_shorttext_a = shorttext.Substring(0, j - suffixLength); + best_shorttext_b = shorttext.Substring(j + prefixLength); + } + } + if (best_common.Length * 2 >= longtext.Length) { + return new string[]{best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common}; + } else { + return null; + } + } + + /** + * Reduce the number of edits by eliminating semantically trivial + * equalities. + * @param diffs List of Diff objects. + */ + public void diff_cleanupSemantic(List diffs) { + bool changes = false; + // Stack of indices where equalities are found. + Stack equalities = new Stack(); + // Always equal to equalities[equalitiesLength-1][1] + string lastEquality = null; + int pointer = 0; // Index of current position. + // Number of characters that changed prior to the equality. + int length_insertions1 = 0; + int length_deletions1 = 0; + // Number of characters that changed after the equality. + int length_insertions2 = 0; + int length_deletions2 = 0; + while (pointer < diffs.Count) { + if (diffs[pointer].operation == Operation.EQUAL) { // Equality found. + equalities.Push(pointer); + length_insertions1 = length_insertions2; + length_deletions1 = length_deletions2; + length_insertions2 = 0; + length_deletions2 = 0; + lastEquality = diffs[pointer].text; + } else { // an insertion or deletion + if (diffs[pointer].operation == Operation.INSERT) { + length_insertions2 += diffs[pointer].text.Length; + } else { + length_deletions2 += diffs[pointer].text.Length; + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastEquality != null && (lastEquality.Length + <= Math.Max(length_insertions1, length_deletions1)) + && (lastEquality.Length + <= Math.Max(length_insertions2, length_deletions2))) { + // Duplicate record. + diffs.Insert(equalities.Peek(), + new Diff(Operation.DELETE, lastEquality)); + // Change second copy to insert. + diffs[equalities.Peek() + 1].operation = Operation.INSERT; + // Throw away the equality we just deleted. + equalities.Pop(); + if (equalities.Count > 0) { + equalities.Pop(); + } + pointer = equalities.Count > 0 ? equalities.Peek() : -1; + length_insertions1 = 0; // Reset the counters. + length_deletions1 = 0; + length_insertions2 = 0; + length_deletions2 = 0; + lastEquality = null; + changes = true; + } + } + pointer++; + } + + // Normalize the diff. + if (changes) { + diff_cleanupMerge(diffs); + } + diff_cleanupSemanticLossless(diffs); + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1; + while (pointer < diffs.Count) { + if (diffs[pointer - 1].operation == Operation.DELETE && + diffs[pointer].operation == Operation.INSERT) { + string deletion = diffs[pointer - 1].text; + string insertion = diffs[pointer].text; + int overlap_length1 = diff_commonOverlap(deletion, insertion); + int overlap_length2 = diff_commonOverlap(insertion, deletion); + if (overlap_length1 >= overlap_length2) { + if (overlap_length1 >= deletion.Length / 2.0 || + overlap_length1 >= insertion.Length / 2.0) { + // Overlap found. + // Insert an equality and trim the surrounding edits. + diffs.Insert(pointer, new Diff(Operation.EQUAL, + insertion.Substring(0, overlap_length1))); + diffs[pointer - 1].text = + deletion.Substring(0, deletion.Length - overlap_length1); + diffs[pointer + 1].text = insertion.Substring(overlap_length1); + pointer++; + } + } else { + if (overlap_length2 >= deletion.Length / 2.0 || + overlap_length2 >= insertion.Length / 2.0) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + diffs.Insert(pointer, new Diff(Operation.EQUAL, + deletion.Substring(0, overlap_length2))); + diffs[pointer - 1].operation = Operation.INSERT; + diffs[pointer - 1].text = + insertion.Substring(0, insertion.Length - overlap_length2); + diffs[pointer + 1].operation = Operation.DELETE; + diffs[pointer + 1].text = deletion.Substring(overlap_length2); + pointer++; + } + } + pointer++; + } + pointer++; + } + } + + /** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * @param diffs List of Diff objects. + */ + public void diff_cleanupSemanticLossless(List diffs) { + int pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.Count - 1) { + if (diffs[pointer - 1].operation == Operation.EQUAL && + diffs[pointer + 1].operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + string equality1 = diffs[pointer - 1].text; + string edit = diffs[pointer].text; + string equality2 = diffs[pointer + 1].text; + + // First, shift the edit as far left as possible. + int commonOffset = this.diff_commonSuffix(equality1, edit); + if (commonOffset > 0) { + string commonString = edit.Substring(edit.Length - commonOffset); + equality1 = equality1.Substring(0, equality1.Length - commonOffset); + edit = commonString + edit.Substring(0, edit.Length - commonOffset); + equality2 = commonString + equality2; + } + + // Second, step character by character right, + // looking for the best fit. + string bestEquality1 = equality1; + string bestEdit = edit; + string bestEquality2 = equality2; + int bestScore = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + while (edit.Length != 0 && equality2.Length != 0 + && edit[0] == equality2[0]) { + equality1 += edit[0]; + edit = edit.Substring(1) + equality2[0]; + equality2 = equality2.Substring(1); + int score = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + // The >= encourages trailing rather than leading whitespace on + // edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (diffs[pointer - 1].text != bestEquality1) { + // We have an improvement, save it back to the diff. + if (bestEquality1.Length != 0) { + diffs[pointer - 1].text = bestEquality1; + } else { + diffs.RemoveAt(pointer - 1); + pointer--; + } + diffs[pointer].text = bestEdit; + if (bestEquality2.Length != 0) { + diffs[pointer + 1].text = bestEquality2; + } else { + diffs.RemoveAt(pointer + 1); + pointer--; + } + } + } + pointer++; + } + } + + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 6 (best) to 0 (worst). + * @param one First string. + * @param two Second string. + * @return The score. + */ + private int diff_cleanupSemanticScore(string one, string two) { + if (one.Length == 0 || two.Length == 0) { + // Edges are the best. + return 6; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + char char1 = one[one.Length - 1]; + char char2 = two[0]; + bool nonAlphaNumeric1 = !Char.IsLetterOrDigit(char1); + bool nonAlphaNumeric2 = !Char.IsLetterOrDigit(char2); + bool whitespace1 = nonAlphaNumeric1 && Char.IsWhiteSpace(char1); + bool whitespace2 = nonAlphaNumeric2 && Char.IsWhiteSpace(char2); + bool lineBreak1 = whitespace1 && Char.IsControl(char1); + bool lineBreak2 = whitespace2 && Char.IsControl(char2); + bool blankLine1 = lineBreak1 && BLANKLINEEND.IsMatch(one); + bool blankLine2 = lineBreak2 && BLANKLINESTART.IsMatch(two); + + if (blankLine1 || blankLine2) { + // Five points for blank lines. + return 5; + } else if (lineBreak1 || lineBreak2) { + // Four points for line breaks. + return 4; + } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { + // Three points for end of sentences. + return 3; + } else if (whitespace1 || whitespace2) { + // Two points for whitespace. + return 2; + } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { + // One point for non-alphanumeric. + return 1; + } + return 0; + } + + // Define some regex patterns for matching boundaries. + private Regex BLANKLINEEND = new Regex("\\n\\r?\\n\\Z"); + private Regex BLANKLINESTART = new Regex("\\A\\r?\\n\\r?\\n"); + + /** + * Reduce the number of edits by eliminating operationally trivial + * equalities. + * @param diffs List of Diff objects. + */ + public void diff_cleanupEfficiency(List diffs) { + bool changes = false; + // Stack of indices where equalities are found. + Stack equalities = new Stack(); + // Always equal to equalities[equalitiesLength-1][1] + string lastEquality = string.Empty; + int pointer = 0; // Index of current position. + // Is there an insertion operation before the last equality. + bool pre_ins = false; + // Is there a deletion operation before the last equality. + bool pre_del = false; + // Is there an insertion operation after the last equality. + bool post_ins = false; + // Is there a deletion operation after the last equality. + bool post_del = false; + while (pointer < diffs.Count) { + if (diffs[pointer].operation == Operation.EQUAL) { // Equality found. + if (diffs[pointer].text.Length < this.Diff_EditCost + && (post_ins || post_del)) { + // Candidate found. + equalities.Push(pointer); + pre_ins = post_ins; + pre_del = post_del; + lastEquality = diffs[pointer].text; + } else { + // Not a candidate, and can never become one. + equalities.Clear(); + lastEquality = string.Empty; + } + post_ins = post_del = false; + } else { // An insertion or deletion. + if (diffs[pointer].operation == Operation.DELETE) { + post_del = true; + } else { + post_ins = true; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if ((lastEquality.Length != 0) + && ((pre_ins && pre_del && post_ins && post_del) + || ((lastEquality.Length < this.Diff_EditCost / 2) + && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0) + (post_ins ? 1 : 0) + + (post_del ? 1 : 0)) == 3))) { + // Duplicate record. + diffs.Insert(equalities.Peek(), + new Diff(Operation.DELETE, lastEquality)); + // Change second copy to insert. + diffs[equalities.Peek() + 1].operation = Operation.INSERT; + equalities.Pop(); // Throw away the equality we just deleted. + lastEquality = string.Empty; + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = true; + equalities.Clear(); + } else { + if (equalities.Count > 0) { + equalities.Pop(); + } + + pointer = equalities.Count > 0 ? equalities.Peek() : -1; + post_ins = post_del = false; + } + changes = true; + } + } + pointer++; + } + + if (changes) { + diff_cleanupMerge(diffs); + } + } + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param diffs List of Diff objects. + */ + public void diff_cleanupMerge(List diffs) { + // Add a dummy entry at the end. + diffs.Add(new Diff(Operation.EQUAL, string.Empty)); + int pointer = 0; + int count_delete = 0; + int count_insert = 0; + string text_delete = string.Empty; + string text_insert = string.Empty; + int commonlength; + while (pointer < diffs.Count) { + switch (diffs[pointer].operation) { + case Operation.INSERT: + count_insert++; + text_insert += diffs[pointer].text; + pointer++; + break; + case Operation.DELETE: + count_delete++; + text_delete += diffs[pointer].text; + pointer++; + break; + case Operation.EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete + count_insert > 1) { + if (count_delete != 0 && count_insert != 0) { + // Factor out any common prefixies. + commonlength = this.diff_commonPrefix(text_insert, text_delete); + if (commonlength != 0) { + if ((pointer - count_delete - count_insert) > 0 && + diffs[pointer - count_delete - count_insert - 1].operation + == Operation.EQUAL) { + diffs[pointer - count_delete - count_insert - 1].text + += text_insert.Substring(0, commonlength); + } else { + diffs.Insert(0, new Diff(Operation.EQUAL, + text_insert.Substring(0, commonlength))); + pointer++; + } + text_insert = text_insert.Substring(commonlength); + text_delete = text_delete.Substring(commonlength); + } + // Factor out any common suffixies. + commonlength = this.diff_commonSuffix(text_insert, text_delete); + if (commonlength != 0) { + diffs[pointer].text = text_insert.Substring(text_insert.Length + - commonlength) + diffs[pointer].text; + text_insert = text_insert.Substring(0, text_insert.Length + - commonlength); + text_delete = text_delete.Substring(0, text_delete.Length + - commonlength); + } + } + // Delete the offending records and add the merged ones. + pointer -= count_delete + count_insert; + diffs.Splice(pointer, count_delete + count_insert); + if (text_delete.Length != 0) { + diffs.Splice(pointer, 0, + new Diff(Operation.DELETE, text_delete)); + pointer++; + } + if (text_insert.Length != 0) { + diffs.Splice(pointer, 0, + new Diff(Operation.INSERT, text_insert)); + pointer++; + } + pointer++; + } else if (pointer != 0 + && diffs[pointer - 1].operation == Operation.EQUAL) { + // Merge this equality with the previous one. + diffs[pointer - 1].text += diffs[pointer].text; + diffs.RemoveAt(pointer); + } else { + pointer++; + } + count_insert = 0; + count_delete = 0; + text_delete = string.Empty; + text_insert = string.Empty; + break; + } + } + if (diffs[diffs.Count - 1].text.Length == 0) { + diffs.RemoveAt(diffs.Count - 1); // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by + // equalities which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + bool changes = false; + pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < (diffs.Count - 1)) { + if (diffs[pointer - 1].operation == Operation.EQUAL && + diffs[pointer + 1].operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + if (diffs[pointer].text.EndsWith(diffs[pointer - 1].text, + StringComparison.Ordinal)) { + // Shift the edit over the previous equality. + diffs[pointer].text = diffs[pointer - 1].text + + diffs[pointer].text.Substring(0, diffs[pointer].text.Length - + diffs[pointer - 1].text.Length); + diffs[pointer + 1].text = diffs[pointer - 1].text + + diffs[pointer + 1].text; + diffs.Splice(pointer - 1, 1); + changes = true; + } else if (diffs[pointer].text.StartsWith(diffs[pointer + 1].text, + StringComparison.Ordinal)) { + // Shift the edit over the next equality. + diffs[pointer - 1].text += diffs[pointer + 1].text; + diffs[pointer].text = + diffs[pointer].text.Substring(diffs[pointer + 1].text.Length) + + diffs[pointer + 1].text; + diffs.Splice(pointer + 1, 1); + changes = true; + } + } + pointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + this.diff_cleanupMerge(diffs); + } + } + + /** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. "The cat" vs "The big cat", 1->1, 5->8 + * @param diffs List of Diff objects. + * @param loc Location within text1. + * @return Location within text2. + */ + public int diff_xIndex(List diffs, int loc) { + int chars1 = 0; + int chars2 = 0; + int last_chars1 = 0; + int last_chars2 = 0; + Diff lastDiff = null; + foreach (Diff aDiff in diffs) { + if (aDiff.operation != Operation.INSERT) { + // Equality or deletion. + chars1 += aDiff.text.Length; + } + if (aDiff.operation != Operation.DELETE) { + // Equality or insertion. + chars2 += aDiff.text.Length; + } + if (chars1 > loc) { + // Overshot the location. + lastDiff = aDiff; + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + if (lastDiff != null && lastDiff.operation == Operation.DELETE) { + // The location was deleted. + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); + } + + /** + * Convert a Diff list into a pretty HTML report. + * @param diffs List of Diff objects. + * @return HTML representation. + */ + public string diff_prettyHtml(List diffs) { + StringBuilder html = new StringBuilder(); + foreach (Diff aDiff in diffs) { + string text = aDiff.text.Replace("&", "&").Replace("<", "<") + .Replace(">", ">").Replace("\n", "¶
"); + switch (aDiff.operation) { + case Operation.INSERT: + html.Append("").Append(text) + .Append(""); + break; + case Operation.DELETE: + html.Append("").Append(text) + .Append(""); + break; + case Operation.EQUAL: + html.Append("").Append(text).Append(""); + break; + } + } + return html.ToString(); + } + + /** + * Compute and return the source text (all equalities and deletions). + * @param diffs List of Diff objects. + * @return Source text. + */ + public string diff_text1(List diffs) { + StringBuilder text = new StringBuilder(); + foreach (Diff aDiff in diffs) { + if (aDiff.operation != Operation.INSERT) { + text.Append(aDiff.text); + } + } + return text.ToString(); + } + + /** + * Compute and return the destination text (all equalities and insertions). + * @param diffs List of Diff objects. + * @return Destination text. + */ + public string diff_text2(List diffs) { + StringBuilder text = new StringBuilder(); + foreach (Diff aDiff in diffs) { + if (aDiff.operation != Operation.DELETE) { + text.Append(aDiff.text); + } + } + return text.ToString(); + } + + /** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param diffs List of Diff objects. + * @return Number of changes. + */ + public int diff_levenshtein(List diffs) { + int levenshtein = 0; + int insertions = 0; + int deletions = 0; + foreach (Diff aDiff in diffs) { + switch (aDiff.operation) { + case Operation.INSERT: + insertions += aDiff.text.Length; + break; + case Operation.DELETE: + deletions += aDiff.text.Length; + break; + case Operation.EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += Math.Max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += Math.Max(insertions, deletions); + return levenshtein; + } + + /** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx + * notation. + * @param diffs Array of Diff objects. + * @return Delta text. + */ + public string diff_toDelta(List diffs) { + StringBuilder text = new StringBuilder(); + foreach (Diff aDiff in diffs) { + switch (aDiff.operation) { + case Operation.INSERT: + text.Append("+").Append(encodeURI(aDiff.text)).Append("\t"); + break; + case Operation.DELETE: + text.Append("-").Append(aDiff.text.Length).Append("\t"); + break; + case Operation.EQUAL: + text.Append("=").Append(aDiff.text.Length).Append("\t"); + break; + } + } + string delta = text.ToString(); + if (delta.Length != 0) { + // Strip off trailing tab character. + delta = delta.Substring(0, delta.Length - 1); + } + return delta; + } + + /** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * @param text1 Source string for the diff. + * @param delta Delta text. + * @return Array of Diff objects or null if invalid. + * @throws ArgumentException If invalid input. + */ + public List diff_fromDelta(string text1, string delta) { + List diffs = new List(); + int pointer = 0; // Cursor in text1 + string[] tokens = delta.Split(new string[] { "\t" }, + StringSplitOptions.None); + foreach (string token in tokens) { + if (token.Length == 0) { + // Blank tokens are ok (from a trailing \t). + continue; + } + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + string param = token.Substring(1); + switch (token[0]) { + case '+': + // decode would change all "+" to " " + param = param.Replace("+", "%2b"); + + param = HttpUtility.UrlDecode(param); + //} catch (UnsupportedEncodingException e) { + // // Not likely on modern system. + // throw new Error("This system does not support UTF-8.", e); + //} catch (IllegalArgumentException e) { + // // Malformed URI sequence. + // throw new IllegalArgumentException( + // "Illegal escape in diff_fromDelta: " + param, e); + //} + diffs.Add(new Diff(Operation.INSERT, param)); + break; + case '-': + // Fall through. + case '=': + int n; + try { + n = Convert.ToInt32(param); + } catch (FormatException e) { + throw new ArgumentException( + "Invalid number in diff_fromDelta: " + param, e); + } + if (n < 0) { + throw new ArgumentException( + "Negative number in diff_fromDelta: " + param); + } + string text; + try { + text = text1.Substring(pointer, n); + pointer += n; + } catch (ArgumentOutOfRangeException e) { + throw new ArgumentException("Delta length (" + pointer + + ") larger than source text length (" + text1.Length + + ").", e); + } + if (token[0] == '=') { + diffs.Add(new Diff(Operation.EQUAL, text)); + } else { + diffs.Add(new Diff(Operation.DELETE, text)); + } + break; + default: + // Anything else is an error. + throw new ArgumentException( + "Invalid diff operation in diff_fromDelta: " + token[0]); + } + } + if (pointer != text1.Length) { + throw new ArgumentException("Delta length (" + pointer + + ") smaller than source text length (" + text1.Length + ")."); + } + return diffs; + } + + + // MATCH FUNCTIONS + + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + public int match_main(string text, string pattern, int loc) { + // Check for null inputs not needed since null can't be passed in C#. + + loc = Math.Max(0, Math.Min(loc, text.Length)); + if (text == pattern) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (text.Length == 0) { + // Nothing to match. + return -1; + } else if (loc + pattern.Length <= text.Length + && text.Substring(loc, pattern.Length) == pattern) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return match_bitap(text, pattern, loc); + } + } + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + protected int match_bitap(string text, string pattern, int loc) { + // assert (Match_MaxBits == 0 || pattern.Length <= Match_MaxBits) + // : "Pattern too long for this application."; + + // Initialise the alphabet. + Dictionary s = match_alphabet(pattern); + + // Highest score beyond which we give up. + double score_threshold = Match_Threshold; + // Is there a nearby exact match? (speedup) + int best_loc = text.IndexOf(pattern, loc, StringComparison.Ordinal); + if (best_loc != -1) { + score_threshold = Math.Min(match_bitapScore(0, best_loc, loc, + pattern), score_threshold); + // What about in the other direction? (speedup) + best_loc = text.LastIndexOf(pattern, + Math.Min(loc + pattern.Length, text.Length), + StringComparison.Ordinal); + if (best_loc != -1) { + score_threshold = Math.Min(match_bitapScore(0, best_loc, loc, + pattern), score_threshold); + } + } + + // Initialise the bit arrays. + int matchmask = 1 << (pattern.Length - 1); + best_loc = -1; + + int bin_min, bin_mid; + int bin_max = pattern.Length + text.Length; + // Empty initialization added to appease C# compiler. + int[] last_rd = new int[0]; + for (int d = 0; d < pattern.Length; d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at + // this error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (match_bitapScore(d, loc + bin_mid, loc, pattern) + <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = (bin_max - bin_min) / 2 + bin_min; + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + int start = Math.Max(1, loc - bin_mid + 1); + int finish = Math.Min(loc + bin_mid, text.Length) + pattern.Length; + + int[] rd = new int[finish + 2]; + rd[finish + 1] = (1 << d) - 1; + for (int j = finish; j >= start; j--) { + int charMatch; + if (text.Length <= j - 1 || !s.ContainsKey(text[j - 1])) { + // Out of range. + charMatch = 0; + } else { + charMatch = s[text[j - 1]]; + } + if (d == 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch + | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]; + } + if ((rd[j] & matchmask) != 0) { + double score = match_bitapScore(d, j - 1, loc, pattern); + // This match will almost certainly be better than any existing + // match. But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.Max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + if (match_bitapScore(d + 1, loc, loc, pattern) > score_threshold) { + // No hope for a (better) match at greater error levels. + break; + } + last_rd = rd; + } + return best_loc; + } + + /** + * Compute and return the score for a match with e errors and x location. + * @param e Number of errors in match. + * @param x Location of match. + * @param loc Expected location of match. + * @param pattern Pattern being sought. + * @return Overall score for match (0.0 = good, 1.0 = bad). + */ + private double match_bitapScore(int e, int x, int loc, string pattern) { + float accuracy = (float)e / pattern.Length; + int proximity = Math.Abs(loc - x); + if (Match_Distance == 0) { + // Dodge divide by zero error. + return proximity == 0 ? accuracy : 1.0; + } + return accuracy + (proximity / (float) Match_Distance); + } + + /** + * Initialise the alphabet for the Bitap algorithm. + * @param pattern The text to encode. + * @return Hash of character locations. + */ + protected Dictionary match_alphabet(string pattern) { + Dictionary s = new Dictionary(); + char[] char_pattern = pattern.ToCharArray(); + foreach (char c in char_pattern) { + if (!s.ContainsKey(c)) { + s.Add(c, 0); + } + } + int i = 0; + foreach (char c in char_pattern) { + int value = s[c] | (1 << (pattern.Length - i - 1)); + s[c] = value; + i++; + } + return s; + } + + + // PATCH FUNCTIONS + + + /** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param patch The patch to grow. + * @param text Source text. + */ + protected void patch_addContext(Patch patch, string text) { + if (text.Length == 0) { + return; + } + string pattern = text.Substring(patch.start2, patch.length1); + int padding = 0; + + // Look for the first and last matches of pattern in text. If two + // different matches are found, increase the pattern length. + while (text.IndexOf(pattern, StringComparison.Ordinal) + != text.LastIndexOf(pattern, StringComparison.Ordinal) + && pattern.Length < Match_MaxBits - Patch_Margin - Patch_Margin) { + padding += Patch_Margin; + pattern = text.JavaSubstring(Math.Max(0, patch.start2 - padding), + Math.Min(text.Length, patch.start2 + patch.length1 + padding)); + } + // Add one chunk for good luck. + padding += Patch_Margin; + + // Add the prefix. + string prefix = text.JavaSubstring(Math.Max(0, patch.start2 - padding), + patch.start2); + if (prefix.Length != 0) { + patch.diffs.Insert(0, new Diff(Operation.EQUAL, prefix)); + } + // Add the suffix. + string suffix = text.JavaSubstring(patch.start2 + patch.length1, + Math.Min(text.Length, patch.start2 + patch.length1 + padding)); + if (suffix.Length != 0) { + patch.diffs.Add(new Diff(Operation.EQUAL, suffix)); + } + + // Roll back the start points. + patch.start1 -= prefix.Length; + patch.start2 -= prefix.Length; + // Extend the lengths. + patch.length1 += prefix.Length + suffix.Length; + patch.length2 += prefix.Length + suffix.Length; + } + + /** + * Compute a list of patches to turn text1 into text2. + * A set of diffs will be computed. + * @param text1 Old text. + * @param text2 New text. + * @return List of Patch objects. + */ + public List patch_make(string text1, string text2) { + // Check for null inputs not needed since null can't be passed in C#. + // No diffs provided, compute our own. + List diffs = diff_main(text1, text2, true); + if (diffs.Count > 2) { + diff_cleanupSemantic(diffs); + diff_cleanupEfficiency(diffs); + } + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text1 will be derived from the provided diffs. + * @param diffs Array of Diff objects for text1 to text2. + * @return List of Patch objects. + */ + public List patch_make(List diffs) { + // Check for null inputs not needed since null can't be passed in C#. + // No origin string provided, compute our own. + string text1 = diff_text1(diffs); + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is ignored, diffs are the delta between text1 and text2. + * @param text1 Old text + * @param text2 Ignored. + * @param diffs Array of Diff objects for text1 to text2. + * @return List of Patch objects. + * @deprecated Prefer patch_make(string text1, List diffs). + */ + public List patch_make(string text1, string text2, + List diffs) { + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is not provided, diffs are the delta between text1 and text2. + * @param text1 Old text. + * @param diffs Array of Diff objects for text1 to text2. + * @return List of Patch objects. + */ + public List patch_make(string text1, List diffs) { + // Check for null inputs not needed since null can't be passed in C#. + List patches = new List(); + if (diffs.Count == 0) { + return patches; // Get rid of the null case. + } + Patch patch = new Patch(); + int char_count1 = 0; // Number of characters into the text1 string. + int char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + string prepatch_text = text1; + string postpatch_text = text1; + foreach (Diff aDiff in diffs) { + if (patch.diffs.Count == 0 && aDiff.operation != Operation.EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (aDiff.operation) { + case Operation.INSERT: + patch.diffs.Add(aDiff); + patch.length2 += aDiff.text.Length; + postpatch_text = postpatch_text.Insert(char_count2, aDiff.text); + break; + case Operation.DELETE: + patch.length1 += aDiff.text.Length; + patch.diffs.Add(aDiff); + postpatch_text = postpatch_text.Remove(char_count2, + aDiff.text.Length); + break; + case Operation.EQUAL: + if (aDiff.text.Length <= 2 * Patch_Margin + && patch.diffs.Count() != 0 && aDiff != diffs.Last()) { + // Small equality inside a patch. + patch.diffs.Add(aDiff); + patch.length1 += aDiff.text.Length; + patch.length2 += aDiff.text.Length; + } + + if (aDiff.text.Length >= 2 * Patch_Margin) { + // Time for a new patch. + if (patch.diffs.Count != 0) { + patch_addContext(patch, prepatch_text); + patches.Add(patch); + patch = new Patch(); + // Unlike Unidiff, our patch lists have a rolling context. + // https://github.com/google/diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (aDiff.operation != Operation.INSERT) { + char_count1 += aDiff.text.Length; + } + if (aDiff.operation != Operation.DELETE) { + char_count2 += aDiff.text.Length; + } + } + // Pick up the leftover patch if not empty. + if (patch.diffs.Count != 0) { + patch_addContext(patch, prepatch_text); + patches.Add(patch); + } + + return patches; + } + + /** + * Given an array of patches, return another array that is identical. + * @param patches Array of Patch objects. + * @return Array of Patch objects. + */ + public List patch_deepCopy(List patches) { + List patchesCopy = new List(); + foreach (Patch aPatch in patches) { + Patch patchCopy = new Patch(); + foreach (Diff aDiff in aPatch.diffs) { + Diff diffCopy = new Diff(aDiff.operation, aDiff.text); + patchCopy.diffs.Add(diffCopy); + } + patchCopy.start1 = aPatch.start1; + patchCopy.start2 = aPatch.start2; + patchCopy.length1 = aPatch.length1; + patchCopy.length2 = aPatch.length2; + patchesCopy.Add(patchCopy); + } + return patchesCopy; + } + + /** + * Merge a set of patches onto the text. Return a patched text, as well + * as an array of true/false values indicating which patches were applied. + * @param patches Array of Patch objects + * @param text Old text. + * @return Two element Object array, containing the new text and an array of + * bool values. + */ + public Object[] patch_apply(List patches, string text) { + if (patches.Count == 0) { + return new Object[] { text, new bool[0] }; + } + + // Deep copy the patches so that no changes are made to originals. + patches = patch_deepCopy(patches); + + string nullPadding = this.patch_addPadding(patches); + text = nullPadding + text + nullPadding; + patch_splitMax(patches); + + int x = 0; + // delta keeps track of the offset between the expected and actual + // location of the previous patch. If there are patches expected at + // positions 10 and 20, but the first patch was found at 12, delta is 2 + // and the second patch has an effective expected position of 22. + int delta = 0; + bool[] results = new bool[patches.Count]; + foreach (Patch aPatch in patches) { + int expected_loc = aPatch.start2 + delta; + string text1 = diff_text1(aPatch.diffs); + int start_loc; + int end_loc = -1; + if (text1.Length > this.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern + // in the case of a monster delete. + start_loc = match_main(text, + text1.Substring(0, this.Match_MaxBits), expected_loc); + if (start_loc != -1) { + end_loc = match_main(text, + text1.Substring(text1.Length - this.Match_MaxBits), + expected_loc + text1.Length - this.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = this.match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= aPatch.length2 - aPatch.length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + string text2; + if (end_loc == -1) { + text2 = text.JavaSubstring(start_loc, + Math.Min(start_loc + text1.Length, text.Length)); + } else { + text2 = text.JavaSubstring(start_loc, + Math.Min(end_loc + this.Match_MaxBits, text.Length)); + } + if (text1 == text2) { + // Perfect match, just shove the Replacement text in. + text = text.Substring(0, start_loc) + diff_text2(aPatch.diffs) + + text.Substring(start_loc + text1.Length); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + List diffs = diff_main(text1, text2, false); + if (text1.Length > this.Match_MaxBits + && this.diff_levenshtein(diffs) / (float) text1.Length + > this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + diff_cleanupSemanticLossless(diffs); + int index1 = 0; + foreach (Diff aDiff in aPatch.diffs) { + if (aDiff.operation != Operation.EQUAL) { + int index2 = diff_xIndex(diffs, index1); + if (aDiff.operation == Operation.INSERT) { + // Insertion + text = text.Insert(start_loc + index2, aDiff.text); + } else if (aDiff.operation == Operation.DELETE) { + // Deletion + text = text.Remove(start_loc + index2, diff_xIndex(diffs, + index1 + aDiff.text.Length) - index2); + } + } + if (aDiff.operation != Operation.DELETE) { + index1 += aDiff.text.Length; + } + } + } + } + } + x++; + } + // Strip the padding off. + text = text.Substring(nullPadding.Length, text.Length + - 2 * nullPadding.Length); + return new Object[] { text, results }; + } + + /** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param patches Array of Patch objects. + * @return The padding string added to each side. + */ + public string patch_addPadding(List patches) { + short paddingLength = this.Patch_Margin; + string nullPadding = string.Empty; + for (short x = 1; x <= paddingLength; x++) { + nullPadding += (char)x; + } + + // Bump all the patches forward. + foreach (Patch aPatch in patches) { + aPatch.start1 += paddingLength; + aPatch.start2 += paddingLength; + } + + // Add some padding on start of first diff. + Patch patch = patches.First(); + List diffs = patch.diffs; + if (diffs.Count == 0 || diffs.First().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.Insert(0, new Diff(Operation.EQUAL, nullPadding)); + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.First().text.Length) { + // Grow first equality. + Diff firstDiff = diffs.First(); + int extraLength = paddingLength - firstDiff.text.Length; + firstDiff.text = nullPadding.Substring(firstDiff.text.Length) + + firstDiff.text; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches.Last(); + diffs = patch.diffs; + if (diffs.Count == 0 || diffs.Last().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.Add(new Diff(Operation.EQUAL, nullPadding)); + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.Last().text.Length) { + // Grow last equality. + Diff lastDiff = diffs.Last(); + int extraLength = paddingLength - lastDiff.text.Length; + lastDiff.text += nullPadding.Substring(0, extraLength); + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding; + } + + /** + * Look through the patches and break up any which are longer than the + * maximum limit of the match algorithm. + * Intended to be called only from within patch_apply. + * @param patches List of Patch objects. + */ + public void patch_splitMax(List patches) { + short patch_size = this.Match_MaxBits; + for (int x = 0; x < patches.Count; x++) { + if (patches[x].length1 <= patch_size) { + continue; + } + Patch bigpatch = patches[x]; + // Remove the big old patch. + patches.Splice(x--, 1); + int start1 = bigpatch.start1; + int start2 = bigpatch.start2; + string precontext = string.Empty; + while (bigpatch.diffs.Count != 0) { + // Create one of several smaller patches. + Patch patch = new Patch(); + bool empty = true; + patch.start1 = start1 - precontext.Length; + patch.start2 = start2 - precontext.Length; + if (precontext.Length != 0) { + patch.length1 = patch.length2 = precontext.Length; + patch.diffs.Add(new Diff(Operation.EQUAL, precontext)); + } + while (bigpatch.diffs.Count != 0 + && patch.length1 < patch_size - this.Patch_Margin) { + Operation diff_type = bigpatch.diffs[0].operation; + string diff_text = bigpatch.diffs[0].text; + if (diff_type == Operation.INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.Length; + start2 += diff_text.Length; + patch.diffs.Add(bigpatch.diffs.First()); + bigpatch.diffs.RemoveAt(0); + empty = false; + } else if (diff_type == Operation.DELETE && patch.diffs.Count == 1 + && patch.diffs.First().operation == Operation.EQUAL + && diff_text.Length > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.Length; + start1 += diff_text.Length; + empty = false; + patch.diffs.Add(new Diff(diff_type, diff_text)); + bigpatch.diffs.RemoveAt(0); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.Substring(0, Math.Min(diff_text.Length, + patch_size - patch.length1 - Patch_Margin)); + patch.length1 += diff_text.Length; + start1 += diff_text.Length; + if (diff_type == Operation.EQUAL) { + patch.length2 += diff_text.Length; + start2 += diff_text.Length; + } else { + empty = false; + } + patch.diffs.Add(new Diff(diff_type, diff_text)); + if (diff_text == bigpatch.diffs[0].text) { + bigpatch.diffs.RemoveAt(0); + } else { + bigpatch.diffs[0].text = + bigpatch.diffs[0].text.Substring(diff_text.Length); + } + } + } + // Compute the head context for the next patch. + precontext = this.diff_text2(patch.diffs); + precontext = precontext.Substring(Math.Max(0, + precontext.Length - this.Patch_Margin)); + + string postcontext = null; + // Append the end context for this patch. + if (diff_text1(bigpatch.diffs).Length > Patch_Margin) { + postcontext = diff_text1(bigpatch.diffs) + .Substring(0, Patch_Margin); + } else { + postcontext = diff_text1(bigpatch.diffs); + } + + if (postcontext.Length != 0) { + patch.length1 += postcontext.Length; + patch.length2 += postcontext.Length; + if (patch.diffs.Count != 0 + && patch.diffs[patch.diffs.Count - 1].operation + == Operation.EQUAL) { + patch.diffs[patch.diffs.Count - 1].text += postcontext; + } else { + patch.diffs.Add(new Diff(Operation.EQUAL, postcontext)); + } + } + if (!empty) { + patches.Splice(++x, 0, patch); + } + } + } + } + + /** + * Take a list of patches and return a textual representation. + * @param patches List of Patch objects. + * @return Text representation of patches. + */ + public string patch_toText(List patches) { + StringBuilder text = new StringBuilder(); + foreach (Patch aPatch in patches) { + text.Append(aPatch); + } + return text.ToString(); + } + + /** + * Parse a textual representation of patches and return a List of Patch + * objects. + * @param textline Text representation of patches. + * @return List of Patch objects. + * @throws ArgumentException If invalid input. + */ + public List patch_fromText(string textline) { + List patches = new List(); + if (textline.Length == 0) { + return patches; + } + string[] text = textline.Split('\n'); + int textPointer = 0; + Patch patch; + Regex patchHeader + = new Regex("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$"); + Match m; + char sign; + string line; + while (textPointer < text.Length) { + m = patchHeader.Match(text[textPointer]); + if (!m.Success) { + throw new ArgumentException("Invalid patch string: " + + text[textPointer]); + } + patch = new Patch(); + patches.Add(patch); + patch.start1 = Convert.ToInt32(m.Groups[1].Value); + if (m.Groups[2].Length == 0) { + patch.start1--; + patch.length1 = 1; + } else if (m.Groups[2].Value == "0") { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = Convert.ToInt32(m.Groups[2].Value); + } + + patch.start2 = Convert.ToInt32(m.Groups[3].Value); + if (m.Groups[4].Length == 0) { + patch.start2--; + patch.length2 = 1; + } else if (m.Groups[4].Value == "0") { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = Convert.ToInt32(m.Groups[4].Value); + } + textPointer++; + + while (textPointer < text.Length) { + try { + sign = text[textPointer][0]; + } catch (IndexOutOfRangeException) { + // Blank line? Whatever. + textPointer++; + continue; + } + line = text[textPointer].Substring(1); + line = line.Replace("+", "%2b"); + line = HttpUtility.UrlDecode(line); + if (sign == '-') { + // Deletion. + patch.diffs.Add(new Diff(Operation.DELETE, line)); + } else if (sign == '+') { + // Insertion. + patch.diffs.Add(new Diff(Operation.INSERT, line)); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.Add(new Diff(Operation.EQUAL, line)); + } else if (sign == '@') { + // Start of next patch. + break; + } else { + // WTF? + throw new ArgumentException( + "Invalid patch mode '" + sign + "' in: " + line); + } + textPointer++; + } + } + return patches; + } + + /** + * Encodes a string with URI-style % escaping. + * Compatible with JavaScript's encodeURI function. + * + * @param str The string to encode. + * @return The encoded string. + */ + public static string encodeURI(string str) { + // C# is overzealous in the replacements. Walk back on a few. + return new StringBuilder(HttpUtility.UrlEncode(str)) + .Replace('+', ' ').Replace("%20", " ").Replace("%21", "!") + .Replace("%2a", "*").Replace("%27", "'").Replace("%28", "(") + .Replace("%29", ")").Replace("%3b", ";").Replace("%2f", "/") + .Replace("%3f", "?").Replace("%3a", ":").Replace("%40", "@") + .Replace("%26", "&").Replace("%3d", "=").Replace("%2b", "+") + .Replace("%24", "$").Replace("%2c", ",").Replace("%23", "#") + .Replace("%7e", "~") + .ToString(); + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DiffMatchPatch.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DiffMatchPatch.cs.meta new file mode 100644 index 0000000..577557c --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DiffMatchPatch.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc8e5e6042bc88643ab9223d6eeaae6e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DotNetListenerFactory.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DotNetListenerFactory.cs new file mode 100644 index 0000000..1258861 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DotNetListenerFactory.cs @@ -0,0 +1,20 @@ +using System; +using PubnubApi; + +namespace PubnubChatApi +{ + public class DotNetListenerFactory : ChatListenerFactory + { + public override SubscribeCallback ProduceListener( + Action>? messageCallback = null, + Action? presenceCallback = null, + Action>? signalCallback = null, + Action? objectEventCallback = null, + Action? messageActionCallback = null, + Action? fileCallback = null, Action? statusCallback = null) + { + return new SubscribeCallbackExt(messageCallback, presenceCallback, signalCallback, objectEventCallback, + messageActionCallback, fileCallback, statusCallback); + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DotNetListenerFactory.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DotNetListenerFactory.cs.meta new file mode 100644 index 0000000..700651b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DotNetListenerFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e9717b95285b29c489c0013ddb2454ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ExponentialRateLimiter.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ExponentialRateLimiter.cs new file mode 100644 index 0000000..d0421ab --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ExponentialRateLimiter.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace PubnubChatApi +{ + public class ExponentialRateLimiter : IDisposable + { + private const int ThreadsMaxSleepMs = 1000; + private const int NoSleepRequired = -1; + + private readonly float _exponentialFactor; + private readonly ConcurrentDictionary _limiters; + private readonly CancellationTokenSource _cancellationTokenSource; + private Task _processorTask; + private readonly object _processorLock = new object(); + private bool _disposed = false; + + public ExponentialRateLimiter(float exponentialFactor) + { + _exponentialFactor = exponentialFactor; + _limiters = new ConcurrentDictionary(); + _cancellationTokenSource = new CancellationTokenSource(); + } + + public async void RunWithinLimits(string id, int baseIntervalMs, Func> task, Action callback, Action errorCallback) + { + if (baseIntervalMs == 0) + { + // Execute immediately for zero interval + try + { + var result = await task().ConfigureAwait(false); + callback(result); + } + catch (Exception e) + { + errorCallback(e); + } + return; + } + + var limiter = _limiters.GetOrAdd(id, _ => new RateLimiterRoot + { + Queue = new Queue(), + CurrentPenalty = 0, + BaseIntervalMs = baseIntervalMs, + NextIntervalMs = 0, + ElapsedMs = 0, + Finished = false, + LastTaskStartTime = DateTimeOffset.UtcNow + }); + + lock (limiter.Queue) + { + limiter.Queue.Enqueue(new TaskElement + { + Task = task, + Callback = callback, + ErrorCallback = errorCallback, + Penalty = limiter.CurrentPenalty + }); + } + + EnsureProcessorRunning(); + } + + private void EnsureProcessorRunning() + { + lock (_processorLock) + { + if (_processorTask == null || _processorTask.IsCompleted) + { + _processorTask = Task.Run(ProcessorLoop, _cancellationTokenSource.Token); + } + } + } + + private async Task ProcessorLoop() + { + var slept = 0; + var toSleep = 0; + + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + if (toSleep == NoSleepRequired) + { + break; + } + + if (slept >= toSleep) + { + toSleep = await ProcessQueue(slept).ConfigureAwait(false); + } + else + { + toSleep -= slept; + } + + slept = Math.Min(toSleep, ThreadsMaxSleepMs); + + if (slept > 0) + { + await Task.Delay(slept, _cancellationTokenSource.Token).ConfigureAwait(false); + } + } + } + + private async Task ProcessQueue(int sleptMs) + { + var toSleep = ThreadsMaxSleepMs; + var itemsToRemove = new List(); + var processingTasks = new List(); + + foreach (var kvp in _limiters) + { + var id = kvp.Key; + var limiter = kvp.Value; + + lock (limiter.Queue) + { + limiter.ElapsedMs += sleptMs; + + if (limiter.NextIntervalMs > limiter.ElapsedMs) + { + toSleep = Math.Min(toSleep, limiter.NextIntervalMs - limiter.ElapsedMs); + continue; + } + + // Start processing the task asynchronously + var processingTask = ProcessLimiterAsync(limiter); + processingTasks.Add(processingTask); + + limiter.CurrentPenalty++; + limiter.NextIntervalMs = (int)(limiter.BaseIntervalMs * Math.Pow(_exponentialFactor, limiter.CurrentPenalty)); + limiter.ElapsedMs = 0; + limiter.LastTaskStartTime = DateTimeOffset.UtcNow; + + toSleep = Math.Min(toSleep, limiter.NextIntervalMs); + + if (limiter.Finished) + { + itemsToRemove.Add(id); + } + } + } + + // Wait for all processing tasks to complete before continuing + // This ensures we don't overwhelm the system with concurrent tasks + if (processingTasks.Count > 0) + { + await Task.WhenAll(processingTasks).ConfigureAwait(false); + } + + // Remove finished limiters + foreach (var id in itemsToRemove) + { + _limiters.TryRemove(id, out _); + } + + if (_limiters.IsEmpty) + { + return NoSleepRequired; + } + + return toSleep; + } + + private async Task ProcessLimiterAsync(RateLimiterRoot limiterRoot) + { + TaskElement element; + + // Queue is already locked by caller, but we need to dequeue safely + lock (limiterRoot.Queue) + { + if (limiterRoot.Queue.Count == 0) + { + limiterRoot.Finished = true; + return; + } + + element = limiterRoot.Queue.Dequeue(); + } + + try + { + var result = await element.Task().ConfigureAwait(false); + element.Callback(result); + } + catch (Exception e) + { + element.ErrorCallback(e); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed && disposing) + { + _cancellationTokenSource?.Cancel(); + + try + { + _processorTask?.Wait(5000); // Wait up to 5 seconds for graceful shutdown + } + catch (AggregateException) + { + // Ignore cancellation exceptions during shutdown + } + + _cancellationTokenSource?.Dispose(); + _disposed = true; + } + } + + private class RateLimiterRoot + { + public Queue Queue { get; set; } + public int CurrentPenalty { get; set; } + public int BaseIntervalMs { get; set; } + public int NextIntervalMs { get; set; } + public int ElapsedMs { get; set; } + public bool Finished { get; set; } + public DateTimeOffset LastTaskStartTime { get; set; } + } + + private class TaskElement + { + public Func> Task { get; set; } + public Action Callback { get; set; } + public Action ErrorCallback { get; set; } + public int Penalty { get; set; } + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ExponentialRateLimiter.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ExponentialRateLimiter.cs.meta new file mode 100644 index 0000000..1bf4f93 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ExponentialRateLimiter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 93f89d224a7d2bc4391776b71084b43f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/PubnubChatDotNetPNSDKSource.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/PubnubChatDotNetPNSDKSource.cs new file mode 100644 index 0000000..e44ed08 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/PubnubChatDotNetPNSDKSource.cs @@ -0,0 +1,20 @@ +using System.Globalization; +using System.Reflection; +using PubnubApi; +using PubnubApi.PNSDK; + +namespace PubnubChatApi +{ + public class PubnubChatDotNetPNSDKSource : IPNSDKSource + { + public string GetPNSDK() + { + var assembly = typeof(Pubnub).GetTypeInfo().Assembly; + var assemblyName = new AssemblyName(assembly.FullName); + string assemblyVersion = assemblyName.Version.ToString(); + var targetFramework = assembly.GetCustomAttribute()?.FrameworkDisplayName?.Replace(".",string.Empty).Replace(" ", string.Empty); + + return string.Format(CultureInfo.InvariantCulture, "{0}/CSharpChat/{1}", targetFramework??"UNKNOWN", assemblyVersion); + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/PubnubChatDotNetPNSDKSource.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/PubnubChatDotNetPNSDKSource.cs.meta new file mode 100644 index 0000000..0c7a958 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/PubnubChatDotNetPNSDKSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 874cf6ae276fa514982d83c3aabcbe21 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubUnitySDKReference.asmref b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubUnitySDKReference.asmref new file mode 100644 index 0000000..2a46319 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubUnitySDKReference.asmref @@ -0,0 +1,3 @@ +{ + "reference": "PubNubAPI" +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubUnitySDKReference.asmref.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubUnitySDKReference.asmref.meta new file mode 100644 index 0000000..241421c --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubUnitySDKReference.asmref.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 004d8a4420474df438b9a3d83e2f9329 +AssemblyDefinitionReferenceImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChat.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChat.cs new file mode 100644 index 0000000..713a9a6 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChat.cs @@ -0,0 +1,65 @@ +using System.Threading.Tasks; +using PubnubApi; +using PubnubApi.Unity; + +namespace PubnubChatApi +{ + public static class UnityChat + { + /// + /// Initializes a new instance of the class. + /// + /// Creates a new chat instance setup for Unity environment. + /// + /// + /// Config with Chat specific parameters + /// Config with PubNub keys and values + /// Flag for enabling WebGL mode - sets httpTransportService to UnityWebGLHttpClientService + /// Flag to set Unity specific logger (UnityPubNubLogger) + /// A ChatOperationResult containing the created Chat instance. + /// + /// The constructor initializes the Chat object with a new Pubnub instance. + /// + public static async Task> CreateInstance(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, bool webGLBuildMode = false, bool unityLogging = false) + { + var pubnub = PubnubUnityUtils.NewUnityPubnub(pubnubConfig, webGLBuildMode, unityLogging, new UnityChatPNSDKSource()); + return await Chat.CreateInstance(chatConfig, pubnub, new UnityListenerFactory()); + } + + /// + /// Initializes a new instance of the class. + /// + /// Creates a new chat instance setup for Unity environment. + /// + /// + /// Config with Chat specific parameters + /// Pubnub configuration Scriptable Object asset + /// Client user ID for this instance + /// A ChatOperationResult containing the created Chat instance. + /// + /// The constructor initializes the Chat object with a new Pubnub instance. + /// + public static async Task> CreateInstance(PubnubChatConfig chatConfig, PNConfigAsset configurationAsset, string userId) + { + var pubnub = PubnubUnityUtils.NewUnityPubnub(configurationAsset, userId, new UnityChatPNSDKSource()); + return await Chat.CreateInstance(chatConfig, pubnub, new UnityListenerFactory()); + } + + /// + /// Initializes a new instance of the class. + /// + /// Creates a new chat instance setup for Unity environment. + /// + /// + /// Config with Chat specific parameters + /// An existing Pubnub object instance + /// A ChatOperationResult containing the created Chat instance. + /// + /// The constructor initializes the Chat object with an existing Pubnub instance. + /// + public static async Task> CreateInstance(PubnubChatConfig chatConfig, Pubnub pubnub) + { + return await Chat.CreateInstance(chatConfig, pubnub); + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChat.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChat.cs.meta new file mode 100644 index 0000000..683dcc4 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChat.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 06081c61cb684dadad27c719ca497cb6 +timeCreated: 1757936963 \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChatPNSDKSource.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChatPNSDKSource.cs new file mode 100644 index 0000000..d84c933 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChatPNSDKSource.cs @@ -0,0 +1,37 @@ +using PubnubApi.PNSDK; +using PubnubApi.Unity; + +namespace PubnubChatApi +{ + public class UnityChatPNSDKSource : IPNSDKSource + { + private const string build = "0.4.5"; + + private string GetPlatformString() + { +#if(UNITY_IOS) + return "IOS"; +#elif(UNITY_STANDALONE_WIN) + return "Win"; +#elif(UNITY_STANDALONE_OSX) + return "OSX"; +#elif(UNITY_ANDROID) + return "Android"; +#elif(UNITY_STANDALONE_LINUX) + return "Linux"; +#elif(UNITY_WEBPLAYER) + return "Web"; +#elif(UNITY_WEBGL) + return "WebGL"; +#else + return ""; +#endif + } + + public string GetPNSDK() + { + var unitySdkVersion = new UnityPNSDKSource().Build; + return $"PubNub-CSharp-Unity{GetPlatformString()}/{unitySdkVersion}/CA-Unity/{build}"; + } + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChatPNSDKSource.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChatPNSDKSource.cs.meta new file mode 100644 index 0000000..59f3a10 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChatPNSDKSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 88a9660499303e840a88b222b805ed3d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityListenerFactory.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityListenerFactory.cs new file mode 100644 index 0000000..1792f3a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityListenerFactory.cs @@ -0,0 +1,18 @@ +using System; +using PubnubApi; +using PubnubApi.Unity; +using PubnubChatApi; + +public class UnityListenerFactory : ChatListenerFactory +{ + public override SubscribeCallback ProduceListener(Action> messageCallback = null, + Action presenceCallback = null, + Action> signalCallback = null, + Action objectEventCallback = null, + Action messageActionCallback = null, + Action fileCallback = null, Action statusCallback = null) + { + return new SubscribeCallbackListener(messageCallback, presenceCallback, signalCallback, objectEventCallback, + messageActionCallback, fileCallback, statusCallback); + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityListenerFactory.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityListenerFactory.cs.meta new file mode 100644 index 0000000..cc2580c --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityListenerFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 62bf9bff49fdc654389255c69a492cb0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/libpubnub-chat.dylib b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/libpubnub-chat.dylib deleted file mode 100644 index 1cac054..0000000 Binary files a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/libpubnub-chat.dylib and /dev/null differ diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/libpubnub-chat.dylib.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/libpubnub-chat.dylib.meta deleted file mode 100644 index a24298d..0000000 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/libpubnub-chat.dylib.meta +++ /dev/null @@ -1,33 +0,0 @@ -fileFormatVersion: 2 -guid: d13fed5ac0e94463ebe84579c27ecddd -PluginImporter: - externalObjects: {} - serializedVersion: 2 - iconMap: {} - executionOrder: {} - defineConstraints: [] - isPreloaded: 0 - isOverridable: 0 - isExplicitlyReferenced: 0 - validateReferences: 1 - platformData: - - first: - Any: - second: - enabled: 0 - settings: {} - - first: - Editor: Editor - second: - enabled: 1 - settings: - DefaultValueInitialized: true - - first: - Standalone: OSXUniversal - second: - enabled: 1 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/pubnub-chat.dll b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/pubnub-chat.dll deleted file mode 100644 index 384311a..0000000 Binary files a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/pubnub-chat.dll and /dev/null differ diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/pubnub-chat.dll.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/pubnub-chat.dll.meta deleted file mode 100644 index 8e8daf5..0000000 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/pubnub-chat.dll.meta +++ /dev/null @@ -1,27 +0,0 @@ -fileFormatVersion: 2 -guid: 96dd69b2e50863f479192e4cd7dc1551 -PluginImporter: - externalObjects: {} - serializedVersion: 2 - iconMap: {} - executionOrder: {} - defineConstraints: [] - isPreloaded: 0 - isOverridable: 0 - isExplicitlyReferenced: 0 - validateReferences: 1 - platformData: - - first: - Any: - second: - enabled: 1 - settings: {} - - first: - Editor: Editor - second: - enabled: 0 - settings: - DefaultValueInitialized: true - userData: - assetBundleName: - assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatConfigAsset.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatConfigAsset.cs index 5ca1ef9..8621e3b 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatConfigAsset.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatConfigAsset.cs @@ -1,16 +1,11 @@ -using System; -using PubnubChatApi.Entities.Data; +using PubnubChatApi; using UnityEngine; -namespace PubnubChat +namespace PubnubChatApi { [CreateAssetMenu(fileName = "PubnubChatConfigAsset", menuName = "PubNub/PubNub Chat Config Asset")] public class PubnubChatConfigAsset : ScriptableObject { - [field: SerializeField] public string PublishKey { get; private set; } - [field: SerializeField] public string SubscribeKey { get; private set; } - [field: SerializeField] public string UserId { get; private set; } - [field: SerializeField] public string AuthKey { get; private set; } [field: SerializeField] public int TypingTimeout { get; private set; } = 5000; [field: SerializeField] public int TypingTimeoutDifference { get; private set; } = 1000; [field: SerializeField] public int RateLimitFactor { get; private set; } @@ -20,22 +15,7 @@ public class PubnubChatConfigAsset : ScriptableObject public static implicit operator PubnubChatConfig(PubnubChatConfigAsset asset) { - if (string.IsNullOrEmpty(asset.UserId)) - { - throw new NullReferenceException("You need to set the UserId before passing configuration"); - } - - if (string.IsNullOrEmpty(asset.PublishKey)) - { - throw new NullReferenceException("You need to set the PublishKey before passing configuration"); - } - - if (string.IsNullOrEmpty(asset.SubscribeKey)) - { - throw new NullReferenceException("You need to set the SubscribeKey before passing configuration"); - } - - return new PubnubChatConfig(asset.PublishKey, asset.SubscribeKey, asset.UserId, asset.AuthKey, + return new PubnubChatConfig( asset.TypingTimeout, asset.TypingTimeoutDifference, rateLimitFactor: asset.RateLimitFactor, rateLimitPerChannel: asset.RateLimitPerChannel, diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatSample.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatSample.cs index bb12d21..c23d068 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatSample.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatSample.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; -using PubnubChat; -using PubNubChatAPI.Entities; -using PubnubChatApi.Entities.Data; +using PubnubApi; +using PubnubChatApi; using UnityEngine; /// @@ -9,26 +8,55 @@ /// public class PubnubChatSample : MonoBehaviour { + //Note that you can also use the PNConfigAsset scriptable object here. + [SerializeField] private string publishKey; + [SerializeField] private string subscribeKey; + [SerializeField] private string userId; + [SerializeField] private PubnubChatConfigAsset configAsset; private async void Start() { //Initialize Chat instance with Pubnub keys + user ID - var chat = await Chat.CreateInstance(configAsset); + var createChat = await UnityChat.CreateInstance(configAsset, new PNConfiguration(new UserId(userId)) + { + PublishKey = publishKey, + SubscribeKey = subscribeKey, + LogLevel = PubnubLogLevel.Error + }, unityLogging: true); + + //Abort if initialization failed - because we set LogLevel to PubnubLogLevel.Error we will also see the exact reason in the Unity console. + if (createChat.Error) + { + Debug.LogError($@"Chat initialization failed! Error: {createChat.Exception.Message}"); + return; + } + var chat = createChat.Result; - //Get config-defined user id handle - if (!chat.TryGetCurrentUser(out var user)) + //Get config-defined user id handle, abort on fail + var getCurrentUser = await chat.GetCurrentUser(); + if (getCurrentUser.Error) { - Debug.LogError("Wasn't able to get current user! Is the Chat Config set-up correctly?"); + Debug.LogError($"Wasn't able to get current user! Is the Chat Config set-up correctly? Error: {getCurrentUser.Exception.Message}"); return; - } + } + var user = getCurrentUser.Result; - //Create a new channel - var channel = await chat.CreatePublicConversation("MainChannel"); + //Create a new channel, abort on fail + var createChannel = await chat.CreatePublicConversation("MainChannel"); + if (createChannel.Error) + { + Debug.LogError($"Wasn't able to create channel! Error: {createChannel.Exception.Message}"); + return; + } + var channel = createChannel.Result; + //Define reaction on receiving new messages channel.OnMessageReceived += message => Debug.Log($"Received message: {message.MessageText}"); //Join channel + give time to establish connection - channel.Join(); + //Note that Join(), like all methods that make contact with the server, also returns a ChatOperationResult + //We could also potentially have abort logic here if the operation failed. + await channel.Join(); await Task.Delay(4000); //Send test message @@ -55,15 +83,26 @@ await user.Update(new ChatUserData() //Wait a moment to wait for them to be processed await Task.Delay(15000); - //Fetch message history (from all time) - foreach (var historyMessage in await channel.GetMessageHistory("99999999999999999", "00000000000000000", 50)) + //Fetch message history (from all time), again with abort logic + var getHistory = await channel.GetMessageHistory("99999999999999999", "00000000000000000", 50); + if (getHistory.Error) + { + Debug.LogError($"Wasn't able to get history! Error: {getHistory.Exception.Message}"); + return; + } + foreach (var historyMessage in getHistory.Result) { Debug.Log($"Message from history with timetoken {historyMessage.TimeToken}: {historyMessage.MessageText}"); } - //Get main users memberships - var userMembershipsWrapper = await user.GetMemberships(); - foreach (var userMembership in userMembershipsWrapper.Memberships) + //Get main users memberships, again with abort logic + var getMemberships = await user.GetMemberships(); + if (getMemberships.Error) + { + Debug.LogError($"Wasn't able to get memberships! Error: {getMemberships.Exception.Message}"); + return; + } + foreach (var userMembership in getMemberships.Result.Memberships) { Debug.Log($"Membership - User: {userMembership.UserId}, Channel: {userMembership.ChannelId}"); } @@ -79,8 +118,8 @@ await user.Update(new ChatUserData() //Wait a moment to wait for the restriction to be registered await Task.Delay(15000); - //Print channel's user restriction - var restriction = await channel.GetUserRestrictions(user); + //Print channel's user restriction, this time with no abort logic - shorter syntax but can result in exception if null result isn't handled. + var restriction = (await channel.GetUserRestrictions(user)).Result; Debug.Log($"{user.Id}'s ban status is: {restriction.Ban}, reason: {restriction.Reason}"); } } diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/package.json b/unity-chat/PubnubChatUnity/Assets/PubnubChat/package.json index abfc916..f825d46 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/package.json +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/package.json @@ -1,6 +1,6 @@ { "name": "com.pubnub.pubnubchat", - "version": "0.4.4", + "version": "1.0.0", "displayName": "Pubnub Chat", "description": "PubNub Unity Chat SDK", "unity": "2022.3", @@ -23,5 +23,8 @@ "description": "Contains a ScriptableObject with PubnubChatConfig properties that can be used to initialize a new Chat object and a sample Pubnub Chat MonoBehaviour script that runs a few example tasks in the console.", "path": "Samples~/PubnubChatConfigAsset" } - ] + ], + "dependencies": { + "com.pubnub.sdk": "9.2.0" + } } \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets.meta b/unity-chat/PubnubChatUnity/Assets/Snippets.meta new file mode 100644 index 0000000..ae3f3da --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 156fcc55be8e60348a2c237b5c601d74 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/AccessControlSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/AccessControlSample.cs new file mode 100644 index 0000000..46d8c0b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/AccessControlSample.cs @@ -0,0 +1,123 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class AccessControlSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task CheckPermissionsExample() + { + // snippet.check_permissions_example + var pnConfiguration = new PNConfiguration(new UserId("UserId")) + { + PublishKey = "PublishKey", + SubscribeKey = "SubscribeKey", + AuthKey = "AuthKey" + }; + + var chatConfig = new PubnubChatConfig(); + var chatResult = await Chat.CreateInstance(chatConfig, pnConfiguration); + if (chatResult.Error) + { + Debug.Log("Failed to create chat instance"); + return; + } + var chat = chatResult.Result; + + // get the ChatAccessManager instance + var chatAccessManager = chat.ChatAccessManager; + + // define the permissions, resource type, and resource name + PubnubAccessPermission permissionToCheck = PubnubAccessPermission.Write; + PubnubAccessResourceType resourceTypeToCheck = PubnubAccessResourceType.Channels; + string channelName = "support"; + + // check if the current user can send (write) messages to the 'support' channel + bool canSendMessage = await chatAccessManager.CanI(permissionToCheck, resourceTypeToCheck, channelName); + + // output the result + if (canSendMessage) + { + Debug.Log("The current user has permission to send messages to the 'support' channel."); + } + else + { + Debug.Log("The current user does not have permission to send messages to the 'support' channel."); + } + // snippet.end + } + + public static async Task SetAuthTokenExample() + { + // snippet.set_auth_token_example + var pnConfiguration = new PNConfiguration(new UserId("UserId")) + { + PublishKey = "PublishKey", + SubscribeKey = "SubscribeKey" + }; + var chatConfig = new PubnubChatConfig(); + var chatResult = await Chat.CreateInstance(chatConfig, pnConfiguration); + if (chatResult.Error) + { + Debug.Log("Failed to create chat instance"); + return; + } + var chat = chatResult.Result; + + // Set a new authentication token + chat.PubnubInstance.SetAuthToken("p0thisAkFl043rhDdHRsCkNyZXisRGNoYW6hanNlY3JldAFDZ3Jwsample3KgQ3NwY6BDcGF0pERjaGFuoENnctokenVzcqBDc3BjoERtZXRhoENzaWdYIGOAeTyWGJI"); + // snippet.end + } + + public static async Task ParseTokenExample() + { + // snippet.parse_token_example + var pnConfiguration = new PNConfiguration(new UserId("UserId")) + { + PublishKey = "PublishKey", + SubscribeKey = "SubscribeKey" + }; + var chatConfig = new PubnubChatConfig(); + var chatResult = await Chat.CreateInstance(chatConfig, pnConfiguration); + if (chatResult.Error) + { + Debug.Log("Failed to create chat instance"); + return; + } + var chat = chatResult.Result; + + // Parse an existing token + var tokenDetails = chat.PubnubInstance.ParseToken("p0thisAkFl043rhDdHRsCkNyZXisRGNoYW6hanNlY3JldAFDZ3Jwsample3KgQ3NwY6BDcGF0pERjaGFuoENnctokenVzcqBDc3BjoERtZXRhoENzaWdYIGOAeTyWGJI"); + + // Output the token details + Debug.Log("Token Details: " + chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(tokenDetails)); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/AccessControlSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/AccessControlSample.cs.meta new file mode 100644 index 0000000..14712c7 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/AccessControlSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e2e9641f332f40d42a97c3b00dd737ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ChannelReferencesSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/ChannelReferencesSample.cs new file mode 100644 index 0000000..a4843f8 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ChannelReferencesSample.cs @@ -0,0 +1,134 @@ +// snippet.using +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; +// snippet.end + +public class ChannelReferencesSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task AddChannelReferenceExample() + { + // snippet.add_channel_reference_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) return; + var testChannel = channelResult.Result; + + // Create a message draft + var messageDraft = testChannel.CreateMessageDraft(); + + // Update the message with the initial text + messageDraft.Update("Hello Alex! I have sent you this link on the #offtopic channel."); + + // Add a channel mention for the "#offtopic" channel + messageDraft.AddMention(45, 9, new MentionTarget + { + Target = "group.offtopic", // Assuming the channel ID is "group.offtopic" + Type = MentionType.Channel + }); + // snippet.end + } + + public static void RemoveChannelReferenceExample(MessageDraft messageDraft) + { + // snippet.remove_channel_reference_example + // assume the message reads + // Hello Alex! I have sent you this link on the #offtopic channel. + + // Remove the channel reference for "#offtopic" + messageDraft.RemoveMention(45); + // snippet.end + } + + public static async Task InsertSuggestedChannelReferenceExample() + { + // snippet.insert_suggested_channel_reference_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) return; + var channel = channelResult.Result; + + var messageDraft = channel.CreateMessageDraft(); + + messageDraft.ShouldSearchForSuggestions = true; + + messageDraft.OnDraftUpdated += (elements, mentions) => + { + if (!mentions.Any()) + { + return; + } + messageDraft.InsertSuggestedMention(mentions[0], mentions[0].ReplaceTo); + }; + + messageDraft.Update("Alex are you a member of the #offtop channel?"); + // snippet.end + } + + public static async Task CheckMessageChannelReferencesExample() + { + // snippet.check_message_channel_references_example + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // get the message with the specified timetoken + var messageResult = await channel.GetMessage("16200000000000000"); + if (!messageResult.Error) + { + var message = messageResult.Result; + Debug.Log($"Message: {message.MessageText}"); + + // check if the message contains any channel references + if (message.ReferencedChannels != null && message.ReferencedChannels.Count > 0) + { + Debug.Log("The message contains channel references."); + foreach (var referencedChannel in message.ReferencedChannels) + { + Debug.Log($"Referenced Channel: {referencedChannel.Name}"); + } + } + else + { + Debug.Log("The message does not contain any channel references."); + } + } + else + { + Debug.Log("Message with the specified timetoken not found."); + } + } + else + { + Debug.Log("Support channel not found."); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ChannelReferencesSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/ChannelReferencesSample.cs.meta new file mode 100644 index 0000000..f695a75 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ChannelReferencesSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 100018d01ad74fc488ac00b36566246b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ChannelSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/ChannelSample.cs new file mode 100644 index 0000000..54d2e62 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ChannelSample.cs @@ -0,0 +1,51 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class ChannelSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task EventSubscriptionExample() + { + // snippet.event_subscription_example + // Get or create a channel + var channelResult = await chat.GetChannel("my_channel"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + + channel.OnMessageReceived += (message) => + { + Debug.Log("New message received!"); + }; + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ChannelSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/ChannelSample.cs.meta new file mode 100644 index 0000000..7f41c6f --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ChannelSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a0fd5aa90aea76743b9ab5037a81b7eb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ChatSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/ChatSample.cs new file mode 100644 index 0000000..a4adb8b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ChatSample.cs @@ -0,0 +1,44 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class ChatSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static void AnyEventSubscriptionExample() + { + // snippet.any_event_subscription_example + chat.OnAnyEvent += (chatEvent) => + { + Debug.Log($"New event of type {chatEvent.Type} received!"); + }; + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ChatSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/ChatSample.cs.meta new file mode 100644 index 0000000..c918564 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ChatSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f46151e01a578a44aabe040035c021a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ConfigurationSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/ConfigurationSample.cs new file mode 100644 index 0000000..52c53eb --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ConfigurationSample.cs @@ -0,0 +1,60 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class ConfigurationSample +{ + public static async Task BasicInitializationExample() + { + // snippet.basic_initialization_example + var pnConfiguration = new PNConfiguration(new UserId("userId")) + { + PublishKey = "publishKey", + SubscribeKey = "subscribeKey" + }; + + var chatConfig = new PubnubChatConfig(storeUserActivityTimestamp: true); + + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + + if (!chatResult.Error) + { + var chatInstance = chatResult.Result; + Debug.Log("Chat instance created successfully!"); + } + else + { + Debug.LogError($"Failed to create chat instance"); + } + // snippet.end + } + + public static async Task WebGLInitializationExample() + { + // snippet.webgl_initialization_example + var pnConfiguration = new PNConfiguration(new UserId("userId")) + { + PublishKey = "publishKey", + SubscribeKey = "subscribeKey" + }; + + var chatConfig = new PubnubChatConfig(storeUserActivityTimestamp: true); + + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration, webGLBuildMode: true); + + if (!chatResult.Error) + { + var chatInstance = chatResult.Result; + Debug.Log("Chat instance created successfully for WebGL!"); + } + else + { + Debug.LogError($"Failed to create chat instance"); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ConfigurationSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/ConfigurationSample.cs.meta new file mode 100644 index 0000000..cd9c38c --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ConfigurationSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b8a617b48677df4a8f0c91d9cf78f86 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/CreateChannelSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/CreateChannelSample.cs new file mode 100644 index 0000000..e5e3686 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/CreateChannelSample.cs @@ -0,0 +1,110 @@ +// snippet.using +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class CreateChannelSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task CreateDirectConversationExample() + { + // snippet.create_direct_conversation_example + var user = await chat.GetUser("agent-007"); + if (user.Error) + { + Debug.Log("Couldn't find user!"); + return; + } + + string channelId = "direct.agent-001&agent-007"; + ChatChannelData channelData = new ChatChannelData + { + Name = "Customer XYZ Discussion", + Description = "Conversation about customer XYZ", + CustomData = new Dictionary(), + Status = "active", + Type = "direct" + }; + + // Call the method to create the direct conversation + var result = await chat.CreateDirectConversation(user.Result, channelId, channelData); + // snippet.end + } + + public static async Task CreateGroupConversationExample() + { + // snippet.create_group_conversation_example + // reference both agents you want to talk to + var user1Result = await chat.GetUser("agent-007"); + if (user1Result.Error) + { + Debug.Log("Couldn't find first user!"); + return; + } + var user2Result = await chat.GetUser("agent-008"); + if (user2Result.Error) + { + Debug.Log("Couldn't find second user!"); + return; + } + + List users = new List { user1Result.Result, user2Result.Result }; + + // Define the channel's ID and details via ChatChannelData + string channelId = "group-chat-1"; // Optional, can be auto-generated + ChatChannelData channelData = new ChatChannelData + { + Name = "Weekly syncs on customer XYZ", + Description = "Discussion about customer XYZ", + CustomData = new Dictionary() { { "purpose", "premium-support" } }, + Status = "active", + Type = "group" + }; + + // Call the method to create the group conversation + var result = await chat.CreateGroupConversation(users, channelId, channelData); + // snippet.end + } + + public static async Task CreatePublicConversationExample() + { + // snippet.create_public_conversation_example + ChatChannelData additionalData = new ChatChannelData + { + Name = "Support channel", + Description = "Discussion about support for all suers", + Status = "active", + Type = "public" + }; + + var publicConversation = await chat.CreatePublicConversation("ask-support", additionalData); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/CreateChannelSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/CreateChannelSample.cs.meta new file mode 100644 index 0000000..ce3bed3 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/CreateChannelSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8456f5245b059104e86d1b438625e040 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/CreateUserSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/CreateUserSample.cs new file mode 100644 index 0000000..f3c7252 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/CreateUserSample.cs @@ -0,0 +1,58 @@ +// snippet.using +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; + +// snippet.end + +public class CreateUserSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task CreateUserExample() + { + // snippet.create_user_example + // Define the custom data for the user + var userData = new ChatUserData + { + Username = "Support Agent", + ProfileUrl = "https://example.com/avatar.png", + Email = "agent@example.com", + CustomData = new Dictionary + { + { "title", "Customer Support Agent" }, + { "linkedin_profile", "https://www.linkedin.com/in/support-agent" } + }, + Status = "active", + Type = "support" + }; + + // Create the user with the specified ID and custom data + var result = await chat.CreateUser("support_agent_15", userData); + var user = result.Result; + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/CreateUserSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/CreateUserSample.cs.meta new file mode 100644 index 0000000..3b0c11e --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/CreateUserSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d14b84643e9d61f42ad92c9a2788f843 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/CustomEventsSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/CustomEventsSample.cs new file mode 100644 index 0000000..f47e53a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/CustomEventsSample.cs @@ -0,0 +1,127 @@ +// snippet.using +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubApi.Unity; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class CustomEventsSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task EmitCustomEventExample() + { + // snippet.emit_custom_event_example + await chat.EmitEvent( + type: PubnubChatEventType.Custom, + channelId: "CUSTOMER-SATISFACTION-CREW", + jsonPayload: + "{\"chatID\": \"chat1234\"," + + "\"timestamp\": \"2022-04-30T10:30:00Z\"," + + "\"customerID\": \"customer5678\"," + + "\"triggerWord\": \"frustrated\"}" + ); + // snippet.end + } + + public static async Task ListenForCustomEventsExample() + { + // snippet.listen_for_custom_events_example + var channelResult = await chat.GetChannel("CUSTOMER-SATISFACTION-CREW"); + if (channelResult.Error) return; + var channel = channelResult.Result; + + // simulated event data received + string eventData = + "\"chatID\":\"chat1234\"," + + "\"timestamp\":\"2022-04-30T10:30:00Z\"," + + "\"customerID\":\"customer5678\"," + + "\"triggerWord\":\"frustrated\""; + + // example function to handle the "frustrated" event and satisfy the customer + void HandleFrustratedEvent(string eventData) { + //basic JSON parsing using the pluggable library + var data = chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(eventData); + + // extract relevant information from the event data + string customerID = data["customerID"].ToString(); + string timestamp = data["timestamp"].ToString(); + string triggerWord = data["triggerWord"].ToString(); + + // create a response + string response = "Thank you for reaching out. We're sorry to hear " + + $"that you're {triggerWord}. Our team is here to help and will work to resolve your" + + "concerns as quickly as possible. Your satisfaction is important to us."; + + // send the response back to the customer's chat + SendResponseToCustomerChat(customerID, timestamp, response); + } + + // example event listener using "SetListeningForCustomEvents()" on some channel + channel.SetListeningForCustomEvents(true); + channel.OnCustomEvent += customEvent => + { + if(customEvent.Payload.Contains("\"triggerWord\":\frustrated\"")) + { + HandleFrustratedEvent(customEvent.Payload); + } + }; + // snippet.end + } + + // Helper method for the example above + private static void SendResponseToCustomerChat(string customerID, string timestamp, string response) + { + // Implementation would go here + Debug.Log($"Sending response to {customerID}: {response}"); + } + + public static async Task GetEventsHistoryExample() + { + // snippet.get_events_history_example + // define the required parameters + string channelId = "CUSTOMER-SATISFACTION-CREW"; + int count = 10; + + // fetch the last 10 historical events + var historyResult = await chat.GetEventsHistory(channelId, null, null, count); + if (historyResult.Error) + { + // Handle error + return; + } + var history = historyResult.Result; + + // process the returned historical events + foreach (var eventItem in history.Events) + { + Debug.Log($"Timestamp: {eventItem.TimeToken}, Event type: {eventItem.Type}"); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/CustomEventsSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/CustomEventsSample.cs.meta new file mode 100644 index 0000000..15ead8a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/CustomEventsSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: db9c9c5a9112e0746afdffc90ca3bffc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteChannelSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteChannelSample.cs new file mode 100644 index 0000000..8b65df7 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteChannelSample.cs @@ -0,0 +1,55 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class DeleteChannelSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task DeleteChannelUsingChannelObjectExample() + { + // snippet.delete_channel_using_channel_object_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Channel to delete doesn't exist."); + return; + } + var channel = channelResult.Result; + await channel.Delete(); + // snippet.end + } + + public static async Task DeleteChannelUsingChatObjectExample() + { + // snippet.delete_channel_using_chat_object_example + await chat.DeleteChannel("support"); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteChannelSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteChannelSample.cs.meta new file mode 100644 index 0000000..394ee65 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteChannelSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1c3af7909bc37aa41b574520396d4f72 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteMessageSample.cs new file mode 100644 index 0000000..621697b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteMessageSample.cs @@ -0,0 +1,88 @@ +// snippet.using +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class DeleteMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + + // snippet.end + } + + public static async Task PermanentDeleteMessageExample() + { + // snippet.permanent_delete_message_example + // reference the "channel" object + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + // invoke the method on the "channel" object + var messagesResult = await channel.GetMessageHistory("16200000000000000", "16200000000000001", 1); + if (messagesResult.Error || !messagesResult.Result.Any()) + { + Debug.Log("Couldn't find message!"); + return; + } + var message = messagesResult.Result[0]; + + // permanently remove the message + await message.Delete(false); + // snippet.end + } + + public static async Task SoftDeleteMessageExample() + { + // snippet.soft_delete_message_example + // reference the "channel" object + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + // invoke the method on the "channel" object + var messagesResult = await channel.GetMessageHistory("16200000000000000", "16200000000000001", 1); + if (messagesResult.Error || !messagesResult.Result.Any()) + { + Debug.Log("Couldn't find message!"); + return; + } + var message = messagesResult.Result[0]; + + // soft delete the message + await message.Delete(soft: true); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteMessageSample.cs.meta new file mode 100644 index 0000000..fc62b6a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3c2ab73c761904c42865117c6aa2e40e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteUserSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteUserSample.cs new file mode 100644 index 0000000..a599e3c --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteUserSample.cs @@ -0,0 +1,55 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class DeleteUserSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task DeleteUserUsingUserObjectExample() + { + // snippet.delete_user_using_user_object_example + var userResult = await chat.GetUser("support_agent_15"); + if (userResult.Error) + { + Debug.Log("Couldn't find user!"); + return; + } + var user = userResult.Result; + await user.DeleteUser(); + // snippet.end + } + + public static async Task DeleteUserUsingChatObjectExample() + { + // snippet.delete_user_using_chat_object_example + await chat.DeleteUser("support_agent_15"); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteUserSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteUserSample.cs.meta new file mode 100644 index 0000000..297bd50 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteUserSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7c2565ebba1d3f244aa17c08b5e5cb11 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsChannelSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsChannelSample.cs new file mode 100644 index 0000000..58e79b9 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsChannelSample.cs @@ -0,0 +1,46 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class DetailsChannelSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task GetChannelDetailsExample() + { + // snippet.get_channel_details_example + var result = await chat.GetChannel("support"); + if (!result.Error) + { + var channel = result.Result; + Debug.Log($"Found channel with name {channel.Name}"); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsChannelSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsChannelSample.cs.meta new file mode 100644 index 0000000..ed3f02c --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsChannelSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4b52888ac9bec35409a6d5172eb19619 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsMessageSample.cs new file mode 100644 index 0000000..82fce44 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsMessageSample.cs @@ -0,0 +1,101 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class DetailsMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task GetMessageDetailsExample() + { + // snippet.get_message_details_example + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + // get the message + var messageResult = await channel.GetMessage("16200000000000001"); + if (!messageResult.Error) + { + var message = messageResult.Result; + Debug.Log($"Message: {message.MessageText}"); + } + // snippet.end + } + + public static async Task GetMessageContentExample() + { + // snippet.get_message_content_example + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + // get the message + var messageResult = await channel.GetMessage("16200000000000001"); + if (!messageResult.Error) + { + var message = messageResult.Result; + Debug.Log($"Message: {message.MessageText}"); + } + // snippet.end + } + + public static async Task CheckDeletionStatusExample() + { + // snippet.check_deletion_status_example + // get the message + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + // get the message + var messageResult = await channel.GetMessage("16200000000000000"); + if (!messageResult.Error) + { + var message = messageResult.Result; + Debug.Log($"Is deleted?: {message.IsDeleted}"); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsMessageSample.cs.meta new file mode 100644 index 0000000..698cb42 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 737981135a6154f4bb3a057b9dde2edd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsUserSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsUserSample.cs new file mode 100644 index 0000000..eec812a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsUserSample.cs @@ -0,0 +1,64 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class DetailsUserSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task GetUserDetailsExample() + { + // snippet.get_user_details_example + var result = await chat.GetUser("support_agent_15"); + if (!result.Error) + { + var user = result.Result; + Debug.Log($"Found user with name {user.UserName}"); + } + // snippet.end + } + + public static async Task GetCurrentUserExample() + { + // snippet.get_current_user_example + var result = await chat.GetCurrentUser(); + if (!result.Error) + { + var user = result.Result; + Debug.Log($"Current user is {user.UserName}"); + + // perform additional actions with the user if needed + } + else + { + Debug.Log("Current user not found."); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsUserSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsUserSample.cs.meta new file mode 100644 index 0000000..f3a4749 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DetailsUserSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 459837697cf451341a64cb7325af2a2f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DraftsMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/DraftsMessageSample.cs new file mode 100644 index 0000000..7c97af4 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DraftsMessageSample.cs @@ -0,0 +1,230 @@ +// snippet.using +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; + +// snippet.end + +public class DraftsMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task CreateMessageDraftExample() + { + // snippet.create_message_draft_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) return; + var channel = channelResult.Result; + + var messageDraft = channel.CreateMessageDraft(); + // snippet.end + } + + public static async Task AddMessageDraftChangeListenerExample() + { + // snippet.add_message_draft_change_listener_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) return; + var channel = channelResult.Result; + + // Create a message draft + var messageDraft = channel.CreateMessageDraft(); + + // Enable receiving search suggestions + messageDraft.ShouldSearchForSuggestions = true; + + + // Use a dedicated callback + void InsertDelegateCallback(List elements, List mentions) + { + // your logic goes here + } + + // Add the InsertDelegateCallback function to the OnDraftUpdated event + messageDraft.OnDraftUpdated += InsertDelegateCallback; + + // Or use a lambda + // Event handlers added with a lambda + messageDraft.OnDraftUpdated += (elements, mentions) => + { + // your logic goes here + }; + // snippet.end + } + + public static async Task RemoveMessageDraftChangeListenerExample() + { + // snippet.remove_message_draft_change_listener_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) return; + var channel = channelResult.Result; + + // Create a message draft + var messageDraft = channel.CreateMessageDraft(); + + // Enable receiving search suggestions + messageDraft.ShouldSearchForSuggestions = true; + + + // Use a dedicated callback + void InsertDelegateCallback(List elements, List mentions) + { + // your logic goes here + } + + // Add the InsertDelegateCallback function to the OnDraftUpdated event + messageDraft.OnDraftUpdated += InsertDelegateCallback; + + // Remove the InsertDelegateCallback function to the OnDraftUpdated event + messageDraft.OnDraftUpdated -= InsertDelegateCallback; + // snippet.end + } + + public static async Task AddMessageElementExample() + { + // snippet.add_message_element_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) return; + var channel = channelResult.Result; + + var messageDraft = channel.CreateMessageDraft(); + + // Add initial text + messageDraft.Update("Hello Alex!"); + + // Add a user mention to the string "Alex" + messageDraft.AddMention(6, 4, new MentionTarget { + Target = "alex_d", + Type = MentionType.User + }); + + // Change the text + messageDraft.Update("Hello Alex! I have sent you this link on the #offtopic channel."); + + // Add a URL mention to the string "link" + messageDraft.AddMention(33, 4, new MentionTarget { + Target = "www.pubnub.com", + Type = MentionType.Url + }); + + // Add a channel mention to the string "#offtopic" + messageDraft.AddMention(45, 9, new MentionTarget { + Target = "group.offtopic", + Type = MentionType.Channel + }); + // snippet.end + } + + public static void RemoveMessageElementExample(MessageDraft messageDraft) + { + // snippet.remove_message_element_example + // Assume the message reads: + // Hello Alex! I have sent you this link on the #offtopic channel. + + // Remove the link mention + messageDraft.RemoveMention(33); + // snippet.end + } + + public static void UpdateMessageTextExample(MessageDraft messageDraft) + { + // snippet.update_message_text_example + // the message reads: + // I sent [Alex] this picture. + // where [Alex] is a user mention + messageDraft.Update("I did not send Alex this picture."); + // the message now reads: + // I did not send [Alex] this picture. + // the mention is preserved because its text wasn't changed + // snippet.end + } + + public static async Task InsertSuggestedMessageElementExample() + { + // snippet.insert_suggested_message_element_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) return; + var channel = channelResult.Result; + + var messageDraft = channel.CreateMessageDraft(); + messageDraft.ShouldSearchForSuggestions = true; + messageDraft.OnDraftUpdated += (elements, mentions) => + { + if (!mentions.Any()) + { + return; + } + messageDraft.InsertSuggestedMention(mentions[0], mentions[0].ReplaceTo); + }; + messageDraft.InsertText(0, "maybe i'll mention @John"); + // snippet.end + } + + public static void InsertMessageTextExample(MessageDraft messageDraft) + { + // snippet.insert_message_text_example + // The message reads: + // Check this support article https://www.support-article.com/. + + // Add "out" after "Check" (position 6, right after "Check ") + messageDraft.InsertText(6, "out "); + + // The message now reads: + // Check out this support article https://www.support-article.com/. + // snippet.end + } + + public static void RemoveMessageTextExample(MessageDraft messageDraft) + { + // snippet.remove_message_text_example + // The message reads: + // Check out this support article https://www.support-article.com/. + + messageDraft.RemoveText(5, 4); + // The message now reads: + // Check this support article https://www.support-article.com/. + // snippet.end + } + + public static async Task SendDraftMessageExample() + { + // snippet.send_draft_message_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) return; + var channel = channelResult.Result; + + // Create a message draft + var messageDraft = channel.CreateMessageDraft(); + + // Add initial text + messageDraft.Update("Hello Alex!"); + + // Send the message + await messageDraft.Send(); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DraftsMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/DraftsMessageSample.cs.meta new file mode 100644 index 0000000..fd97be2 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DraftsMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a36fb0f66aabd6c43acea3b9ec1dac7a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ForwardMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/ForwardMessageSample.cs new file mode 100644 index 0000000..b548de6 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ForwardMessageSample.cs @@ -0,0 +1,93 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class ForwardMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task ForwardMessageExample() + { + // snippet.forward_message_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + // reference a message on the "support" channel + var messageResult = await channel.GetMessage("16686902600029072"); + if (messageResult.Error) + { + Debug.Log("Couldn't find message!"); + return; + } + var message = messageResult.Result; + + // use the "forward()" method to send the message to the "incident-management" channel + await message.Forward("incident-management"); + // snippet.end + } + + public static async Task ForwardMessageUsingChannelExample() + { + // snippet.forward_message_using_channel_example + var originalChannelResult = await chat.GetChannel("support"); + if (originalChannelResult.Error) + { + Debug.Log("Couldn't find original channel!"); + return; + } + var originalChannel = originalChannelResult.Result; + + // reference a message on the "support" channel + var messageResult = await originalChannel.GetMessage("16686902600029072"); + if (messageResult.Error) + { + Debug.Log("Couldn't find message!"); + return; + } + var message = messageResult.Result; + + // reference the "incident-management" channel to which you want to forward the message + var channelResult = await chat.GetChannel("incident-management"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + // use the "ForwardMessage()" method to send the message to the "incident-management" channel + await channel.ForwardMessage(message); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ForwardMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/ForwardMessageSample.cs.meta new file mode 100644 index 0000000..18f67b9 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ForwardMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3336be32a21d8c340b1d2151044bcd55 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/HistoryMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/HistoryMessageSample.cs new file mode 100644 index 0000000..898cb6e --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/HistoryMessageSample.cs @@ -0,0 +1,52 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class HistoryMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task GetMessageHistoryExample() + { + // snippet.get_message_history_example + // reference the "channel" object + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + // invoke the method on the "channel" object + var messagesResult = await channel.GetMessageHistory("15343325214676133", null, 10); + var messages = messagesResult.Result; + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/HistoryMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/HistoryMessageSample.cs.meta new file mode 100644 index 0000000..6c64f9b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/HistoryMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 58a32f17a435070488e6f68b8b39985f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/InviteChannelSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/InviteChannelSample.cs new file mode 100644 index 0000000..7f825cb --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/InviteChannelSample.cs @@ -0,0 +1,119 @@ +// snippet.using +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class InviteChannelSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task InviteOneUserExample() + { + // snippet.invite_one_user_example + // reference "support-agent-15" + var userResult = await chat.GetUser("support-agent-15"); + if (userResult.Error) + { + Debug.Log("Couldn't find user to invite!"); + return; + } + var user = userResult.Result; + + // get the channel + var channelResult = await chat.GetChannel("high-prio-incidents"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel to invite!"); + return; + } + var channel = channelResult.Result; + + // invite the agent to join the channel + await channel.Invite(user); + // snippet.end + } + + public static async Task InviteMultipleUsersExample() + { + // snippet.invite_multiple_users_example + // reference "support-agent-15" + var user1Result = await chat.GetUser("support-agent-15"); + if (user1Result.Error) + { + Debug.Log("Couldn't find first user!"); + return; + } + var user1 = user1Result.Result; + + // reference "support-agent-16" + var user2Result = await chat.GetUser("support-agent-16"); + if (user2Result.Error) + { + Debug.Log("Couldn't find second user!"); + return; + } + var user2 = user2Result.Result; + + // reference the "high-prio-incidents" channel + var channelResult = await chat.GetChannel("high-prio-incidents"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + // invite both agents to join the channel + var newMemberships = await channel.InviteMultiple(new List { user1, user2 }); + // snippet.end + } + + public static async Task ListenToInviteEventsExample() + { + // snippet.listen_to_invite_events_example + var userResult = await chat.GetUser("support-agent-2"); + if(userResult.Error){ + Debug.Log("Couldn't find user!"); + return; + } + var user = userResult.Result; + + //start listening + user.SetListeningForInviteEvents(true); + //lambda event handler + user.OnInviteEvent += (inviteEvent) => + { + if(inviteEvent.ChannelId == "support" && inviteEvent.UserId == "support-agent-2") + { + Debug.Log("User support-agent-2 has been invited to the support channel!"); + } + }; + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/InviteChannelSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/InviteChannelSample.cs.meta new file mode 100644 index 0000000..487a67b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/InviteChannelSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e31e9e084c3f4754196919ec95a51fa7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/JoinChannelSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/JoinChannelSample.cs new file mode 100644 index 0000000..fbddb02 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/JoinChannelSample.cs @@ -0,0 +1,58 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class JoinChannelSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task JoinChannelExample() + { + // snippet.join_channel_example + // reference the "channel" object + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + channel.OnMessageReceived += OnMessageReceivedHandler; // or use lambda + + void OnMessageReceivedHandler(Message message) + { + Debug.Log($"Message received: {message.MessageText}"); + } + + // join the channel and add metadata to the newly created membership + await channel.Join(); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/JoinChannelSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/JoinChannelSample.cs.meta new file mode 100644 index 0000000..88670d0 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/JoinChannelSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 66c1e39e0d9df8a42a04153f9ddff78f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/LeaveChannelSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/LeaveChannelSample.cs new file mode 100644 index 0000000..0009e29 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/LeaveChannelSample.cs @@ -0,0 +1,58 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class LeaveChannelSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task LeaveChannelExample() + { + // snippet.leave_channel_example + // reference the "channel" object + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + channel.OnMessageReceived += OnMessageReceivedHandler; // or use lambda + void OnMessageReceivedHandler(Message message) + { + Debug.Log($"Message received: {message.MessageText}"); + } + // join the channel and add metadata to the newly created membership + await channel.Join(); + // and leave + await channel.Leave(); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/LeaveChannelSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/LeaveChannelSample.cs.meta new file mode 100644 index 0000000..3404d7a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/LeaveChannelSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a0a00f498b8077c42985eeaabb8ec0d0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/LinksMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/LinksMessageSample.cs new file mode 100644 index 0000000..5549f7f --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/LinksMessageSample.cs @@ -0,0 +1,131 @@ +// snippet.using +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class LinksMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task AddLinkMentionExample() + { + // snippet.add_link_mention_example + var channelResult = await chat.GetChannel("offtopic"); + if (channelResult.Error) return; + var testChannel = channelResult.Result; + + // Create a message draft + var messageDraft = testChannel.CreateMessageDraft(); + + // Update the message with the initial text + messageDraft.Update("Hello Alex! I have sent you this link on the #offtopic channel."); + + // Add a URL to the word "link" + messageDraft.AddMention(33, 4, new MentionTarget + { + Target = "https://example.com", + Type = MentionType.Url + }); + // snippet.end + } + + public static void RemoveLinkMentionExample(MessageDraft messageDraft) + { + // snippet.remove_link_mention_example + // assume the message reads + // Hello Alex! I have sent you this link on the #offtopic channel.` + + // remove the link mention + messageDraft.RemoveMention(33); + // snippet.end + } + + public static async Task LinkSuggestionsExample() + { + // snippet.link_suggestions_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) return; + var channel = channelResult.Result; + + var messageDraft = channel.CreateMessageDraft(); + + messageDraft.ShouldSearchForSuggestions = true; + + messageDraft.OnDraftUpdated += (elements, mentions) => + { + if (!mentions.Any()) + { + return; + } + messageDraft.InsertSuggestedMention(mentions[0], mentions[0].ReplaceTo); + }; + + messageDraft.Update("Alex, update the link to https://www.pubnub.com "); + // snippet.end + } + + public static async Task GetTextLinksExample() + { + // snippet.get_text_links_example + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + // get the message with the specific timetoken + var messageResult = await channel.GetMessage("16200000000000000"); + if (!messageResult.Error) + { + var message = messageResult.Result; + // check if the message contains any text links + if (message.TextLinks != null && message.TextLinks.Count > 0) + { + Debug.Log("The message contains the following text links:"); + foreach (var textLink in message.TextLinks) + { + Debug.Log($"Text Link: {textLink.Link}"); + } + } + else + { + Debug.Log("The message does not contain any text links."); + } + } + else + { + Debug.Log("Message with specified timetoken not found."); + } + } + else + { + Debug.Log("Channel 'support' not found."); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/LinksMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/LinksMessageSample.cs.meta new file mode 100644 index 0000000..44749fc --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/LinksMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8fdfbae3069ad1447aeb7804f8c5a9d8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ListChannelSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/ListChannelSample.cs new file mode 100644 index 0000000..dc7f8eb --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ListChannelSample.cs @@ -0,0 +1,71 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class ListChannelSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task GetAllChannelsExample() + { + // snippet.get_all_channels_example + // fetch all channels + var channelsWrapper = await chat.GetChannels(); + + // print all channel IDs + foreach (var channel in channelsWrapper.Channels) + { + Debug.Log(channel.Id); + } + // snippet.end + } + + public static async Task PaginationExample() + { + // snippet.pagination_example + // fetch the initial 25 channels + var channelsWrapper = await chat.GetChannels(limit: 25); + + Debug.Log("Initial 25 channels:"); + foreach (var channel in channelsWrapper.Channels) + { + Debug.Log($"Id: {channel.Id}"); + } + + // fetch the next set of channels using the page object from returned wrapper + var nextChannelsWrapper = await chat.GetChannels(limit: 25, page: channelsWrapper.Page); + + Debug.Log("\nNext set of channels:"); + foreach (var channel in nextChannelsWrapper.Channels) + { + Debug.Log($"Id: {channel.Id}"); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ListChannelSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/ListChannelSample.cs.meta new file mode 100644 index 0000000..3668345 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ListChannelSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 556d2ef7f8c999346990138c599e9b83 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ListUserSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/ListUserSample.cs new file mode 100644 index 0000000..1e76c92 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ListUserSample.cs @@ -0,0 +1,108 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class ListUserSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task GetUsersExample() + { + // snippet.get_users_example + // fetch all existing users + var usersWrapper = await chat.GetUsers(); + + // check if users were successfully fetched + if (!usersWrapper.Error) + { + Debug.Log("Existing user IDs:"); + + // loop through the users and print their IDs + foreach (var user in usersWrapper.Result.Users) + { + Debug.Log(user.Id); + } + } + else + { + Debug.Log("No users found or unable to fetch users."); + } + // snippet.end + } + + public static async Task PaginationExample() + { + // snippet.pagination_example + // fetch the initial 25 users + var initialUsers = await chat.GetUsers(limit: 25); + if (initialUsers.Error) + { + Debug.Log("Couldn't fetch initial users!"); + return; + } + + Debug.Log("Initial 25 users:"); + foreach (var user in initialUsers.Result.Users) + { + Debug.Log($"Id: {user.Id}, UserName: {user.UserName}, Status: {user.Status}"); + } + + // fetch the next set of users using the pagination token + var nextUsers = await chat.GetUsers(limit: 25, page: initialUsers.Result.Page); + if (nextUsers.Error) + { + Debug.Log("Couldn't fetch next users!"); + return; + } + + Debug.Log("\nNext users:"); + foreach (var user in nextUsers.Result.Users) + { + Debug.Log($"Id: {user.Id}, UserName: {user.UserName}, Status: {user.Status}"); + } + // snippet.end + } + + public static async Task DeletedUsersExample() + { + // snippet.deleted_users_example + var deletedUsers = await chat.GetUsers(filter: "Status='deleted'"); + if (deletedUsers.Error) + { + Debug.Log("Couldn't fetch deleted users!"); + return; + } + + foreach (var user in deletedUsers.Result.Users) + { + Debug.Log($"Id: {user.Id}, UserName: {user.UserName}, Status: {user.Status}"); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ListUserSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/ListUserSample.cs.meta new file mode 100644 index 0000000..bd46be7 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ListUserSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5952abdcc6cb1c04cb692634a0d7e0d9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/MembershipChannelSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/MembershipChannelSample.cs new file mode 100644 index 0000000..84ae544 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/MembershipChannelSample.cs @@ -0,0 +1,139 @@ +// snippet.using +using System; +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; +// snippet.end + +public class MembershipChannelSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task GetMembershipsExample() + { + // snippet.get_memberships_example + var userResult = await chat.GetUser("support_agent_15"); + if (userResult.Error) + { + Debug.Log("User not found."); + return; + } + var user = userResult.Result; + Debug.Log($"Found user with name {user.UserName}"); + + // Get the memberships of the user + var membershipsResult = await user.GetMemberships(); + + if (!membershipsResult.Error) + { + Debug.Log($"Memberships of user {user.UserName}:"); + + foreach (var membership in membershipsResult.Result.Memberships) + { + Debug.Log($"Channel ID: {membership.ChannelId}"); + } + } + // snippet.end + } + + public static async Task GetMembershipUpdatesExample() + { + // snippet.get_membership_updates_example + // reference the "support_agent_15" user + var userResult = await chat.GetUser("support_agent_15"); + if (!userResult.Error) + { + var user = userResult.Result; + Debug.Log($"Found user with name {user.UserName}"); + + // get the list of all user memberships + var membershipsResponse = await user.GetMemberships(); + + // extract the actual memberships from the response + if (!membershipsResponse.Error) + { + var memberships = membershipsResponse.Result.Memberships; + if (memberships.Any()) + { + // get the first membership + var firstMembership = memberships.First(); + + // output the first membership details + Debug.Log($"First membership for user {user.UserName} is in channel {firstMembership.ChannelId}"); + + // start listening for updates on memberships + firstMembership.SetListeningForUpdates(true); + + // attach an event handler for membership updates + firstMembership.OnMembershipUpdated += OnMembershipUpdatedHandler; + + // example event handler for membership updates + void OnMembershipUpdatedHandler(Membership updatedMembership) + { + Debug.Log($"Membership updated: {updatedMembership.ChannelId}"); + } + } + else + { + Debug.Log("The user 'support_agent_15' has no memberships."); + } + } + } + else + { + Debug.Log("User 'support_agent_15' not found."); + } + // snippet.end + } + + public static async Task UpdateMembershipExample() + { + // snippet.update_membership_example + // reference the "support_agent_15" user + var userResult = await chat.GetUser("support_agent_15"); + if (userResult.Error) + { + Debug.Log("Couldn't find user!"); + return; + } + var user = userResult.Result; + + // get the list of all user memberships and filter out the right channel + var membershipsWrapperResult = await user.GetMemberships( + filter: "channel.id == 'high-priority-incidents'" + ); + + if(!membershipsWrapperResult.Error && membershipsWrapperResult.Result.Memberships.Any()) + { + var membership = membershipsWrapperResult.Result.Memberships[0]; + membership.MembershipData.CustomData["role"] = "premium-support"; + // add custom metadata to the user membership + await membership.Update(membership.MembershipData); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/MembershipChannelSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/MembershipChannelSample.cs.meta new file mode 100644 index 0000000..662fcf9 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/MembershipChannelSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 02f00d86294dc8b48b3c717469a1a53b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/MembershipSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/MembershipSample.cs new file mode 100644 index 0000000..656a2a3 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/MembershipSample.cs @@ -0,0 +1,51 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class MembershipSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task MembershipUpdatedEventExample() + { + // snippet.membership_updated_event_example + // Get user memberships + var membershipsResult = await chat.GetUserMemberships("myUniqueUserId"); + if (!membershipsResult.Error && membershipsResult.Result.Memberships.Count > 0) + { + var membership = membershipsResult.Result.Memberships[0]; + + membership.OnMembershipUpdated += (membership) => + { + Debug.Log("Membership metadata updated!"); + }; + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/MembershipSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/MembershipSample.cs.meta new file mode 100644 index 0000000..3c58960 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/MembershipSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e182c27f69c17964bb29c3e3704103a9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/MentionsUserSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/MentionsUserSample.cs new file mode 100644 index 0000000..376bb19 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/MentionsUserSample.cs @@ -0,0 +1,174 @@ +// snippet.using +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; +// snippet.end + +public class MentionsUserSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task AddUserMentionExample() + { + // snippet.add_user_mention_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) return; + var testChannel = channelResult.Result; + + // Create a message draft + var messageDraft = testChannel.CreateMessageDraft(); + + // Update the message with the initial text + messageDraft.Update("Hello Alex! I have sent you this link on the #offtopic channel."); + + // Add a user mention to the string "Alex" + messageDraft.AddMention(6, 4, new MentionTarget + { + Target = "alex_d", + Type = MentionType.User + }); + // snippet.end + } + + public static void RemoveUserMentionExample(MessageDraft messageDraft) + { + // snippet.remove_user_mention_example + // assume the message reads + // Hello Alex! I have sent you this link on the #offtopic channel.` + + // remove the user reference + messageDraft.RemoveMention(6); + // snippet.end + } + + public static async Task InsertSuggestedMentionExample() + { + // snippet.insert_suggested_mention_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) return; + var channel = channelResult.Result; + + var messageDraft = channel.CreateMessageDraft(); + + messageDraft.ShouldSearchForSuggestions = true; + + messageDraft.OnDraftUpdated += (elements, mentions) => + { + if (!mentions.Any()) + { + return; + } + messageDraft.InsertSuggestedMention(mentions[0], mentions[0].ReplaceTo); + }; + + messageDraft.Update("@Alex are you there?"); + // snippet.end + } + + public static async Task CheckMessageMentionsExample() + { + // snippet.check_message_mentions_example + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // get the message with the specified timetoken + var messageResult = await channel.GetMessage("16200000000000000"); + if (!messageResult.Error) + { + var message = messageResult.Result; + Debug.Log($"Message: {message.MessageText}"); + + // check if the message contains any mentions + if (message.MentionedUsers != null && message.MentionedUsers.Count > 0) + { + Debug.Log("The message contains mentions."); + foreach (var mentionedUser in message.MentionedUsers) + { + Debug.Log($"Mentioned User: {mentionedUser.Name}"); + } + } + else + { + Debug.Log("The message does not contain any mentions."); + } + } + else + { + Debug.Log("Message with the specified timetoken not found."); + } + } + else + { + Debug.Log("Support channel not found."); + } + // snippet.end + } + + public static async Task GetCurrentUserMentionsExample() + { + // snippet.get_current_user_mentions_example + // fetch the last 10 mentions for the current user + var mentions = await chat.GetCurrentUserMentions(string.Empty, string.Empty, 10); + + if (!mentions.Error && mentions.Result.Mentions.Any()) + { + foreach (var mention in mentions.Result.Mentions) + { + Debug.Log($"Mentioned in Channel ID: {mention.ChannelId}, Message: {mention.Message.MessageText}"); + } + } + else + { + Debug.Log("No mentions found."); + } + // snippet.end + } + + public static async Task NotificationForMentionExample() + { + // snippet.notification_for_mention_example + var userResult = await chat.GetCurrentUser(); + if (userResult.Error) + { + return; + } + var user = userResult.Result; + user.SetListeningForMentionEvents(true); + user.OnMentionEvent += mentionEvent => + { + if(mentionEvent.ChannelId == "support") + { + Debug.Log($"{user.Id} has been mentioned on the support channel!"); + } + }; + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/MentionsUserSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/MentionsUserSample.cs.meta new file mode 100644 index 0000000..cb257dc --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/MentionsUserSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 460915d8e7724a846bda6aff02a3f337 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/MessageDraftSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/MessageDraftSample.cs new file mode 100644 index 0000000..6e02537 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/MessageDraftSample.cs @@ -0,0 +1,58 @@ +// snippet.using +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; + +// snippet.end + +public class MessageDraftSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task DraftUpdatedEventExample() + { + // snippet.draft_updated_event_example + // Get a channel and create a message draft + var channelResult = await chat.GetChannel("my_channel"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + var messageDraft = channel.CreateMessageDraft(); + + messageDraft.ShouldSearchForSuggestions = true; + + messageDraft.OnDraftUpdated += (elements, mentions) => + { + if (!mentions.Any()) + { + return; + } + messageDraft.InsertSuggestedMention(mentions[0], mentions[0].ReplaceTo); + }; + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/MessageDraftSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/MessageDraftSample.cs.meta new file mode 100644 index 0000000..59e2b10 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/MessageDraftSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 86274ba967aa284449035cb5e7684409 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/MessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/MessageSample.cs new file mode 100644 index 0000000..da8cb15 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/MessageSample.cs @@ -0,0 +1,57 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class MessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task MessageUpdatedEventExample() + { + // snippet.message_updated_event_example + // Get a channel and a message + var channelResult = await chat.GetChannel("my_channel"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + var messageResult = await channel.GetMessage("message_timetoken"); + + if (!messageResult.Error) + { + var message = messageResult.Result; + + message.OnMessageUpdated += (message) => + { + Debug.Log("Message was edited!"); + }; + } + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/MessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/MessageSample.cs.meta new file mode 100644 index 0000000..e0918ff --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/MessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c9921cbe3a00c9642a7f321915c02b63 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ModerationMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/ModerationMessageSample.cs new file mode 100644 index 0000000..1d12030 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ModerationMessageSample.cs @@ -0,0 +1,90 @@ +// snippet.using +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class ModerationMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task ReportMessageExample() + { + // snippet.report_message_example + // get the "support" channel + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Support channel not found."); + return; + } + var channel = channelResult.Result; + + Debug.Log($"Found channel with name {channel.Name}"); + + // retrieve the message history with the desired count + var messageHistoryResult = await channel.GetMessageHistory(null, null, 1); + if (messageHistoryResult.Error) + { + Debug.Log("Could not retrieve message history."); + return; + } + + // get the last message from the returned list + var lastMessage = messageHistoryResult.Result.FirstOrDefault(); + + // report the last message if it exists + if (lastMessage != null) + { + await lastMessage.Report("This is insulting!"); + Debug.Log("Reported the last message in the support channel."); + } + else + { + Debug.Log("No messages found in the channel history."); + } + // snippet.end + } + + public static async Task ListenToReportEventsExample() + { + // snippet.listen_to_report_events_example + var channelResult = await chat.GetChannel("support"); + if(channelResult.Error){ + Debug.Log("Couldn't find the support channel!"); + return; + } + var channel = channelResult.Result; + channel.SetListeningForReportEvents(true); + channel.OnReportEvent += reportEvent => + { + Debug.Log("Message reported on the support channel!"); + }; + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ModerationMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/ModerationMessageSample.cs.meta new file mode 100644 index 0000000..d467d8e --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ModerationMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d21f79bb1f7b6024f9e79a6d5e1e8e95 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ModerationUserSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/ModerationUserSample.cs new file mode 100644 index 0000000..c32992b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ModerationUserSample.cs @@ -0,0 +1,257 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class ModerationUserSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task MuteUserChatObjectExample() + { + // snippet.mute_user_chat_object_example + await chat.SetRestriction( + userId: "support_agent_15", + channelId: "support", + restriction: new Restriction() + { + Ban = false, + Mute = true, + Reason = string.Empty + } + ); + // snippet.end + } + + public static async Task MuteUserUserObjectExample() + { + // snippet.mute_user_user_object_example + var userResult = await chat.GetUser("support_agent_15"); + if (!userResult.Error) + { + var user = userResult.Result; + await user.SetRestriction( + "support", + new Restriction() + { + Ban = false, + Mute = true, + Reason = string.Empty + } + ); + } + // snippet.end + } + + public static async Task MuteUserChannelObjectExample() + { + // snippet.mute_user_channel_object_example + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + await channel.SetRestrictions( + "support_agent_15", + new Restriction() + { + Ban = false, + Mute = true, + Reason = string.Empty + } + ); + } + // snippet.end + } + + public static async Task BanUserChatObjectExample() + { + // snippet.ban_user_chat_object_example + await chat.SetRestriction( + "support_agent_15", + "support", + new Restriction() + { + Ban = true, + Mute = false, + Reason = "Violated community guidelines" + } + ); + // snippet.end + } + + public static async Task BanUserUserObjectExample() + { + // snippet.ban_user_user_object_example + var userResult = await chat.GetUser("support_agent_15"); + if (!userResult.Error) + { + var user = userResult.Result; + await user.SetRestriction( + "support", + new Restriction() + { + Ban = true, + Mute = false, + Reason = "Violated community guidelines" + } + ); + } + // snippet.end + } + + public static async Task BanUserChannelObjectExample() + { + // snippet.ban_user_channel_object_example + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + await channel.SetRestrictions( + "support_agent_15", + new Restriction() + { + Ban = true, + Mute = false, + Reason = "Violated community guidelines" + } + ); + } + // snippet.end + } + + public static async Task GetChannelRestrictionsExample() + { + // snippet.get_channel_restrictions_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + var userResult = await chat.GetUser("support_agent_15"); + if (userResult.Error) + { + Debug.Log("Couldn't find user!"); + return; + } + var user = userResult.Result; + + // check user restrictions + var restrictionResult = await user.GetChannelRestrictions(channel); + var restriction = restrictionResult.Result; + // snippet.end + } + + public static async Task GetUserRestrictionsExample() + { + // snippet.get_user_restrictions_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + var userResult = await chat.GetUser("support_agent_15"); + if (userResult.Error) + { + Debug.Log("Couldn't find user!"); + return; + } + var restrictedUser = userResult.Result; + + var restrictionResult = await channel.GetUserRestrictions(restrictedUser); + var restriction = restrictionResult.Result; + // snippet.end + } + + public static async Task GetChannelsRestrictionsExample() + { + // snippet.get_channels_restrictions_example + var userResult = await chat.GetUser("support_agent_15"); + if(userResult.Error){ + return; + } + var user = userResult.Result; + var restrictionsWrapperResult = await user.GetChannelsRestrictions(); + if (!restrictionsWrapperResult.Error) + { + foreach(var restriction in restrictionsWrapperResult.Result.Restrictions) + { + Debug.Log($"Channel: {restriction.ChannelId}, Ban: {restriction.Ban}, Mute: {restriction.Mute}"); + } + } + // snippet.end + } + + public static async Task GetUsersRestrictionsExample() + { + // snippet.get_users_restrictions_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) return; + var channel = channelResult.Result; + + var restrictionsWrapperResult = await channel.GetUsersRestrictions(); + if (!restrictionsWrapperResult.Error) + { + foreach(var userRestriction in restrictionsWrapperResult.Result.Restrictions) + { + Debug.Log( + $"User: {userRestriction.UserId}, " + + $"Banned: {userRestriction.Ban}, " + + $"Muted: {userRestriction.Mute}, " + + $"Reason: {userRestriction.Reason}"); + } + } + // snippet.end + } + + public static async Task SetListeningForModerationEventsExample() + { + // snippet.set_listening_for_moderation_events_example + var userResult = await chat.GetUser("support_agent_15"); + if(userResult.Error){ + Debug.Log("Couldn't find user!"); + return; + } + var user = userResult.Result; + + user.SetListeningForModerationEvents(true); + + user.OnModerationEvent += OnModerationEventHandler; // or use lambda + + void OnModerationEventHandler(ChatEvent moderationEvent) + { + Debug.Log($"Moderation event received, payload: {moderationEvent.Payload}"); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ModerationUserSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/ModerationUserSample.cs.meta new file mode 100644 index 0000000..21ff6e9 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ModerationUserSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a29c09847e37d6a478efca2a985c95c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/PinnedMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/PinnedMessageSample.cs new file mode 100644 index 0000000..355da8a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/PinnedMessageSample.cs @@ -0,0 +1,164 @@ +// snippet.using +using System; +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class PinnedMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task PinMessageUsingPinExample() + { + // snippet.pin_message_using_pin_example + // get the "incident-management" channel + var channelResult = await chat.GetChannel("incident-management"); + if (channelResult.Error) + { + Debug.Log("Incident-management channel not found."); + return; + } + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // retrieve the message history with the desired count + var messageHistoryResult = await channel.GetMessageHistory(null, null, 1); + if (messageHistoryResult.Error) + { + Debug.Log("Could not retrieve message history."); + return; + } + + // get the last message from the returned list + var lastMessage = messageHistoryResult.Result.FirstOrDefault(); + + // pin the last message if it exists + if (lastMessage != null) + { + await lastMessage.Pin(); + Debug.Log("Pinned the last message in the incident-management channel."); + } + else + { + Debug.Log("No messages found in the channel history."); + } + // snippet.end + } + + public static async Task PinMessageUsingChannelExample() + { + // snippet.pin_message_using_channel_example + // get the "incident-management" channel + var channelResult = await chat.GetChannel("incident-management"); + if (channelResult.Error) + { + Debug.Log("Incident-management channel not found."); + return; + } + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // retrieve the message history with the desired count + var messageHistoryResult = await channel.GetMessageHistory(null, null, 1); + if (messageHistoryResult.Error) + { + Debug.Log("Could not retrieve message history."); + return; + } + + // get the last message from the returned list + var lastMessage = messageHistoryResult.Result.FirstOrDefault(); + + // pin the last message using the PinMessage method if it exists + if (lastMessage != null) + { + await channel.PinMessage(lastMessage); + Debug.Log("Pinned the last message in the incident-management channel."); + } + else + { + Debug.Log("No messages found in the channel history."); + } + // snippet.end + } + + public static async Task GetPinnedMessageExample() + { + // snippet.get_pinned_message_example + var channelResult = await chat.GetChannel("incident-management"); + if (channelResult.Error) + { + Debug.Log("Channel 'incident-management' not found."); + return; + } + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // Try to get the pinned message from the channel + var pinnedMessageResult = await channel.GetPinnedMessage(); + if (!pinnedMessageResult.Error) + { + var pinnedMessage = pinnedMessageResult.Result; + Debug.Log("Pinned message found: " + pinnedMessage.MessageText); + } + else + { + Debug.Log("No pinned message found."); + } + // snippet.end + } + + public static async Task UnpinMessageExample() + { + // snippet.unpin_message_example + // attempt to get the channel named "incident-management" + var channelResult = await chat.GetChannel("incident-management"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // attempt to unpin a message + try + { + await channel.UnpinMessage(); + Debug.Log("Message has been unpinned successfully."); + } + catch (Exception ex) + { + Debug.Log($"Failed to unpin the message: {ex.Message}"); + } + } + else + { + Debug.Log("Channel 'incident-management' not found."); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/PinnedMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/PinnedMessageSample.cs.meta new file mode 100644 index 0000000..f4f822e --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/PinnedMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c24af8938e198a4438c85835a1be9d7b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/PresenceUserSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/PresenceUserSample.cs new file mode 100644 index 0000000..66f1cdc --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/PresenceUserSample.cs @@ -0,0 +1,198 @@ +// snippet.using +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class PresenceUserSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task WherePresentUserObjectExample() + { + // snippet.where_present_user_object_example + var userResult = await chat.GetUser("support_agent_15"); + if (userResult.Error) + { + Debug.Log("Couldn't find user!"); + return; + } + var user = userResult.Result; + + var channelIdsResult = await user.WherePresent(); + if (!channelIdsResult.Error) + { + var channelIds = channelIdsResult.Result; + } + // snippet.end + } + + public static async Task WherePresentChatObjectExample() + { + // snippet.where_present_chat_object_example + // reference the "chat" object and invoke the "wherePresent()" method. + var channelIdsResult = await chat.WherePresent("support_agent_15"); + if (!channelIdsResult.Error) + { + var channelIds = channelIdsResult.Result; + } + // snippet.end + } + + public static async Task IsPresentOnUserObjectExample() + { + // snippet.is_present_on_user_object_example + var userResult = await chat.GetUser("support_agent_15"); + if (userResult.Error) + { + Debug.Log("Couldn't find user!"); + return; + } + var user = userResult.Result; + + var isPresentOnResult = await user.IsPresentOn("support"); + if (!isPresentOnResult.Error) + { + var isPresentOn = isPresentOnResult.Result; + } + // snippet.end + } + + public static async Task IsUserPresentChannelObjectExample() + { + // snippet.is_user_present_channel_object_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + var isPresentResult = await channel.IsUserPresent("support_agent_15"); + if (!isPresentResult.Error) + { + var isPresent = isPresentResult.Result; + } + // snippet.end + } + + public static async Task IsPresentChatObjectExample() + { + // snippet.is_present_chat_object_example + var isPresentResult = await chat.IsPresent("support_agent_15", "support"); + if (!isPresentResult.Error) + { + var isPresent = isPresentResult.Result; + } + // snippet.end + } + + public static async Task WhoIsPresentChannelObjectExample() + { + // snippet.who_is_present_channel_object_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + var userIdsResult = await channel.WhoIsPresent(); + if (!userIdsResult.Error) + { + var userIds = userIdsResult.Result; + } + // snippet.end + } + + public static async Task WhoIsPresentChatObjectExample() + { + // snippet.who_is_present_chat_object_example + var userIdsResult = await chat.WhoIsPresent("support"); + if (!userIdsResult.Error) + { + var userIds = userIdsResult.Result; + } + // snippet.end + } + + public static async Task OnPresenceUpdateExample() + { + // snippet.on_presence_update_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + channel.OnPresenceUpdate += OnPresenceUpdateHandler; // or use lambda + + void OnPresenceUpdateHandler(List users) + { + Debug.Log($"Users present: {string.Join(", ", users)}"); + } + // snippet.end + } + + public static async Task CheckUserActiveStatusExample() + { + // snippet.check_user_active_status_example + var userResult = await chat.GetUser("support_agent_15"); + if (!userResult.Error) + { + var user = userResult.Result; + Debug.Log($"Is user active?: {user.Active}"); + } + else + { + Debug.Log("User not found."); + } + // snippet.end + } + + public static async Task GetLastActiveTimestampExample() + { + // snippet.get_last_active_timestamp_example + // Get the user's last active timestamp + var userResult = await chat.GetUser("support_agent_15"); + if (!userResult.Error) + { + var user = userResult.Result; + Debug.Log($"User last active timestamp: {user.LastActiveTimeStamp}"); + } + else + { + Debug.Log("User not found."); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/PresenceUserSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/PresenceUserSample.cs.meta new file mode 100644 index 0000000..e193e88 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/PresenceUserSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8467b04420fab7f4cb05aaa72889c79b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/QuotesMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/QuotesMessageSample.cs new file mode 100644 index 0000000..ee5d9b0 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/QuotesMessageSample.cs @@ -0,0 +1,97 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class QuotesMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task QuoteMessageExample() + { + // snippet.quote_message_example + var quotedMessageResult = await chat.GetMessage("support", "16200000000000001"); + if(quotedMessageResult.Error) + { + return; + } + var quotedMessage = quotedMessageResult.Result; + + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) return; + var testChannel = channelResult.Result; + + await testChannel.SendText("message with a quote", new SendTextParams() + { + QuotedMessage = quotedMessage + }); + // snippet.end + } + + public static async Task GetQuotedMessageExample() + { + // snippet.get_quoted_message_example + var channelId = "support"; + var messageTimeToken = "16200000000000001"; + + // retrieve the channel details + var channelResult = await chat.GetChannel(channelId); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // retrieve the specific message by its timetoken + var messageResult = await channel.GetMessage(messageTimeToken); + if (!messageResult.Error) + { + var message = messageResult.Result; + // try to get the quoted message + var quotedMessageResult = await message.GetQuotedMessage(); + if (!quotedMessageResult.Error) + { + var quotedMessage = quotedMessageResult.Result; + Debug.Log($"Quoted message: {quotedMessage.MessageText}"); + } + else + { + Debug.Log("No quoted message found."); + } + } + else + { + Debug.Log("Message not found."); + } + } + else + { + Debug.Log("Channel not found."); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/QuotesMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/QuotesMessageSample.cs.meta new file mode 100644 index 0000000..279eb84 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/QuotesMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fe677930ec857b84aad8cf3b77f73155 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ReactionsMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/ReactionsMessageSample.cs new file mode 100644 index 0000000..0875fc0 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ReactionsMessageSample.cs @@ -0,0 +1,155 @@ +// snippet.using +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class ReactionsMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task AddReactionExample() + { + // snippet.add_reaction_example + // reference the "support" channel and ensure it's found + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Support channel not found."); + return; + } + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // get the message history with the desired count + var messageHistoryResult = await channel.GetMessageHistory(null, null, 1); + if (messageHistoryResult.Error) + { + Debug.Log("Could not retrieve message history."); + return; + } + + // get the last message from the returned list + var lastMessage = messageHistoryResult.Result.FirstOrDefault(); + + if (lastMessage != null) + { + // add the "thumb up" emoji to the last message + await lastMessage.ToggleReaction("\\u{1F44D}"); + Debug.Log("Added 'thumb up' reaction to the last message."); + } + else + { + Debug.Log("No messages found in the channel history."); + } + // snippet.end + } + + public static async Task GetReactionsExample() + { + // snippet.get_reactions_example + // reference the "support" channel and ensure it's found + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Support channel not found."); + return; + } + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // get the message history with the desired count + var messageHistoryResult = await channel.GetMessageHistory(null, null, 1); + if (messageHistoryResult.Error) + { + Debug.Log("Could not retrieve message history."); + return; + } + + // get the last message from the returned list + var lastMessage = messageHistoryResult.Result.FirstOrDefault(); + + if (lastMessage != null) + { + // output all reactions added to the last message + var reactions = lastMessage.Reactions; + foreach (var reaction in reactions) + { + Debug.Log($"Reaction: {reaction.Value}"); + } + } + else + { + Debug.Log("No messages found in the channel history."); + } + // snippet.end + } + + public static async Task CheckUserReactionExample() + { + // snippet.check_user_reaction_example + // reference the "support" channel and ensure it's found + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Support channel not found."); + return; + } + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // get the message history with the desired count + var messageHistoryResult = await channel.GetMessageHistory(null, null, 1); + if (messageHistoryResult.Error) + { + Debug.Log("Could not retrieve message history."); + return; + } + + // get the last message from the returned list + var lastMessage = messageHistoryResult.Result.FirstOrDefault(); + + if (lastMessage != null) + { + // Check if the current user added the "thumb up" emoji to the last message + if (lastMessage.HasUserReaction("\\u{1F44D}")) + { + Debug.Log("The current user has added a 'thumb up' reaction to the last message."); + } + else + { + Debug.Log("The current user has not added a 'thumb up' reaction to the last message."); + } + } + else + { + Debug.Log("No messages found in the channel history."); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ReactionsMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/ReactionsMessageSample.cs.meta new file mode 100644 index 0000000..97eb70a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ReactionsMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 14aecde144940b94cb66e6d3dddde200 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ReadReceiptsMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/ReadReceiptsMessageSample.cs new file mode 100644 index 0000000..94c7f03 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ReadReceiptsMessageSample.cs @@ -0,0 +1,76 @@ +// snippet.using +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class ReadReceiptsMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task ReadReceiptsExample() + { + // snippet.read_receipts_example + // reference the channel where you want to listen to message signals + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // join the channel and start listening to read receipt events + await channel.Join(); + channel.SetListeningForReadReceiptsEvents(true); + + // subscribe to the OnReadReceiptEvent event + channel.OnReadReceiptEvent += OnReadHandler; + } + else + { + Debug.Log("Channel not found"); + } + + // the event handler + void OnReadHandler(Dictionary> readEvent) + { + // print the message details to the console + foreach (var kvp in readEvent) + { + var channel = kvp.Key; + foreach (var user in kvp.Value) + { + Debug.Log( + $"Received a read receipt event on channel {channel}" + + $" from user {user}"); + } + } + // you can add additional logic here, such as confirming receipt to the user or processing the message further + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ReadReceiptsMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/ReadReceiptsMessageSample.cs.meta new file mode 100644 index 0000000..ca73a9e --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ReadReceiptsMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 27ea1391c6241c8448c6119c9abc2f4d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/RestoreMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/RestoreMessageSample.cs new file mode 100644 index 0000000..6e8cd9c --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/RestoreMessageSample.cs @@ -0,0 +1,68 @@ +// snippet.using +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class RestoreMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task RestoreMessageExample() + { + // snippet.restore_message_example + // reference the "channel" object + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Channel 'support' not found."); + return; + } + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // invoke the method on the "channel" object to get message history + var messagesResult = await channel.GetMessageHistory( + "16200000000000000", + "16200000000000001", + 1 + ); + if (messagesResult.Error || !messagesResult.Result.Any()) + { + Debug.Log("Message not found."); + return; + } + + // Find the specific message with the given timetoken + var message = messagesResult.Result[0]; + + // Restore the message + await message.Restore(); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/RestoreMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/RestoreMessageSample.cs.meta new file mode 100644 index 0000000..13aeca3 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/RestoreMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4dd2a075e8ffae24293ac6650c63449e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/SendReceiveMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/SendReceiveMessageSample.cs new file mode 100644 index 0000000..d477e11 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/SendReceiveMessageSample.cs @@ -0,0 +1,50 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class SendReceiveMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task SendMessageExample() + { + // snippet.send_message_example + // reference the channel where you want to send a text message + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + await channel.SendText("Hi everyone!"); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/SendReceiveMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/SendReceiveMessageSample.cs.meta new file mode 100644 index 0000000..449f2e4 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/SendReceiveMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 850ce97abd50c0443bdd0eb5891f8a62 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ThreadsMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/ThreadsMessageSample.cs new file mode 100644 index 0000000..5a88ada --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ThreadsMessageSample.cs @@ -0,0 +1,566 @@ +// snippet.using +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using Channel = PubnubChatApi.Channel; +using UnityEngine; + +// snippet.end + +public class ThreadsMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task CreateThreadExample() + { + // snippet.create_thread_example + // retrieve the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // fetch the last message from the "support" channel + // fetch only the last message + int count = 1; + var messageHistoryResult = await channel.GetMessageHistory(null, null, count); // Omitting unnecessary time tokens + if (messageHistoryResult.Error) + { + Debug.Log("Could not fetch message history."); + return; + } + var lastMessage = messageHistoryResult.Result.FirstOrDefault(); + + // check if there are any messages + if (lastMessage != null) + { + // call the CreateThread method on the last message + var threadChannelResult = lastMessage.CreateThread(); + if (threadChannelResult.Error) + { + Debug.Log($"Could not create thread: {threadChannelResult.Exception.Message}"); + return; + } + var threadChannel = threadChannelResult.Result; + + // (optional) display thread creation information + Debug.Log($"Thread created for message with ID {lastMessage.Id} in channel 'support'."); + Debug.Log($"Thread Channel ID: {threadChannel.Id}"); + } + else + { + Debug.Log("No messages found in the 'support' channel."); + } + } + else + { + Debug.Log("Support channel not found."); + } + // snippet.end + } + + public static async Task SendThreadMessageExample() + { + // snippet.send_thread_message_example + // retrieve the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // fetch the last message from the "support" channel + // fetch only the last message + int count = 1; + var messageHistoryResult = await channel.GetMessageHistory(null, null, count); // omitting unnecessary timetokens + if (messageHistoryResult.Error) + { + Debug.Log("Could not fetch message history."); + return; + } + var lastMessage = messageHistoryResult.Result.FirstOrDefault(); + + // check if there are any messages + if (lastMessage != null) + { + // call the CreateThread method on the last message + var threadChannelResult = lastMessage.CreateThread(); + if (threadChannelResult.Error) + { + Debug.Log($"Could not create thread: {threadChannelResult.Exception.Message}"); + return; + } + var threadChannel = threadChannelResult.Result; + + // (optional) display thread creation information + Debug.Log($"Thread created for message with ID {lastMessage.Id} in channel 'support'."); + Debug.Log($"Thread Channel ID: {threadChannel.Id}"); + + // send a reply in the created thread + string replyMessage = "Good job, guys!"; + await threadChannel.SendText(replyMessage); + Debug.Log($"Sent reply in thread: {replyMessage}"); + } + else + { + Debug.Log("No messages found in the 'support' channel."); + } + } + else + { + Debug.Log("Support channel not found."); + } + // snippet.end + } + + public static async Task GetThreadUsingGetThreadExample() + { + // snippet.get_thread_using_get_thread_example + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + // get the message with the specific timetoken + var messageResult = await channel.GetMessage("16200000000000001"); + if (!messageResult.Error) + { + var message = messageResult.Result; + // get the thread channel created from the message + var threadChannelResult = await message.GetThread(); + if (!threadChannelResult.Error) + { + var threadChannel = threadChannelResult.Result; + Debug.Log($"Thread channel successfully retrieved: {threadChannel.Name}"); + } + else + { + Debug.Log("No thread channel associated with this message."); + } + } + else + { + Debug.Log("Message with the given timetoken not found."); + } + } + else + { + Debug.Log("Channel 'support' not found."); + } + // snippet.end + } + + public static async Task GetThreadUsingGetThreadChannelExample() + { + // snippet.get_thread_using_get_thread_channel_example + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + // get the message with the specific timetoken + var messageResult = await channel.GetMessage("16200000000000001"); + if (!messageResult.Error) + { + var message = messageResult.Result; + // get the thread channel created from the message + var threadChannelResult = await chat.GetThreadChannel(message); + if (!threadChannelResult.Error) + { + var threadChannel = threadChannelResult.Result; + Debug.Log($"Thread channel successfully retrieved: {threadChannel.Name}"); + } + else + { + Debug.Log("No thread channel associated with this message."); + } + } + else + { + Debug.Log("Message with the given timetoken not found."); + } + } + else + { + Debug.Log("Channel 'support' not found."); + } + // snippet.end + } + + public static async Task CheckIfMessageHasThreadExample() + { + // snippet.check_if_message_has_thread_example + // reference the channel object + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + // get the message history that includes the message with the specific time token + var messagesResult = await channel.GetMessageHistory("16200000000000000", null, 1); + + // assuming we get exactly one message in response + if (!messagesResult.Error && messagesResult.Result.Count > 0) + { + var message = messagesResult.Result[0]; + + // check if the message starts a thread + if (message.HasThread()) + { + Debug.Log("The message starts a thread."); + } + else + { + Debug.Log("The message does not start a thread."); + } + } + else + { + Debug.Log("No messages found for the specified time token."); + } + } + else + { + Debug.Log("Channel 'support' not found."); + } + // snippet.end + } + + public static async Task GetThreadChannelUpdatesExample() + { + // snippet.get_thread_channel_updates_example + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + // get the message with the specific timetoken + var messageResult = await channel.GetMessage("16200000000000001"); + if (!messageResult.Error) + { + var message = messageResult.Result; + // get the thread channel created from the message + var threadChannelResult = await message.GetThread(); + if (!threadChannelResult.Error) + { + var threadChannel = threadChannelResult.Result; + Debug.Log($"Thread channel successfully retrieved: {threadChannel.Name}"); + + // subscribe to updates on the thread channel + threadChannel.OnChannelUpdate += OnThreadChannelUpdateHandler; + } + else + { + Debug.Log("No thread channel associated with this message."); + } + } + else + { + Debug.Log("Message with the given timetoken not found."); + } + } + else + { + Debug.Log("Channel 'support' not found."); + } + + // handler for thread channel updates + void OnThreadChannelUpdateHandler(Channel threadChannel) + { + Debug.Log($"Thread channel updated: {threadChannel.Id}"); + } + // snippet.end + } + + public static async Task GetHistoricalThreadMessagesExample() + { + // snippet.get_historical_thread_messages_example + // reference the "channel" object + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // get the last message in the channel + var messageHistoryResult = await channel.GetMessageHistory(null, null, 1); + var lastMessage = messageHistoryResult.Result.FirstOrDefault(); + if (lastMessage != null) + { + Debug.Log($"Found last message with timetoken {lastMessage.TimeToken}"); + + // check if the last message has a thread and fetch its thread channel + var threadChannelResult = await lastMessage.GetThread(); + if (!threadChannelResult.Error) + { + var threadChannel = threadChannelResult.Result; + Debug.Log($"Thread channel successfully retrieved: {threadChannel.Name}"); + + // fetch 10 historical thread messages older than timetoken 15343325214676133 + var threadMessagesResult = await threadChannel.GetMessageHistory("15343325214676133", null, 10); + if (!threadMessagesResult.Error) + { + Debug.Log($"Retrieved {threadMessagesResult.Result.Count} historical thread messages."); + foreach (var threadMessage in threadMessagesResult.Result) + { + Debug.Log($"Thread message: {threadMessage.MessageText}"); + } + } + else + { + Debug.Log("Could not retrieve historical thread messages."); + } + } + else + { + Debug.Log("No thread channel associated with this message."); + } + } + else + { + Debug.Log("No messages found in the 'support' channel."); + } + } + else + { + Debug.Log("Support channel not found."); + } + // snippet.end + } + + public static async Task RemoveThreadExample() + { + // snippet.remove_thread_example + // reference the "channel" object + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // get the last message in the channel + var messageHistoryResult = await channel.GetMessageHistory(null, null, 1); + var lastMessage = messageHistoryResult.Result.FirstOrDefault(); + if (lastMessage != null) + { + Debug.Log($"Found last message with timetoken {lastMessage.TimeToken}"); + + // remove the thread for the last message + await lastMessage.RemoveThread(); + Debug.Log("Thread removed successfully."); + } + else + { + Debug.Log("No messages found in the 'support' channel."); + } + } + else + { + Debug.Log("Support channel not found."); + } + // snippet.end + } + + public static async Task PinMessageToThreadChannelExample() + { + // snippet.pin_message_to_thread_channel_example + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // get the last message on the channel, which is the root message for the thread + var channelMessageHistoryResult = await channel.GetMessageHistory(null, null, 1); + var lastChannelMessage = channelMessageHistoryResult.Result.FirstOrDefault(); + if (lastChannelMessage != null) + { + Debug.Log($"Found last channel message with timetoken {lastChannelMessage.TimeToken}"); + + // get the thread channel created from the message + var threadChannelResult = await lastChannelMessage.GetThread(); + if (!threadChannelResult.Error) + { + var threadChannel = threadChannelResult.Result; + Debug.Log($"Thread channel successfully retrieved: {threadChannel.Name}"); + + // get the last message from the thread channel + var threadMessageHistoryResult = await threadChannel.GetMessageHistory(null, null, 1); + var lastThreadMessage = threadMessageHistoryResult.Result.FirstOrDefault(); + if (lastThreadMessage != null) + { + Debug.Log($"Found last thread message with timetoken {lastThreadMessage.TimeToken}"); + + // pin the last thread message to the thread channel + await threadChannel.PinMessage(lastThreadMessage); + Debug.Log("Message pinned to thread channel successfully."); + } + else + { + Debug.Log("No messages found in the thread channel."); + } + } + else + { + Debug.Log("No thread channel associated with this message."); + } + } + else + { + Debug.Log("No messages found in the 'support' channel."); + } + } + else + { + Debug.Log("Support channel not found."); + } + // snippet.end + } + + public static async Task PinMessageToParentChannelExample() + { + // snippet.pin_message_to_parent_channel_example + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // get the last message on the channel, which is the root message for the thread + var channelMessageHistoryResult = await channel.GetMessageHistory(null, null, 1); + var lastChannelMessage = channelMessageHistoryResult.Result.FirstOrDefault(); + if (lastChannelMessage != null) + { + Debug.Log($"Found last channel message with timetoken {lastChannelMessage.TimeToken}"); + + // get the thread channel created from the message + var threadChannelResult = await lastChannelMessage.GetThread(); + if (!threadChannelResult.Error) + { + var threadChannel = threadChannelResult.Result; + Debug.Log($"Thread channel successfully retrieved: {threadChannel.Name}"); + + // get the last message from the thread channel + var threadMessageHistoryResult = await threadChannel.GetMessageHistory(null, null, 1); + var lastThreadMessage = threadMessageHistoryResult.Result.FirstOrDefault(); + if (lastThreadMessage != null) + { + Debug.Log($"Found last thread message with timetoken {lastThreadMessage.TimeToken}"); + + // pin the last thread message to the parent channel + await channel.PinMessage(lastThreadMessage); + Debug.Log("Thread message pinned to parent channel successfully."); + } + else + { + Debug.Log("No messages found in the thread channel."); + } + } + else + { + Debug.Log("No thread channel associated with this message."); + } + } + else + { + Debug.Log("No messages found in the 'support' channel."); + } + } + else + { + Debug.Log("Support channel not found."); + } + // snippet.end + } + + public static async Task UnpinMessageFromThreadChannelExample() + { + // snippet.unpin_message_from_thread_channel_example + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // get the last message on the channel, which is the root message for the thread + var channelMessageHistoryResult = await channel.GetMessageHistory(null, null, 1); + var lastChannelMessage = channelMessageHistoryResult.Result.FirstOrDefault(); + if (lastChannelMessage != null) + { + Debug.Log($"Found last channel message with timetoken {lastChannelMessage.TimeToken}"); + + // get the thread channel created from the message + var threadChannelResult = await lastChannelMessage.GetThread(); + if (!threadChannelResult.Error) + { + var threadChannel = threadChannelResult.Result; + Debug.Log($"Thread channel successfully retrieved: {threadChannel.Name}"); + + // unpin the message from the thread channel + await threadChannel.UnpinMessage(); + Debug.Log("Message unpinned from thread channel successfully."); + } + else + { + Debug.Log("No thread channel associated with this message."); + } + } + else + { + Debug.Log("No messages found in the 'support' channel."); + } + } + else + { + Debug.Log("Support channel not found."); + } + // snippet.end + } + + public static async Task UnpinMessageFromParentChannelExample() + { + // snippet.unpin_message_from_parent_channel_example + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // unpin the message from the parent channel + await channel.UnpinMessage(); + Debug.Log("Message unpinned from parent channel successfully."); + } + else + { + Debug.Log("Support channel not found."); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/ThreadsMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/ThreadsMessageSample.cs.meta new file mode 100644 index 0000000..b501621 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/ThreadsMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c05bf748fff58224f82e3915785cab0b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/TypingIndicatorSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/TypingIndicatorSample.cs new file mode 100644 index 0000000..1d1f217 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/TypingIndicatorSample.cs @@ -0,0 +1,109 @@ +// snippet.using +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; +// snippet.end + +public class TypingIndicatorSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task StartTypingExample() + { + // snippet.start_typing_example + // reference the channel where you want to listen to typing signals + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + // invoke the "startTyping()" method + await channel.StartTyping(); + // snippet.end + } + + public static async Task StopTypingExample() + { + // snippet.stop_typing_example + // reference the channel where you want to listen to typing signals + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + // invoke the "StopTyping()" method + await channel.StopTyping(); + // snippet.end + } + + public static async Task GetTypingEventsExample() + { + // snippet.get_typing_events_example + // reference the channel where you want to listen to typing signals + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // join the channel, start listening for typing + await channel.Join(); + channel.SetListeningForTyping(true); + + // subscribe to the OnUsersTyping event + channel.OnUsersTyping += OnUsersTypingHandler; + + await Task.Delay(4000); + + // indicate that typing has started + await channel.StartTyping(); + } + else + { + Debug.Log("Channel not found"); + } + + // event handler for typing events + void OnUsersTypingHandler(List users) + { + if (users.Count > 0) + { + Debug.Log($"Users typing: {string.Join(", ", users)}"); + } + else + { + Debug.Log("No users are currently typing"); + } + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/TypingIndicatorSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/TypingIndicatorSample.cs.meta new file mode 100644 index 0000000..669e855 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/TypingIndicatorSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9fb9abf92ff06214a88979cc658212fd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/UnreadMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/UnreadMessageSample.cs new file mode 100644 index 0000000..d616869 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/UnreadMessageSample.cs @@ -0,0 +1,279 @@ +// snippet.using +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class UnreadMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task GetLastReadMessageTimetokenExample() + { + // snippet.get_last_read_message_timetoken_example + // reference the "support_agent_15" user + var userResult = await chat.GetUser("support_agent_15"); + if (!userResult.Error) + { + var user = userResult.Result; + Debug.Log($"Found user with name {user.UserName}"); + + // get the list of all user memberships + var membershipsResponse = await user.GetMemberships(); + + if (!membershipsResponse.Error) + { + // extract the actual memberships (support channel only) from the response + var memberships = membershipsResponse.Result.Memberships.Where(x => x.ChannelId == "support").ToList(); + + // since we filtered for the "support" channel, we should find it directly + var membership = memberships.FirstOrDefault(); + + if (membership != null) + { + // retrieve the last read message timetoken + var lastReadToken = membership.LastReadMessageTimeToken; + Debug.Log($"The last read message timetoken for user {user.UserName} on channel 'support' is {lastReadToken}"); + } + else + { + Debug.Log("The user 'support_agent_15' is not a member of the 'support' channel."); + } + } + } + else + { + Debug.Log("User 'support_agent_15' not found."); + } + // snippet.end + } + + public static async Task GetUnreadMessagesCountOneChannelExample() + { + // snippet.get_unread_messages_count_one_channel_example + // reference the "support_agent_15" user + var userResult = await chat.GetUser("support_agent_15"); + if (!userResult.Error) + { + var user = userResult.Result; + Debug.Log($"Found user with name {user.UserName}"); + + // get the list of all user memberships using the GetMemberships method + var membershipsResponse = await user.GetMemberships(); + + if (!membershipsResponse.Error) + { + // extract the actual memberships from the response + var memberships = membershipsResponse.Result.Memberships; + + // filter out the membership for the "support" channel + var membership = memberships.FirstOrDefault(m => m.ChannelId == "support"); + + if (membership != null) + { + // retrieve the number of unread messages + var unreadMessagesCountResult = await membership.GetUnreadMessagesCount(); + if (!unreadMessagesCountResult.Error) + { + Debug.Log($"The number of unread messages for user {user.UserName} on channel 'support' is {unreadMessagesCountResult.Result}"); + } + } + else + { + Debug.Log("The user 'support_agent_15' is not a member of the 'support' channel."); + } + } + } + else + { + Debug.Log("User 'support_agent_15' not found."); + } + // snippet.end + } + + public static async Task GetUnreadMessagesCountAllChannelsExample() + { + // snippet.get_unread_messages_count_all_channels_example + // retrieve the current user + var currentUserResult = await chat.GetCurrentUser(); + if (!currentUserResult.Error) + { + var currentUser = currentUserResult.Result; + Debug.Log($"Current user is {currentUser.UserName}"); + + // retrieve the unread message counts for the current user with default parameters + var unreadMessageCountsResult = await chat.GetUnreadMessagesCounts(); + if (!unreadMessageCountsResult.Error) + { + // process and display the retrieved unread message counts + foreach (var unreadMessage in unreadMessageCountsResult.Result) + { + Debug.Log($"Channel ID: {unreadMessage.ChannelId}, Unread Messages: {unreadMessage.Count}"); + } + } + } + else + { + Debug.Log("Current user not found."); + } + // snippet.end + } + + public static async Task SetLastReadMessageExample() + { + // snippet.set_last_read_message_example + // reference the "support_agent_15" user + var userResult = await chat.GetUser("support_agent_15"); + if (!userResult.Error) + { + var user = userResult.Result; + Debug.Log($"Found user with name {user.UserName}"); + + // get the list of all user memberships + var membershipsResponse = await user.GetMemberships(); + + if (!membershipsResponse.Error) + { + // filter out the right channel + var membership = membershipsResponse.Result.Memberships.FirstOrDefault(m => m.ChannelId == "support"); + if (membership != null) + { + Debug.Log($"Found membership for channel: {membership.ChannelId}"); + + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // return the message object with the "16200000000000001" timetoken + var messageResult = await channel.GetMessage("16200000000000001"); + if (!messageResult.Error) + { + var message = messageResult.Result; + Debug.Log($"Is deleted?: {message.IsDeleted}"); + + // set the last read message for the membership + await membership.SetLastReadMessage(message); + Debug.Log($"Last read message set for user {user.UserName} in channel {channel.Name}"); + } + else + { + Debug.Log("Message with the specified timetoken not found."); + } + } + else + { + Debug.Log("Channel 'support' not found."); + } + } + else + { + Debug.Log("Membership for 'support' channel not found."); + } + } + } + else + { + Debug.Log("User 'support_agent_15' not found."); + } + // snippet.end + } + + public static async Task SetLastReadMessageTimetokenExample() + { + // snippet.set_last_read_message_timetoken_example + // reference the "support_agent_15" user + var userResult = await chat.GetUser("support_agent_15"); + if (!userResult.Error) + { + var user = userResult.Result; + Debug.Log($"Found user with name {user.UserName}"); + + // get the list of all user memberships + var membershipsResponse = await user.GetMemberships(); + + if (!membershipsResponse.Error) + { + // filter out the right channel + var membership = membershipsResponse.Result.Memberships.FirstOrDefault(m => m.ChannelId == "support"); + if (membership != null) + { + Debug.Log($"Found membership for channel: {membership.ChannelId}"); + + // reference the "support" channel + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + Debug.Log($"Found channel with name {channel.Name}"); + + // set the last read message timetoken for the membership + string timeToken = "16200000000000001"; + await membership.SetLastReadMessageTimeToken(timeToken); + Debug.Log($"Last read message timetoken set for user {user.UserName} in channel {channel.Name}"); + } + else + { + Debug.Log("Channel 'support' not found."); + } + } + else + { + Debug.Log("Membership for 'support' channel not found."); + } + } + } + else + { + Debug.Log("User 'support_agent_15' not found."); + } + // snippet.end + } + + public static async Task MarkAllMessagesAsReadExample() + { + // snippet.mark_all_messages_as_read_example + // simulating a previously retrieved page token from the PubNub server + string previouslyReturnedNextPageToken = "NPT"; + + // create an instance of the Page class with the previously returned next page token + PNPageObject nextPage = new PNPageObject + { + Next = previouslyReturnedNextPageToken + }; + + // mark a total of 50 messages as read from the next page + var result = await chat.MarkAllMessagesAsRead(limit: 50, page: nextPage); + + // process the result as needed + Debug.Log("Messages marked as read successfully"); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/UnreadMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/UnreadMessageSample.cs.meta new file mode 100644 index 0000000..ccdc350 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/UnreadMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7bdc72939bfef8849b3be722a04a1a7c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesChannelSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesChannelSample.cs new file mode 100644 index 0000000..19a54d2 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesChannelSample.cs @@ -0,0 +1,103 @@ +// snippet.using +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using Channel = PubnubChatApi.Channel; +using UnityEngine; + +// snippet.end + +public class UpdatesChannelSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task UpdateChannelUsingChannelObjectExample() + { + // snippet.update_channel_using_channel_object_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + var updatedChannelData = new ChatChannelData + { + Description = "Channel for CRM tickets" + }; + + await channel.Update(updatedChannelData); + // snippet.end + } + + public static async Task UpdateChannelUsingChatObjectExample() + { + // snippet.update_channel_using_chat_object_example + var updatedChannelData = new ChatChannelData + { + Description = "Channel for CRM tickets" + }; + + await chat.UpdateChannel("support", updatedChannelData); + // snippet.end + } + + public static async Task ListenForChannelUpdatesExample() + { + // snippet.listen_for_channel_updates_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + channel.SetListeningForUpdates(true); + channel.OnChannelUpdate += OnChannelUpdateHandler; // or use lambda + + void OnChannelUpdateHandler(Channel channel) + { + Debug.Log($"Channel updated: {channel.Id}"); + } + // snippet.end + } + + public static async Task AddListenerToChannelsUpdateExample() + { + // snippet.add_listener_to_channels_update_example + List channelIds = new List { "support", "incidentManagement" }; + Action listener = (Channel channel) => + { + // Print the updated channel name + Debug.Log("Updated Channel Name: " + channel.Name); + }; + + await chat.AddListenerToChannelsUpdate(channelIds, listener); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesChannelSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesChannelSample.cs.meta new file mode 100644 index 0000000..fc77f26 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesChannelSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c94afd7a690141a4187bd44732994c96 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesMessageSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesMessageSample.cs new file mode 100644 index 0000000..daf3e88 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesMessageSample.cs @@ -0,0 +1,111 @@ +// snippet.using +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class UpdatesMessageSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task EditMessageTextExample(Message message) + { + // snippet.edit_message_text_example + await message.EditMessageText("Your ticket number is 78398"); + // snippet.end + } + + public static async Task SetListeningForUpdatesExample() + { + // snippet.set_listening_for_updates_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + // get first message from history + var messageResult = await channel.GetMessageHistory(null, null, 1); + if (messageResult.Error || !messageResult.Result.Any()) + { + Debug.Log("Couldn't find message!"); + return; + } + var message = messageResult.Result.First(); + message.SetListeningForUpdates(true); + message.OnMessageUpdated += OnMessageUpdatedHandler; // or use lambda + + void OnMessageUpdatedHandler(Message message) + { + Debug.Log($"Message updated"); + } + // snippet.end + } + + public static async Task AddListenerToMessagesUpdateExample() + { + // snippet.add_listener_to_messages_update_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + var messagesResult = await channel.GetMessageHistory("15343325214676133", null, 10); + if (messagesResult.Error) + { + Debug.Log("Couldn't get message history!"); + return; + } + var messages = messagesResult.Result; + + List timetokens = new List(); + + // get the timetokens + foreach (var message in messages) + { + // Get the time token of the current message + string timeToken = message.TimeToken; + + // Add the time token to the list + timetokens.Add(timeToken); + } + + void OnMessageUpdatedHandler(Message message) + { + Debug.Log($"Message updated"); + } + + chat.AddListenerToMessagesUpdate(channelId: "support", messageTimeTokens: timetokens, listener: OnMessageUpdatedHandler); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesMessageSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesMessageSample.cs.meta new file mode 100644 index 0000000..3fea4bc --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesMessageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5891bee9cebd0bd47a2d07a45bbbab45 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesUserSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesUserSample.cs new file mode 100644 index 0000000..f1e28fd --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesUserSample.cs @@ -0,0 +1,99 @@ +// snippet.using +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class UpdatesUserSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task UpdateUserUsingUserObjectExample() + { + // snippet.update_user_using_user_object_example + var userResult = await chat.GetUser("user_id"); + if (userResult.Error) + { + Debug.Log("Couldn't find user!"); + return; + } + var user = userResult.Result; + var updatedUserData = new ChatUserData + { + ProfileUrl = "https://www.linkedin.com/mkelly_vp2" + }; + await user.Update(updatedUserData); + // snippet.end + } + + public static async Task UpdateUserUsingChatObjectExample() + { + // snippet.update_user_using_chat_object_example + var updatedUserData = new ChatUserData + { + ProfileUrl = "https://www.linkedin.com/mkelly_vp2" + }; + await chat.UpdateUser("support_agent_15", updatedUserData); + // snippet.end + } + + public static async Task SetListeningForUpdatesExample() + { + // snippet.set_listening_for_updates_example + var userResult = await chat.GetUser("support_agent_15"); + if (userResult.Error) + { + Debug.Log("Couldn't find user!"); + return; + } + var user = userResult.Result; + + user.SetListeningForUpdates(true); + + user.OnUserUpdated += OnUserUpdatedHandler; // or use lambda + void OnUserUpdatedHandler(User user) + { + Debug.Log($"User updated: {user.Id}"); + } + // snippet.end + } + + public static async Task AddListenerToUsersUpdateExample() + { + // snippet.add_listener_to_users_update_example + List users = new List { "support_agent_15", "support-manager" }; + Action listener = (User user) => + { + // Print the updated user name + Debug.Log("Updated user Name: " + user.UserName); + }; + chat.AddListenerToUsersUpdate(users, listener); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesUserSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesUserSample.cs.meta new file mode 100644 index 0000000..2f1609e --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/UpdatesUserSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6b2798ffb67a3fb43b3ac591a31d7e39 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/UserSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/UserSample.cs new file mode 100644 index 0000000..e38115c --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/UserSample.cs @@ -0,0 +1,51 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class UserSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task UserUpdatedEventExample() + { + // snippet.user_updated_event_example + // Get a user + var userResult = await chat.GetUser("user_id"); + if (!userResult.Error) + { + var user = userResult.Result; + + user.OnUserUpdated += (user) => + { + Debug.Log("User metadata updated!"); + }; + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/UserSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/UserSample.cs.meta new file mode 100644 index 0000000..98da84d --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/UserSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 261f84982ecc4bb4e997f1945517a4af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/WatchChannelSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/WatchChannelSample.cs new file mode 100644 index 0000000..ab7bbe7 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/WatchChannelSample.cs @@ -0,0 +1,69 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using Channel = PubnubChatApi.Channel; +using UnityEngine; + +// snippet.end + +public class WatchChannelSample +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + // snippet.end + } + + public static async Task WatchChannelExample() + { + // snippet.watch_channel_example + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Couldn't find channel!"); + return; + } + var channel = channelResult.Result; + + channel.OnMessageReceived += OnMessageReceivedHandler; // or use lambda + + void OnMessageReceivedHandler(Message message) + { + Debug.Log($"Message received: {message.MessageText}"); + } + + channel.Connect(); + // snippet.end + } + + public static void UnwatchChannelExample(Channel channel) + { + // snippet.unwatch_channel_example + void OnMessageReceivedHandler(Message message) + { + Debug.Log($"Message received: {message.MessageText}"); + channel.Disconnect(); + Debug.Log("Disconnected from the channel."); + } + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/WatchChannelSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/WatchChannelSample.cs.meta new file mode 100644 index 0000000..61dcbd4 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/WatchChannelSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3acff2b517a1d8d4b8fb7a6e631333d8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Packages/manifest.json b/unity-chat/PubnubChatUnity/Packages/manifest.json index 4b7a1ad..25a7c11 100644 --- a/unity-chat/PubnubChatUnity/Packages/manifest.json +++ b/unity-chat/PubnubChatUnity/Packages/manifest.json @@ -1,7 +1,9 @@ { "dependencies": { + "com.pubnub.sdk": "https://github.com/pubnub/unity.git?path=/PubNubUnity/Assets/PubNub", "com.unity.collab-proxy": "2.6.0", "com.unity.feature.development": "1.0.1", + "com.unity.ide.rider": "3.0.37", "com.unity.textmeshpro": "3.0.7", "com.unity.timeline": "1.7.6", "com.unity.ugui": "1.0.0", diff --git a/unity-chat/PubnubChatUnity/Packages/packages-lock.json b/unity-chat/PubnubChatUnity/Packages/packages-lock.json index e55f4e3..ba0ef82 100644 --- a/unity-chat/PubnubChatUnity/Packages/packages-lock.json +++ b/unity-chat/PubnubChatUnity/Packages/packages-lock.json @@ -1,5 +1,14 @@ { "dependencies": { + "com.pubnub.sdk": { + "version": "https://github.com/pubnub/unity.git?path=/PubNubUnity/Assets/PubNub", + "depth": 0, + "source": "git", + "dependencies": { + "com.unity.nuget.newtonsoft-json": "3.0.2" + }, + "hash": "841822252c4f8c932961a3386cd55610b749e319" + }, "com.unity.collab-proxy": { "version": "2.6.0", "depth": 0, @@ -16,7 +25,7 @@ }, "com.unity.ext.nunit": { "version": "1.0.6", - "depth": 2, + "depth": 1, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" @@ -36,8 +45,8 @@ } }, "com.unity.ide.rider": { - "version": "3.0.34", - "depth": 1, + "version": "3.0.37", + "depth": 0, "source": "registry", "dependencies": { "com.unity.ext.nunit": "1.0.6" @@ -60,6 +69,13 @@ "dependencies": {}, "url": "https://packages.unity.com" }, + "com.unity.nuget.newtonsoft-json": { + "version": "3.2.1", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.performance.profile-analyzer": { "version": "1.2.3", "depth": 1,