From cb02638e3fecc468eec2ad740e1a9d2dcab43d9e Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Wed, 9 Jul 2025 14:41:17 +0200 Subject: [PATCH 01/28] add C# SDK dependency, WIP new Chat initialization workflow --- .../PubnubChatApi/Entities/Chat.cs | 62 ++++++++++++++----- .../Entities/Data/PubnubChatConfig.cs | 36 ++++++++--- .../PubnubChatApi/PubnubChatApi.csproj | 1 + .../Utilities/PubnubChatDotNetPNSDKSource.cs | 20 ++++++ 4 files changed, 96 insertions(+), 23 deletions(-) create mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PubnubChatDotNetPNSDKSource.cs diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 4711f76..25059c0 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using PubnubApi; using PubnubChatApi.Entities.Data; using PubnubChatApi.Entities.Events; using PubnubChatApi.Enums; @@ -28,7 +29,8 @@ namespace PubNubChatAPI.Entities public class Chat { #region DLL Imports - + + //TODO: REMOVE [DllImport("pubnub-chat")] private static extern IntPtr pn_chat_new( string publish, @@ -40,9 +42,11 @@ private static extern IntPtr pn_chat_new( int store_user_activity_interval, bool store_user_activity_timestamps); + //TODO: REMOVE [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, @@ -308,38 +312,66 @@ string membership_status private Dictionary messageWrappers = new(); private bool fetchUpdates = true; + public Pubnub PubnubInstance { get; private set; } + public event Action OnAnyEvent; public ChatAccessManager ChatAccessManager { get; } public PubnubChatConfig Config { get; } - - /// - /// 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. - /// + + //TODO: REMOVE public static async Task CreateInstance(PubnubChatConfig config) { var chat = await Task.Run(() => new Chat(config)); chat.FetchUpdatesLoop(); return chat; } - + //TODO: REMOVE internal Chat(PubnubChatConfig config) { - chatPointer = pn_chat_new(config.PublishKey, config.SubscribeKey, config.UserId, config.AuthKey, + chatPointer = pn_chat_new(config.OLD_PublishKey, config.OLD_SubscribehKey, config.OLD_UserId, config.OLD_AuthhKey, config.TypingTimeout, config.TypingTimeoutDifference, config.StoreUserActivityInterval, config.StoreUserActivityTimestamp); CUtilities.CheckCFunctionResult(chatPointer); - Config = config; ChatAccessManager = new ChatAccessManager(chatPointer); } + + /// + /// Initializes a new instance of the class. + /// + /// Creates a new chat instance. + /// + /// + /// Config with Chat specific parameters + /// Config with PubNub keys and values + /// + /// The constructor initializes the Chat object with a new Pubnub instance. + /// + public Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig) + { + PubnubInstance = new Pubnub(pubnubConfig); + Config = chatConfig; + ChatAccessManager = new ChatAccessManager(chatPointer); + } + + /// + /// Initializes a new instance of the class. + /// + /// Creates a new chat instance. + /// + /// + /// Config with Chat specific parameters + /// An already initialised instance of Pubnub + /// + /// The constructor initializes the Chat object with the provided existing Pubnub instance. + /// + public Chat(PubnubChatConfig chatConfig, Pubnub pubnub) + { + Config = chatConfig; + PubnubInstance = pubnub; + ChatAccessManager = new ChatAccessManager(chatPointer); + } #region Updates handling diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs index 20a07d7..c55a70e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs @@ -1,3 +1,5 @@ +using PubnubApi; + namespace PubnubChatApi.Entities.Data { public class PubnubChatConfig @@ -11,10 +13,15 @@ public class RateLimitPerChannel public int UnknownConversation; } - public string PublishKey { get; } - public string SubscribeKey { get; } - public string UserId { get; } - public string AuthKey { get; } + //TODO: REMOVE + public string OLD_PublishKey { get; set; } + //TODO: REMOVE + public string OLD_SubscribehKey { get; set; } + //TODO: REMOVE + public string OLD_AuthhKey { get; set; } + //TODO: REMOVE + public string OLD_UserId { get; set; } + public int TypingTimeout { get; } public int TypingTimeoutDifference { get; } public int RateLimitFactor { get; } @@ -22,19 +29,32 @@ public class RateLimitPerChannel public bool StoreUserActivityTimestamp { get; } public int StoreUserActivityInterval { get; } + //TODO: REMOVE public PubnubChatConfig(string publishKey, string subscribeKey, string userId, string authKey = "", int typingTimeout = 5000, int typingTimeoutDifference = 1000, int rateLimitFactor = 2, RateLimitPerChannel rateLimitPerChannel = null, bool storeUserActivityTimestamp = false, int storeUserActivityInterval = 60000) + { + OLD_PublishKey = publishKey; + OLD_SubscribehKey = subscribeKey; + OLD_AuthhKey = authKey; + OLD_UserId = userId; + RateLimitsPerChannel = rateLimitPerChannel; + RateLimitFactor = rateLimitFactor; + StoreUserActivityTimestamp = storeUserActivityTimestamp; + StoreUserActivityInterval = storeUserActivityInterval; + TypingTimeout = typingTimeout; + TypingTimeoutDifference = typingTimeoutDifference; + } + + public PubnubChatConfig(int typingTimeout = 5000, int typingTimeoutDifference = 1000, int rateLimitFactor = 2, + RateLimitPerChannel rateLimitPerChannel = null, bool storeUserActivityTimestamp = false, + int storeUserActivityInterval = 60000) { RateLimitsPerChannel = 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/PubnubChatApi.csproj b/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj index 3d08172..117b2d3 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj @@ -20,6 +20,7 @@ + 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..96cd096 --- /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.Utilities +{ + 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 From 57a4025e2069027bf668e4aff275022fc919254c Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Wed, 9 Jul 2025 17:28:47 +0200 Subject: [PATCH 02/28] WIP on User entity migration --- .../PubNubChatApi.Tests/ChannelTests.cs | 6 +- .../PubNubChatApi.Tests/ChatEventTests.cs | 2 +- .../PubNubChatApi.Tests/ChatTests.cs | 4 +- .../PubNubChatApi.Tests/MembershipTests.cs | 2 +- .../PubNubChatApi.Tests/MessageDraftTests.cs | 6 +- .../PubNubChatApi.Tests/MessageTests.cs | 2 +- .../PubNubChatApi.Tests/TestUtils.cs | 4 +- .../PubNubChatApi.Tests/ThreadsTests.cs | 2 +- .../PubNubChatApi.Tests/UserTests.cs | 24 +-- .../PubnubChatApi/Entities/Base/ChatEntity.cs | 8 + .../Entities/Base/UniqueChatEntity.cs | 5 + .../PubnubChatApi/Entities/Chat.cs | 155 ++++++++++---- .../PubnubChatApi/Entities/User.cs | 195 +++++++++++++----- .../PubnubChatApi/Utilities/PointerParsers.cs | 2 +- 14 files changed, 294 insertions(+), 123 deletions(-) diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs index 747fc70..fbfb328 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs @@ -19,11 +19,11 @@ public async Task Setup() PubnubTestsParameters.SubscribeKey, "ctuuuuuuuuuuuuuuuuuuuuuuuuuuuuu") ); - if (!chat.TryGetCurrentUser(out user)) + if (!chat.OLD_TryGetCurrentUser(out user)) { Assert.Fail(); } - await user.Update(new ChatUserData() + await user.OLD_Update(new ChatUserData() { Username = "Testificate" }); @@ -87,7 +87,7 @@ public async Task TestDeleteChannel() [Test] public async Task TestLeaveChannel() { - var currentChatUser = await chat.GetCurrentUserAsync(); + var currentChatUser = await chat.OLD_GetCurrentUserAsync(); Assert.IsNotNull(currentChatUser, "currentChatUser was null"); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs index 102fc0a..f1707c4 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs @@ -20,7 +20,7 @@ public async Task Setup() "event_tests_user") ); channel = await chat.CreatePublicConversation("event_tests_channel"); - if (!chat.TryGetCurrentUser(out user)) + if (!chat.OLD_TryGetCurrentUser(out user)) { Assert.Fail(); } diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs index 4a0f187..b46cce9 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -19,7 +19,7 @@ public async Task Setup() PubnubTestsParameters.SubscribeKey, "chats_tests_user_10_no_calkiem_nowy_2")); channel = await chat.CreatePublicConversation("chat_tests_channel_2"); - if (!chat.TryGetCurrentUser(out currentUser)) + if (!chat.OLD_TryGetCurrentUser(out currentUser)) { Assert.Fail(); } @@ -59,7 +59,7 @@ public async Task TestGetCurrentUserMentions() [Test] public async Task TestGetCurrentUser() { - Assert.True(chat.TryGetCurrentUser(out var currentUser) && currentUser.Id == this.currentUser.Id); + Assert.True(chat.OLD_TryGetCurrentUser(out var currentUser) && currentUser.Id == this.currentUser.Id); } [Test] diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs index 958a0e9..e06bef2 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs @@ -20,7 +20,7 @@ public async Task Setup() "membership_tests_user_54") ); channel = await chat.CreatePublicConversation("membership_tests_channel"); - if (!chat.TryGetCurrentUser(out user)) + if (!chat.OLD_TryGetCurrentUser(out user)) { Assert.Fail(); } diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs index fa620b3..d9bdb31 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs @@ -23,7 +23,7 @@ public async Task Setup() { ChannelName = "MessageDraftTestingChannel" }); - if (!chat.TryGetCurrentUser(out var user)) + if (!chat.OLD_TryGetCurrentUser(out var user)) { Assert.Fail(); } @@ -31,9 +31,9 @@ public async Task Setup() channel.Join(); await Task.Delay(3000); - if (!chat.TryGetUser("mock_user", out dummyUser)) + if (!chat.OLD_TryGetUser("mock_user", out dummyUser)) { - dummyUser = await chat.CreateUser("mock_user", new ChatUserData() + dummyUser = await chat.OLD_CreateUser("mock_user", new ChatUserData() { Username = "Mock Usernamiski" }); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs index b226123..3636f7d 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs @@ -20,7 +20,7 @@ public async Task Setup() "message_tests_user_2") ); channel = await chat.CreatePublicConversation("message_tests_channel_2"); - if (!chat.TryGetCurrentUser(out user)) + if (!chat.OLD_TryGetCurrentUser(out user)) { Assert.Fail(); } diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs index 5108a6e..895a4b1 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs @@ -6,13 +6,13 @@ public static class TestUtils { public static async Task GetOrCreateUser(this Chat chat, string userId) { - if (chat.TryGetUser(userId, out var user)) + if (chat.OLD_TryGetUser(userId, out var user)) { return user; } else { - return await chat.CreateUser(userId); + return await chat.OLD_CreateUser(userId); } } } \ 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..888bdc4 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs @@ -21,7 +21,7 @@ public async Task Setup() ); var randomId = Guid.NewGuid().ToString()[..10]; channel = await chat.CreatePublicConversation(randomId); - if (!chat.TryGetCurrentUser(out user)) + if (!chat.OLD_TryGetCurrentUser(out user)) { Assert.Fail(); } diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs index 92df044..5794402 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs @@ -21,7 +21,7 @@ public async Task Setup() storeUserActivityTimestamp: true) ); channel = await chat.CreatePublicConversation("user_tests_channel"); - if (!chat.TryGetCurrentUser(out user)) + if (!chat.OLD_TryGetCurrentUser(out user)) { Assert.Fail(); } @@ -66,18 +66,18 @@ public async Task TestUserUpdate() var newRandomUserName = Guid.NewGuid().ToString(); testUser.OnUserUpdated += updatedUser => { - Assert.True(updatedUser.UserName == newRandomUserName); - Assert.True(updatedUser.CustomData == "{\"some_key\":\"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"); - Assert.True(updatedUser.Status == "yes"); - Assert.True(updatedUser.DataType == "someType"); + Assert.True(updatedUser.OLD_UserName == newRandomUserName); + Assert.True(updatedUser.OLD_CustomData == "{\"some_key\":\"some_value\"}"); + Assert.True(updatedUser.OLD_Email == "some@guy.com"); + Assert.True(updatedUser.OLD_ExternalId == "xxx_some_guy_420_xxx"); + Assert.True(updatedUser.OLD_ProfileUrl == "www.some.guy"); + Assert.True(updatedUser.OLD_Status == "yes"); + Assert.True(updatedUser.OLD_DataType == "someType"); updatedReset.Set(); }; testUser.SetListeningForUpdates(true); await Task.Delay(3000); - await testUser.Update(new ChatUserData() + await testUser.OLD_Update(new ChatUserData() { Username = newRandomUserName, CustomDataJson = "{\"some_key\":\"some_value\"}", @@ -94,15 +94,15 @@ await testUser.Update(new ChatUserData() [Test] public async Task TestUserDelete() { - var someUser = await chat.CreateUser(Guid.NewGuid().ToString()); + var someUser = await chat.OLD_CreateUser(Guid.NewGuid().ToString()); - Assert.True(chat.TryGetUser(someUser.Id, out _), "Couldn't get freshly created user"); + Assert.True(chat.OLD_TryGetUser(someUser.Id, out _), "Couldn't get freshly created user"); await someUser.DeleteUser(); await Task.Delay(3000); - Assert.False(chat.TryGetUser(someUser.Id, out _), "Got the freshly deleted user"); + Assert.False(chat.OLD_TryGetUser(someUser.Id, out _), "Got the freshly deleted user"); } [Test] diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs index 0f318f2..faf7a5c 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs @@ -24,6 +24,10 @@ internal ChatEntity(IntPtr pointer) this.pointer = pointer; } + internal ChatEntity() + { + } + internal void UpdatePointer(IntPtr newPointer) { DisposePointer(); @@ -41,6 +45,10 @@ public virtual async void SetListeningForUpdates(bool listen) updateListeningHandle = await SetListening(updateListeningHandle, listen, StreamUpdates); } + public virtual async Task Resync() + { + } + internal async Task SetListening(IntPtr callbackHandle, bool listen, Func streamFunction) { if (listen) diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/UniqueChatEntity.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/UniqueChatEntity.cs index 193407a..a51bf66 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/UniqueChatEntity.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/UniqueChatEntity.cs @@ -6,6 +6,11 @@ public abstract class UniqueChatEntity : ChatEntity { public string Id { get; protected set; } + internal UniqueChatEntity(string uniqueId) + { + Id = uniqueId; + } + internal UniqueChatEntity(IntPtr pointer, string uniqueId) : base(pointer) { Id = uniqueId; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 25059c0..cc6bc29 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -16,6 +16,7 @@ namespace PubNubChatAPI.Entities { //TODO: make IDisposable? + //TODO: global remove CCoreException from inline docs /// /// Main class for the chat. /// @@ -505,7 +506,7 @@ internal void ParseJsonUpdatePointers(string jsonString) break; case PubnubChatEventType.Mention: - if (TryGetUser(chatEvent.UserId, out var mentionedUser)) + if (OLD_TryGetUser(chatEvent.UserId, out var mentionedUser)) { mentionedUser.BroadcastMentionEvent(chatEvent); invoked = true; @@ -513,7 +514,7 @@ internal void ParseJsonUpdatePointers(string jsonString) break; case PubnubChatEventType.Invite: - if (TryGetUser(chatEvent.UserId, out var invitedUser)) + if (OLD_TryGetUser(chatEvent.UserId, out var invitedUser)) { invitedUser.BroadcastInviteEvent(chatEvent); invoked = true; @@ -529,7 +530,7 @@ internal void ParseJsonUpdatePointers(string jsonString) break; case PubnubChatEventType.Moderation: - if (TryGetUser(chatEvent.UserId, out var moderatedUser)) + if (OLD_TryGetUser(chatEvent.UserId, out var moderatedUser)) { moderatedUser.BroadcastModerationEvent(chatEvent); invoked = true; @@ -1080,7 +1081,7 @@ public async Task GetCurrentUserMentions(string startTimeTo return new UserMentionsWrapper(this, internalWrapper); } - + /// /// Tries to retrieve the current User object for this chat. /// @@ -1091,7 +1092,7 @@ public bool TryGetCurrentUser(out User user) { var userPointer = pn_chat_current_user(chatPointer); CUtilities.CheckCFunctionResult(userPointer); - return TryGetUser(userPointer, out user); + return OLD_TryGetUser(userPointer, out user); } /// @@ -1102,7 +1103,23 @@ public bool TryGetCurrentUser(out User user) { return await Task.Run(() => { - var result = TryGetCurrentUser(out var currentUser); + var result = OLD_TryGetCurrentUser(out var currentUser); + return result ? currentUser : null; + }); + } + + public bool OLD_TryGetCurrentUser(out User user) + { + var userPointer = pn_chat_current_user(chatPointer); + CUtilities.CheckCFunctionResult(userPointer); + return OLD_TryGetUser(userPointer, out user); + } + + public async Task OLD_GetCurrentUserAsync() + { + return await Task.Run(() => + { + var result = OLD_TryGetCurrentUser(out var currentUser); return result ? currentUser : null; }); } @@ -1141,13 +1158,40 @@ public void AddListenerToUsersUpdate(List userIds, Action listener { foreach (var userId in userIds) { - if (TryGetUser(userId, out var user)) + if (OLD_TryGetUser(userId, out var user)) { user.OnUserUpdated += listener; } } } + + public async Task OLD_CreateUser(string userId) + { + return await OLD_CreateUser(userId, new ChatUserData()); + } + + public async Task OLD_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; + } + /// /// Creates a new user with the provided user ID. /// @@ -1197,16 +1241,8 @@ public async Task CreateUser(string userId, ChatUserData additionalData) 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); + var user = new User(this, userId, additionalData); + await User.UpdateUserData(this, userId, additionalData); userWrappers.Add(userId, user); return user; } @@ -1297,7 +1333,7 @@ public async Task> WhoIsPresent(string channelId) /// public async Task> WherePresent(string userId) { - if (TryGetUser(userId, out var user)) + if (OLD_TryGetUser(userId, out var user)) { return await user.WherePresent(); } @@ -1326,11 +1362,11 @@ public async Task> WherePresent(string userId) /// /// /// - /// + /// public bool TryGetUser(string userId, out User user) { - var userPointer = pn_chat_get_user(chatPointer, userId); - return TryGetUser(userId, userPointer, out user); + user = GetUserAsync(userId).Result; + return user != null; } /// @@ -1339,21 +1375,50 @@ public bool TryGetUser(string userId, out User user) /// ID of the User to get. /// User object if one with given ID is found, null otherwise. public async Task GetUserAsync(string userId) + { + if (userWrappers.TryGetValue(userId, out var existingUser)) + { + await existingUser.Resync(); + return existingUser; + } + else + { + var data = await User.GetUserData(this, userId); + if (data == null) + { + return null; + } + else + { + var user = new User(this, userId, data); + userWrappers.Add(userId, user); + return user; + } + } + } + + public bool OLD_TryGetUser(string userId, out User user) + { + var userPointer = pn_chat_get_user(chatPointer, userId); + return OLD_TryGetUser(userId, userPointer, out user); + } + + public async Task OLD_GetUserAsync(string userId) { return await Task.Run(() => { - var result = TryGetUser(userId, out var user); + var result = OLD_TryGetUser(userId, out var user); return result ? user : null; }); } - internal bool TryGetUser(IntPtr userPointer, out User user) + internal bool OLD_TryGetUser(IntPtr userPointer, out User user) { var id = User.GetUserIdFromPtr(userPointer); - return TryGetUser(id, userPointer, out user); + return OLD_TryGetUser(id, userPointer, out user); } - internal bool TryGetUser(string userId, IntPtr userPointer, out User user) + internal bool OLD_TryGetUser(string userId, IntPtr userPointer, out User user) { return TryGetWrapper(userWrappers, userId, userPointer, () => new User(this, userId, userPointer), out user); @@ -1398,7 +1463,29 @@ public async Task GetUsers(string filter = "", string sort var internalWrapper = JsonConvert.DeserializeObject(internalWrapperJson); return new UsersResponseWrapper(this, internalWrapper); } - + + public async Task OLD_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)); + } + } + /// /// Updates the user with the provided user ID. /// @@ -1420,23 +1507,13 @@ public async Task GetUsers(string filter = "", string sort /// 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); + await existingUserWrapper.Update(updatedData); } - //TODO: could and should this ever actually happen? else { - userWrappers.Add(userId, new User(this, userId, newPointer)); + await User.UpdateUserData(this, userId, updatedData); } } @@ -1499,7 +1576,7 @@ public async Task GetUserMemberships(string userId, stri string sort = "", int limit = 0, Page page = null) { - if (!TryGetUser(userId, out var user)) + if (!OLD_TryGetUser(userId, out var user)) { return new MembersResponseWrapper(); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index e82b824..4ab390a 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; +using PubnubApi; using PubnubChatApi.Entities.Data; using PubnubChatApi.Entities.Events; using PubnubChatApi.Enums; @@ -78,14 +79,8 @@ private static extern int pn_user_get_channels_restrictions(IntPtr user, string private static extern IntPtr pn_user_stream_updates(IntPtr user); #endregion - - /// - /// The user's user name. - /// - /// This might be user's display name in the chat. - /// - /// - public string UserName + + public string OLD_UserName { get { @@ -94,14 +89,8 @@ public string UserName return buffer.ToString(); } } - - /// - /// The user's external id. - /// - /// This might be user's id in the external system (e.g. Database, CRM, etc.) - /// - /// - public string ExternalId + + public string OLD_ExternalId { get { @@ -111,13 +100,7 @@ public string ExternalId } } - /// - /// The user's profile url. - /// - /// This might be user's profile url to download the profile picture. - /// - /// - public string ProfileUrl + public string OLD_ProfileUrl { get { @@ -127,13 +110,7 @@ public string ProfileUrl } } - /// - /// The user's email. - /// - /// This should be user's email address. - /// - /// - public string Email + public string OLD_Email { get { @@ -142,14 +119,8 @@ public string Email return buffer.ToString(); } } - - /// - /// The user's custom data. - /// - /// This might be any custom data that you want to store for the user. - /// - /// - public string CustomData + + public string OLD_CustomData { get { @@ -158,14 +129,8 @@ public string CustomData return buffer.ToString(); } } - - /// - /// The user's status. - /// - /// This is a string that represents the user's status. - /// - /// - public string Status + + public string OLD_Status { get { @@ -174,14 +139,8 @@ public string Status return buffer.ToString(); } } - - /// - /// The user's data type. - /// - /// This is a string that represents the user's data type. - /// - /// - public string DataType + + public string OLD_DataType { get { @@ -191,6 +150,64 @@ public string DataType } } + 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 string CustomData => userData.CustomDataJson; + + /// + /// 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 @@ -232,7 +249,7 @@ public string LastActiveTimeStamp /// }; /// /// - /// + /// /// public event Action OnUserUpdated; @@ -240,10 +257,17 @@ public string LastActiveTimeStamp public event Action OnInviteEvent; public event Action OnModerationEvent; + //TODO: REMOVE internal User(Chat chat, string userId, IntPtr userPointer) : base(userPointer, userId) { this.chat = chat; } + + internal User(Chat chat, string userId, ChatUserData chatUserData) : base(userId) + { + userData = chatUserData; + this.chat = chat; + } public async void SetListeningForMentionEvents(bool listen) { @@ -301,7 +325,12 @@ internal void BroadcastUserUpdate() { OnUserUpdated?.Invoke(this); } - + + public async Task OLD_Update(ChatUserData updatedData) + { + await chat.OLD_UpdateUser(Id, updatedData); + } + /// /// Updates the user. /// @@ -324,7 +353,59 @@ internal void BroadcastUserUpdate() /// public async Task Update(ChatUserData updatedData) { - await chat.UpdateUser(Id, updatedData); + userData = updatedData; + await UpdateUserData(chat, Id, updatedData); + } + + internal static async Task UpdateUserData(Chat chat, string userId, ChatUserData chatUserData) + { + await chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true) + .Uuid(userId) + .Name(chatUserData.Username) + .Email(chatUserData.Email) + .ExternalId(chatUserData.ExternalId) + .Custom(new Dictionary() + { + { "ProfileUrl", chatUserData.ProfileUrl }, + { "Status", chatUserData.Status}, + { "Type", chatUserData.Type}, + { "CustomDataJson", chatUserData.CustomDataJson} + }) + .ExecuteAsync(); + } + + internal static async Task GetUserData(Chat chat, string userId) + { + var result = await chat.PubnubInstance.GetUuidMetadata().Uuid(userId).IncludeCustom(true).ExecuteAsync(); + if (result.Status.Error) + { + chat.PubnubInstance.PNConfig.Logger.Error($"Error when trying to Resync() User \"{userId}\": {result.Status.ErrorData.Information}"); + return null; + } + try + { + return new ChatUserData() + { + Username = result.Result.Name, + Email = result.Result.Email, + ExternalId = result.Result.ExternalId, + CustomDataJson = result.Result.Custom["CustomDataJson"].ToString(), + ProfileUrl = result.Result.Custom["ProfileUrl"].ToString(), + Status = result.Result.Custom["Status"].ToString(), + Type = result.Result.Custom["DataType"].ToString(), + }; + } + catch (Exception e) + { + chat.PubnubInstance.PNConfig.Logger.Error($"Error when trying to parse data for User \"{userId}\": {e.Message}"); + return null; + } + } + + public override async Task Resync() + { + var newData = await GetUserData(chat, Id); + userData = newData; } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs index 8cb92e9..8b6bef5 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs @@ -92,7 +92,7 @@ internal static List ParseJsonUserPointers(Chat chat, IntPtr[] userPointer foreach (var userPointer in userPointers) { var id = User.GetUserIdFromPtr(userPointer); - if (chat.TryGetUser(id, userPointer, out var user)) + if (chat.OLD_TryGetUser(id, userPointer, out var user)) { users.Add(user); } From 83dbc0a92b0b25f6929c3fe4a354f46b7552ea5e Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Thu, 10 Jul 2025 14:38:05 +0200 Subject: [PATCH 03/28] more WIP on User migration --- .../PubNubChatApi.Tests/ChatTests.cs | 2 +- .../PubnubTestsParameters.cs | 4 +- .../PubnubChatApi/Entities/Chat.cs | 48 +++++++++++++++---- .../Entities/Data/ChatUserData.cs | 21 ++++++++ .../Entities/Data/UsersResponseWrapper.cs | 11 ++++- .../PubnubChatApi/Entities/User.cs | 24 ++++------ 6 files changed, 82 insertions(+), 28 deletions(-) diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs index b46cce9..66183f8 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -76,7 +76,7 @@ public async Task TestGetEventHistory() [Test] public async Task TestGetUsers() { - var users = await chat.GetUsers(); + var users = await chat.OLD_GetUsers(); Assert.True(users.Users.Any()); } diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubnubTestsParameters.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubnubTestsParameters.cs index c8680ba..5fde5cd 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-b23194b7-f485-43dc-9544-115997a7d9a9" : EnvPublishKey; + public static readonly string SubscribeKey = string.IsNullOrEmpty(EnvSubscribeKey) ? "sub-c-eaa73d83-9ed7-4e49-8b5a-1b1d64160735" : 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/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index cc6bc29..3db96d7 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -1090,9 +1090,8 @@ public async Task GetCurrentUserMentions(string startTimeTo /// public bool TryGetCurrentUser(out User user) { - var userPointer = pn_chat_current_user(chatPointer); - CUtilities.CheckCFunctionResult(userPointer); - return OLD_TryGetUser(userPointer, out user); + user = GetCurrentUserAsync().Result; + return user != null; } /// @@ -1101,11 +1100,8 @@ public bool TryGetCurrentUser(out User user) /// User object if there is a current user, null otherwise. public async Task GetCurrentUserAsync() { - return await Task.Run(() => - { - var result = OLD_TryGetCurrentUser(out var currentUser); - return result ? currentUser : null; - }); + var userId = PubnubInstance.GetCurrentUserId(); + return await GetUserAsync(userId); } public bool OLD_TryGetCurrentUser(out User user) @@ -1423,7 +1419,7 @@ internal bool OLD_TryGetUser(string userId, IntPtr userPointer, out User user) return TryGetWrapper(userWrappers, userId, userPointer, () => new User(this, userId, userPointer), out user); } - + /// /// Gets the list of users with the provided parameters. /// @@ -1452,6 +1448,40 @@ internal bool OLD_TryGetUser(string userId, IntPtr userPointer, out User user) /// /// public async Task GetUsers(string filter = "", string sort = "", int limit = 0, + PNPageObject page = null) + { + var result = await PubnubInstance.GetAllUuidMetadata().Filter(filter).Sort(new List() { sort }) + .Limit(limit).Page(page).ExecuteAsync(); + if (result.Status.Error) + { + PubnubInstance.PNConfig.Logger?.Error($"Error when trying to GetUsers(): {result.Status.ErrorData.Information}"); + return default; + } + + var response = new UsersResponseWrapper() + { + Users = new List(), + Total = result.Result.TotalCount, + Page = result.Result.Page + }; + foreach (var resultMetadata in result.Result.Uuids) + { + if (userWrappers.TryGetValue(resultMetadata.Uuid, out var existingUserWrapper)) + { + existingUserWrapper.UpdateLocalData(resultMetadata); + response.Users.Add(existingUserWrapper); + } + else + { + var user = new User(this, resultMetadata.Uuid, resultMetadata); + userWrappers.Add(user.Id, user); + response.Users.Add(user); + } + } + return response; + } + + public async Task OLD_GetUsers(string filter = "", string sort = "", int limit = 0, Page page = null) { var buffer = new StringBuilder(8192); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs index 6d88df1..b9803a5 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs @@ -1,3 +1,5 @@ +using PubnubApi; + namespace PubnubChatApi.Entities.Data { /// @@ -18,5 +20,24 @@ public class ChatUserData public string CustomDataJson { get; set; } = string.Empty; 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.Custom.TryGetValue("Status", out var status) ? status.ToString() : string.Empty, + Type = metadataResult.Custom.TryGetValue("DataType", out var dataType) + ? dataType.ToString() + : string.Empty, + CustomDataJson = metadataResult.Custom.TryGetValue("CustomDataJson", out var custom) + ? custom.ToString() + : string.Empty, + }; + } + } } \ 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..12dd604 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Utilities; @@ -8,17 +9,23 @@ namespace PubnubChatApi.Entities.Data public struct UsersResponseWrapper { public List Users; - public Page Page; + public PNPageObject Page; public int Total; + //TODO: REMOVE internal UsersResponseWrapper(Chat chat, InternalUsersResponseWrapper internalWrapper) { - Page = internalWrapper.Page; + Page = new PNPageObject() + { + Next = internalWrapper.Page.Next, + Prev = internalWrapper.Page.Previous + }; Total = internalWrapper.Total; Users = PointerParsers.ParseJsonUserPointers(chat, internalWrapper.Users); } } + //TODO: REMOVE internal struct InternalUsersResponseWrapper { public IntPtr[] Users; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index 4ab390a..e5b9f9f 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -265,7 +265,7 @@ internal User(Chat chat, string userId, IntPtr userPointer) : base(userPointer, internal User(Chat chat, string userId, ChatUserData chatUserData) : base(userId) { - userData = chatUserData; + UpdateLocalData(chatUserData); this.chat = chat; } @@ -353,7 +353,7 @@ public async Task OLD_Update(ChatUserData updatedData) /// public async Task Update(ChatUserData updatedData) { - userData = updatedData; + UpdateLocalData(updatedData); await UpdateUserData(chat, Id, updatedData); } @@ -364,9 +364,9 @@ await chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true) .Name(chatUserData.Username) .Email(chatUserData.Email) .ExternalId(chatUserData.ExternalId) + .ProfileUrl(chatUserData.ProfileUrl) .Custom(new Dictionary() { - { "ProfileUrl", chatUserData.ProfileUrl }, { "Status", chatUserData.Status}, { "Type", chatUserData.Type}, { "CustomDataJson", chatUserData.CustomDataJson} @@ -384,16 +384,7 @@ await chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true) } try { - return new ChatUserData() - { - Username = result.Result.Name, - Email = result.Result.Email, - ExternalId = result.Result.ExternalId, - CustomDataJson = result.Result.Custom["CustomDataJson"].ToString(), - ProfileUrl = result.Result.Custom["ProfileUrl"].ToString(), - Status = result.Result.Custom["Status"].ToString(), - Type = result.Result.Custom["DataType"].ToString(), - }; + return (ChatUserData)result.Result; } catch (Exception e) { @@ -401,11 +392,16 @@ await chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true) return null; } } + + internal void UpdateLocalData(ChatUserData newData) + { + userData = newData; + } public override async Task Resync() { var newData = await GetUserData(chat, Id); - userData = newData; + UpdateLocalData(newData); } /// From 12c19b52d77dd962820a63e8a95abebf68d86eff Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Fri, 11 Jul 2025 15:38:20 +0200 Subject: [PATCH 04/28] add public channel creation and getting, fix wrong json keys in custom user data --- .../PubNubChatApi.Tests/ChannelTests.cs | 42 ++-- .../PubNubChatApi.Tests/ChatEventTests.cs | 2 +- .../PubNubChatApi.Tests/ChatTests.cs | 10 +- .../PubNubChatApi.Tests/MembershipTests.cs | 10 +- .../PubNubChatApi.Tests/MessageDraftTests.cs | 6 +- .../PubNubChatApi.Tests/MessageTests.cs | 6 +- .../PubnubTestsParameters.cs | 4 +- .../PubNubChatApi.Tests/RestrictionsTests.cs | 4 +- .../PubNubChatApi.Tests/ThreadsTests.cs | 2 +- .../PubNubChatApi.Tests/UserTests.cs | 6 +- .../PubnubChatApi/Entities/Channel.cs | 180 +++++++++++++----- .../PubnubChatApi/Entities/Chat.cs | 167 +++++++++++----- .../Entities/Data/ChatChannelData.cs | 19 ++ .../Entities/Data/ChatUserData.cs | 6 +- .../PubnubChatApi/Entities/Message.cs | 2 +- .../PubnubChatApi/Entities/ThreadMessage.cs | 2 +- .../PubnubChatApi/Entities/User.cs | 22 ++- .../PubnubChatApi/Utilities/PointerParsers.cs | 2 +- 18 files changed, 340 insertions(+), 152 deletions(-) diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs index fbfb328..d3a5fe6 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs @@ -40,7 +40,7 @@ public async Task CleanUp() [Test] public async Task TestUpdateChannel() { - var channel = await chat.CreatePublicConversation(); + var channel = await chat.OLD_CreatePublicConversation(); channel.SetListeningForUpdates(true); await Task.Delay(3000); @@ -56,11 +56,11 @@ public async Task TestUpdateChannel() }; 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.OLD_Description == updatedData.ChannelDescription, "updatedChannel.Description != updatedData.ChannelDescription"); + Assert.True(updatedChannel.OLD_CustomDataJson == updatedData.ChannelCustomDataJson, "updatedChannel.CustomDataJson != updatedData.ChannelCustomDataJson"); + Assert.True(updatedChannel.OLD_Name == updatedData.ChannelName, "updatedChannel.Name != updatedData.ChannelDescription"); + Assert.True(updatedChannel.OLD_Status == updatedData.ChannelStatus, "updatedChannel.Status != updatedData.ChannelStatus"); + Assert.True(updatedChannel.OLD_Type == updatedData.ChannelType, "updatedChannel.Type != updatedData.ChannelType"); updateReset.Set(); }; await channel.Update(updatedData); @@ -71,17 +71,17 @@ public async Task TestUpdateChannel() [Test] public async Task TestDeleteChannel() { - var channel = await chat.CreatePublicConversation(); + var channel = await chat.OLD_CreatePublicConversation(); await Task.Delay(3000); - Assert.True(chat.TryGetChannel(channel.Id, out _), "Couldn't fetch created channel from chat"); + Assert.True(chat.OLD_TryGetChannel(channel.Id, out _), "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"); + Assert.False(chat.OLD_TryGetChannel(channel.Id, out _), "Fetched the supposedly-deleted channel from chat"); } [Test] @@ -91,7 +91,7 @@ public async Task TestLeaveChannel() Assert.IsNotNull(currentChatUser, "currentChatUser was null"); - var channel = await chat.CreatePublicConversation(); + var channel = await chat.OLD_CreatePublicConversation(); channel.Join(); await Task.Delay(3000); @@ -112,7 +112,7 @@ public async Task TestLeaveChannel() [Test] public async Task TestGetUserSuggestions() { - var channel = await chat.CreatePublicConversation("user_suggestions_test_channel"); + var channel = await chat.OLD_CreatePublicConversation("user_suggestions_test_channel"); channel.Join(); await Task.Delay(5000); @@ -124,7 +124,7 @@ public async Task TestGetUserSuggestions() [Test] public async Task TestGetMemberships() { - var channel = await chat.CreatePublicConversation("get_members_test_channel"); + var channel = await chat.OLD_CreatePublicConversation("get_members_test_channel"); channel.Join(); await Task.Delay(3500); var memberships = await channel.GetMemberships(); @@ -134,7 +134,7 @@ public async Task TestGetMemberships() [Test] public async Task TestStartTyping() { - var channel = (await chat.CreateDirectConversation(talkUser, "sttc")).CreatedChannel; + var channel = (await chat.OLD_CreateDirectConversation(talkUser, "sttc")).CreatedChannel; channel.Join(); await Task.Delay(2500); channel.SetListeningForTyping(true); @@ -156,7 +156,7 @@ public async Task TestStartTyping() [Test] public async Task TestStopTyping() { - var channel = (await chat.CreateDirectConversation(talkUser, "stop_typing_test_channel")).CreatedChannel; + var channel = (await chat.OLD_CreateDirectConversation(talkUser, "stop_typing_test_channel")).CreatedChannel; channel.Join(); await Task.Delay(2500); channel.SetListeningForTyping(true); @@ -181,7 +181,7 @@ public async Task TestStopTyping() [Test] public async Task TestStopTypingFromTimer() { - var channel = (await chat.CreateDirectConversation(talkUser, "stop_typing_timeout_test_channel")).CreatedChannel; + var channel = (await chat.OLD_CreateDirectConversation(talkUser, "stop_typing_timeout_test_channel")).CreatedChannel; channel.Join(); await Task.Delay(2500); channel.SetListeningForTyping(true); @@ -206,7 +206,7 @@ public async Task TestStopTypingFromTimer() [Test] public async Task TestPinMessage() { - var channel = await chat.CreatePublicConversation("pin_message_test_channel_37"); + var channel = await chat.OLD_CreatePublicConversation("pin_message_test_channel_37"); channel.Join(); await Task.Delay(3500); @@ -233,7 +233,7 @@ public async Task TestPinMessage() [Test] public async Task TestUnPinMessage() { - var channel = await chat.CreatePublicConversation("unpin_message_test_channel"); + var channel = await chat.OLD_CreatePublicConversation("unpin_message_test_channel"); channel.Join(); await Task.Delay(3500); var receivedManualEvent = new ManualResetEvent(false); @@ -260,7 +260,7 @@ public async Task TestUnPinMessage() [Test] public async Task TestCreateMessageDraft() { - var channel = await chat.CreatePublicConversation("message_draft_test_channel"); + var channel = await chat.OLD_CreatePublicConversation("message_draft_test_channel"); try { var draft = channel.CreateMessageDraft(); @@ -275,7 +275,7 @@ public async Task TestCreateMessageDraft() [Test] public async Task TestEmitUserMention() { - var channel = await chat.CreatePublicConversation("user_mention_test_channel"); + var channel = await chat.OLD_CreatePublicConversation("user_mention_test_channel"); channel.Join(); await Task.Delay(2500); var receivedManualEvent = new ManualResetEvent(false); @@ -294,7 +294,7 @@ public async Task TestEmitUserMention() [Test] public async Task TestChannelIsPresent() { - var someChannel = await chat.CreatePublicConversation(); + var someChannel = await chat.OLD_CreatePublicConversation(); someChannel.Join(); await Task.Delay(4000); @@ -307,7 +307,7 @@ public async Task TestChannelIsPresent() [Test] public async Task TestChannelWhoIsPresent() { - var someChannel = await chat.CreatePublicConversation(); + var someChannel = await chat.OLD_CreatePublicConversation(); someChannel.Join(); await Task.Delay(4000); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs index f1707c4..80cc7af 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs @@ -19,7 +19,7 @@ public async Task Setup() PubnubTestsParameters.SubscribeKey, "event_tests_user") ); - channel = await chat.CreatePublicConversation("event_tests_channel"); + channel = await chat.OLD_CreatePublicConversation("event_tests_channel"); if (!chat.OLD_TryGetCurrentUser(out user)) { Assert.Fail(); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs index 66183f8..afcd136 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -18,7 +18,7 @@ public async Task Setup() PubnubTestsParameters.PublishKey, PubnubTestsParameters.SubscribeKey, "chats_tests_user_10_no_calkiem_nowy_2")); - channel = await chat.CreatePublicConversation("chat_tests_channel_2"); + channel = await chat.OLD_CreatePublicConversation("chat_tests_channel_2"); if (!chat.OLD_TryGetCurrentUser(out currentUser)) { Assert.Fail(); @@ -93,7 +93,7 @@ public async Task TestCreateDirectConversation() { var convoUser = await chat.GetOrCreateUser("direct_conversation_user"); var directConversation = - await chat.CreateDirectConversation(convoUser, "direct_conversation_test"); + await chat.OLD_CreateDirectConversation(convoUser, "direct_conversation_test"); Assert.True(directConversation.CreatedChannel is { Id: "direct_conversation_test" }); Assert.True(directConversation.HostMembership != null && directConversation.HostMembership.UserId == currentUser.Id); Assert.True(directConversation.InviteesMemberships != null && @@ -107,7 +107,7 @@ public async Task TestCreateGroupConversation() 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"); + chat.OLD_CreateGroupConversation([convoUser1, convoUser2, convoUser3], "group_conversation_test"); Assert.True(groupConversation.CreatedChannel is { Id: "group_conversation_test" }); Assert.True(groupConversation.HostMembership != null && groupConversation.HostMembership.UserId == currentUser.Id); Assert.True(groupConversation.InviteesMemberships is { Count: 3 }); @@ -120,7 +120,7 @@ public async Task TestForwardMessage() { var messageForwardReceivedManualEvent = new ManualResetEvent(false); - var forwardingChannel = await chat.CreatePublicConversation("forwarding_channel"); + var forwardingChannel = await chat.OLD_CreatePublicConversation("forwarding_channel"); forwardingChannel.OnMessageReceived += message => { Assert.True(message.MessageText == "message_to_forward"); @@ -190,7 +190,7 @@ public async Task TestReadReceipts() PubnubTestsParameters.SubscribeKey, "other_chat_user") ); - if (!otherChat.TryGetChannel(channel.Id, out var otherChatChannel)) + if (!otherChat.OLD_TryGetChannel(channel.Id, out var otherChatChannel)) { Assert.Fail(); return; diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs index e06bef2..11b4c37 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs @@ -19,7 +19,7 @@ public async Task Setup() PubnubTestsParameters.SubscribeKey, "membership_tests_user_54") ); - channel = await chat.CreatePublicConversation("membership_tests_channel"); + channel = await chat.OLD_CreatePublicConversation("membership_tests_channel"); if (!chat.OLD_TryGetCurrentUser(out user)) { Assert.Fail(); @@ -81,7 +81,7 @@ public async Task TestUpdateMemberships() [Test] public async Task TestInvite() { - var testChannel = (await chat.CreateGroupConversation([user], "test_invite_group_channel")).CreatedChannel; + var testChannel = (await chat.OLD_CreateGroupConversation([user], "test_invite_group_channel")).CreatedChannel; var testUser = await chat.GetOrCreateUser("test_invite_user"); var returnedMembership = await testChannel.Invite(testUser); Assert.True(returnedMembership.ChannelId == testChannel.Id && returnedMembership.UserId == testUser.Id); @@ -90,7 +90,7 @@ public async Task TestInvite() [Test] public async Task TestInviteMultiple() { - var testChannel = (await chat.CreateGroupConversation([user], "invite_multiple_test_group_channel_3")) + var testChannel = (await chat.OLD_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"); @@ -107,7 +107,7 @@ public async Task TestInviteMultiple() [Test] public async Task TestLastRead() { - var testChannel = await chat.CreatePublicConversation("last_read_test_channel_57"); + var testChannel = await chat.OLD_CreatePublicConversation("last_read_test_channel_57"); testChannel.Join(); await Task.Delay(4000); @@ -146,7 +146,7 @@ public async Task TestLastRead() [Test] public async Task TestUnreadMessagesCount() { - var unreadChannel = await chat.CreatePublicConversation($"test_channel_{Guid.NewGuid()}"); + var unreadChannel = await chat.OLD_CreatePublicConversation($"test_channel_{Guid.NewGuid()}"); unreadChannel.Join(); await Task.Delay(3500); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs index d9bdb31..1b921b0 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs @@ -19,7 +19,7 @@ public async Task Setup() PubnubTestsParameters.SubscribeKey, "message_draft_tests_user") ); - channel = await chat.CreatePublicConversation("message_draft_tests_channel", new ChatChannelData() + channel = await chat.OLD_CreatePublicConversation("message_draft_tests_channel", new ChatChannelData() { ChannelName = "MessageDraftTestingChannel" }); @@ -39,9 +39,9 @@ public async Task Setup() }); } - if (!chat.TryGetChannel("dummy_channel", out dummyChannel)) + if (!chat.OLD_TryGetChannel("dummy_channel", out dummyChannel)) { - dummyChannel = await chat.CreatePublicConversation("dummy_channel"); + dummyChannel = await chat.OLD_CreatePublicConversation("dummy_channel"); } } diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs index 3636f7d..1df5548 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs @@ -19,7 +19,7 @@ public async Task Setup() PubnubTestsParameters.SubscribeKey, "message_tests_user_2") ); - channel = await chat.CreatePublicConversation("message_tests_channel_2"); + channel = await chat.OLD_CreatePublicConversation("message_tests_channel_2"); if (!chat.OLD_TryGetCurrentUser(out user)) { Assert.Fail(); @@ -60,7 +60,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 = await chat.OLD_CreatePublicConversation("message_data_test_channel"); testChannel.Join(); await Task.Delay(2500); testChannel.OnMessageReceived += async message => @@ -200,7 +200,7 @@ public async Task TestRestoreMessage() [Test] public async Task TestPinMessage() { - var pinTestChannel = await chat.CreatePublicConversation(); + var pinTestChannel = await chat.OLD_CreatePublicConversation(); pinTestChannel.Join(); await Task.Delay(2500); pinTestChannel.SetListeningForUpdates(true); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubnubTestsParameters.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubnubTestsParameters.cs index 5fde5cd..c8680ba 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) ? "pub-c-b23194b7-f485-43dc-9544-115997a7d9a9" : EnvPublishKey; - public static readonly string SubscribeKey = string.IsNullOrEmpty(EnvSubscribeKey) ? "sub-c-eaa73d83-9ed7-4e49-8b5a-1b1d64160735" : EnvSubscribeKey; + 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 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..e8502df 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs @@ -30,7 +30,7 @@ public async Task CleanUp() public async Task TestSetRestrictions() { var user = await chat.GetOrCreateUser("user123"); - var channel = await chat.CreatePublicConversation("new_channel"); + var channel = await chat.OLD_CreatePublicConversation("new_channel"); await Task.Delay(2000); @@ -59,7 +59,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 = await chat.OLD_CreatePublicConversation("new_channel_2"); await Task.Delay(4000); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs index 888bdc4..7ab1409 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs @@ -20,7 +20,7 @@ public async Task Setup() "threads_tests_user_2") ); var randomId = Guid.NewGuid().ToString()[..10]; - channel = await chat.CreatePublicConversation(randomId); + channel = await chat.OLD_CreatePublicConversation(randomId); if (!chat.OLD_TryGetCurrentUser(out user)) { Assert.Fail(); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs index 5794402..4c99c88 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs @@ -20,7 +20,7 @@ public async Task Setup() "user_tests_user", storeUserActivityTimestamp: true) ); - channel = await chat.CreatePublicConversation("user_tests_channel"); + channel = await chat.OLD_CreatePublicConversation("user_tests_channel"); if (!chat.OLD_TryGetCurrentUser(out user)) { Assert.Fail(); @@ -108,7 +108,7 @@ public async Task TestUserDelete() [Test] public async Task TestUserWherePresent() { - var someChannel = await chat.CreatePublicConversation(); + var someChannel = await chat.OLD_CreatePublicConversation(); someChannel.Join(); await Task.Delay(4000); @@ -121,7 +121,7 @@ public async Task TestUserWherePresent() [Test] public async Task TestUserIsPresentOn() { - var someChannel = await chat.CreatePublicConversation(); + var someChannel = await chat.OLD_CreatePublicConversation(); someChannel.Join(); await Task.Delay(4000); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index b06921a..cfc958a 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -171,16 +171,8 @@ string membership_status ); #endregion - - /// - /// The name of the channel. - /// - /// - /// The name of the channel that is human meaningful. - /// - /// - /// The name of the channel. - public string Name + + public string OLD_Name { get { @@ -190,13 +182,7 @@ public string Name } } - /// - /// The description of the channel. - /// - /// - /// The description that allows users to understand the purpose of the channel. - /// - public string Description + public string OLD_Description { get { @@ -206,17 +192,7 @@ public string Description } } - /// - /// 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. - /// - public string CustomDataJson + public string OLD_CustomDataJson { get { @@ -225,14 +201,8 @@ public string CustomDataJson return buffer.ToString(); } } - - /// - /// The information about the last update of the channel. - /// - /// The time when the channel was last updated. - /// - /// - public string Updated + + public string OLD_Updated { get { @@ -242,13 +212,7 @@ public string Updated } } - /// - /// The status of the channel. - /// - /// The last status response received from the server. - /// - /// - public string Status + public string OLD_Status { get { @@ -257,14 +221,8 @@ public string Status return buffer.ToString(); } } - - /// - /// The type of the channel. - /// - /// The type of the response received from the server when the channel was created. - /// - /// - public string Type + + public string OLD_Type { get { @@ -274,6 +232,62 @@ public string Type } } + /// + /// The name of the channel. + /// + /// + /// The name of the channel that is human meaningful. + /// + /// + /// The name of the channel. + public string Name => channelData.ChannelName; + + /// + /// The description of the channel. + /// + /// + /// The description that allows users to understand the purpose of the channel. + /// + public string Description => channelData.ChannelDescription; + + /// + /// 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. + /// + public string CustomDataJson => channelData.ChannelCustomDataJson; + + /// + /// The information about the last update of the channel. + /// + /// The time when the channel was last updated. + /// + /// + public string Updated => channelData.ChannelUpdated; + + /// + /// The status of the channel. + /// + /// The last status response received from the server. + /// + /// + public string Status => channelData.ChannelStatus; + + /// + /// The type of the channel. + /// + /// The type of the response received from the server when the channel was created. + /// + /// + public string Type => channelData.ChannelType; + + private ChatChannelData channelData; + protected Chat chat; private IntPtr customEventsListeningHandle; private IntPtr reportEventsListeningHandle; @@ -354,6 +368,70 @@ internal Channel(Chat chat, string channelId, IntPtr channelPointer) : base(chan this.chat = chat; } + internal Channel(Chat chat, string channelId, ChatChannelData data) : base(channelId) + { + this.chat = chat; + UpdateLocalData(data); + } + + internal void UpdateLocalData(ChatChannelData? newData) + { + if (newData == null) + { + return; + } + channelData = newData; + } + + internal static async Task UpdateChannelData(Chat chat, string channelId, ChatChannelData data) + { + //chat.PubnubInstance.setmem + var result = await chat.PubnubInstance.SetChannelMetadata().IncludeCustom(true) + .Channel(channelId) + .Name(data.ChannelName) + .Description(data.ChannelDescription) + .Custom(new Dictionary() + { + {"custom", data.ChannelCustomDataJson}, + {"updated", data.ChannelUpdated}, + {"status", data.ChannelStatus}, + }) + .ExecuteAsync(); + if (result.Status.Error) + { + chat.PubnubInstance.PNConfig.Logger?.Error($"Error when trying to set data for channel \"{channelId}\": {result.Status.ErrorData.Information}"); + return false; + } + return true; + } + + internal static async Task GetChannelData(Chat chat, string channelId) + { + var result = await chat.PubnubInstance.GetChannelMetadata().IncludeCustom(true) + .Channel(channelId) + .ExecuteAsync(); + if (result.Status.Error) + { + chat.PubnubInstance.PNConfig.Logger?.Error($"Error when trying to get data for channel \"{channelId}\": {result.Status.ErrorData.Information}"); + return null; + } + try + { + return (ChatChannelData)result.Result; + } + catch (Exception e) + { + chat.PubnubInstance.PNConfig.Logger.Error($"Error when trying to parse data for Channel \"{channelId}\": {e.Message}"); + return null; + } + } + + public override async Task Resync() + { + var newData = await GetChannelData(chat, Id); + UpdateLocalData(newData); + } + protected override IntPtr StreamUpdates() { return pn_channel_stream_updates(pointer); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 3db96d7..71d516c 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -439,7 +439,7 @@ internal void ParseJsonUpdatePointers(string jsonString) continue; } - if (!TryGetChannel(readChannelId.ToString(), out var readReceiptChannel)) + if (!OLD_TryGetChannel(readChannelId.ToString(), out var readReceiptChannel)) { Debug.WriteLine("Can't find the read receipt channel!"); continue; @@ -465,7 +465,7 @@ internal void ParseJsonUpdatePointers(string jsonString) foreach (var kvp in typings) { - if (TryGetChannel(kvp.Key, out var typingChannel)) + if (OLD_TryGetChannel(kvp.Key, out var typingChannel)) { typingChannel.TryParseAndBroadcastTypingEvent(kvp.Value); OnAnyEvent?.Invoke(new ChatEvent() @@ -498,7 +498,7 @@ internal void ParseJsonUpdatePointers(string jsonString) var properChannelId = (index < 0) ? chatEvent.ChannelId : chatEvent.ChannelId.Remove(index, moderationPrefix.Length); - if (TryGetChannel(properChannelId, out var reportChannel)) + if (OLD_TryGetChannel(properChannelId, out var reportChannel)) { reportChannel.BroadcastReportEvent(chatEvent); invoked = true; @@ -522,7 +522,7 @@ internal void ParseJsonUpdatePointers(string jsonString) break; case PubnubChatEventType.Custom: - if (TryGetChannel(chatEvent.ChannelId, out var customEventChannel)) + if (OLD_TryGetChannel(chatEvent.ChannelId, out var customEventChannel)) { customEventChannel.BroadcastCustomEvent(chatEvent); invoked = true; @@ -618,7 +618,7 @@ internal void ParseJsonUpdatePointers(string jsonString) 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); + OLD_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(); } @@ -678,7 +678,7 @@ internal void ParseJsonUpdatePointers(string jsonString) channelId.LastIndexOf("-pnpres", StringComparison.Ordinal)); } - if (TryGetChannel(channelId, out var channel)) + if (OLD_TryGetChannel(channelId, out var channel)) { channel.BroadcastPresenceUpdate(); } @@ -708,13 +708,13 @@ public void AddListenerToChannelsUpdate(List channelIds, Action { foreach (var channelId in channelIds) { - if (TryGetChannel(channelId, out var channel)) + if (OLD_TryGetChannel(channelId, out var channel)) { channel.OnChannelUpdate += listener; } } } - + /// /// Creates a new public conversation. /// @@ -734,7 +734,7 @@ public void AddListenerToChannelsUpdate(List channelIds, Action /// /// /// - public async Task CreatePublicConversation(string channelId = "") + public async Task CreatePublicConversation(string channelId = "") { if (string.IsNullOrEmpty(channelId)) { @@ -765,7 +765,51 @@ public async Task CreatePublicConversation(string channelId = "") /// /// /// - public async Task CreatePublicConversation(string channelId, ChatChannelData additionalData) + public async Task CreatePublicConversation(string channelId, ChatChannelData additionalData) + { + var existingChannel = await GetChannelAsync(channelId); + if (existingChannel != null) + { + PubnubInstance.PNConfig.Logger?.Debug("Trying to create a channel with ID that already exists! Returning existing one."); + return existingChannel; + } + + var updated = await Channel.UpdateChannelData(this, channelId, additionalData); + if (updated) + { + var channel = new Channel(this, channelId, additionalData); + channelWrappers.Add(channelId, channel); + return channel; + } + else + { + return null; + } + } + + public async Task CreateDirectConversation(User user, string channelId = "", + ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) + { + throw new NotImplementedException(); + } + + public async Task CreateGroupConversation(List users, string channelId = "", + ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) + { + throw new NotImplementedException(); + } + + public async Task OLD_CreatePublicConversation(string channelId = "") + { + if (string.IsNullOrEmpty(channelId)) + { + channelId = Guid.NewGuid().ToString(); + } + + return await OLD_CreatePublicConversation(channelId, new ChatChannelData()); + } + + public async Task OLD_CreatePublicConversation(string channelId, ChatChannelData additionalData) { if (channelWrappers.TryGetValue(channelId, out var existingChannel)) { @@ -786,7 +830,7 @@ public async Task CreatePublicConversation(string channelId, ChatChanne return channel; } - public async Task CreateDirectConversation(User user, string channelId = "", + public async Task OLD_CreateDirectConversation(User user, string channelId = "", ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) { if (string.IsNullOrEmpty(channelId)) @@ -821,7 +865,7 @@ public async Task CreateDirectConversation(User user, str var createdChannelPointer = pn_chat_get_created_channel_wrapper_channel(wrapperPointer); CUtilities.CheckCFunctionResult(createdChannelPointer); - TryGetChannel(createdChannelPointer, out var createdChannel); + OLD_TryGetChannel(createdChannelPointer, out var createdChannel); var hostMembershipPointer = pn_chat_get_created_channel_wrapper_host_membership(wrapperPointer); CUtilities.CheckCFunctionResult(hostMembershipPointer); @@ -842,7 +886,7 @@ public async Task CreateDirectConversation(User user, str }; } - public async Task CreateGroupConversation(List users, string channelId = "", + public async Task OLD_CreateGroupConversation(List users, string channelId = "", ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) { if (string.IsNullOrEmpty(channelId)) @@ -873,7 +917,7 @@ public async Task CreateGroupConversation(List user var createdChannelPointer = pn_chat_get_created_channel_wrapper_channel(wrapperPointer); CUtilities.CheckCFunctionResult(createdChannelPointer); - TryGetChannel(createdChannelPointer, out var createdChannel); + OLD_TryGetChannel(createdChannelPointer, out var createdChannel); var hostMembershipPointer = pn_chat_get_created_channel_wrapper_host_membership(wrapperPointer); CUtilities.CheckCFunctionResult(hostMembershipPointer); @@ -914,6 +958,40 @@ public async Task CreateGroupConversation(List user /// /// public bool TryGetChannel(string channelId, out Channel channel) + { + channel = GetChannelAsync(channelId).Result; + return channel != null; + } + + /// + /// 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) + { + if (channelWrappers.TryGetValue(channelId, out var existingChannel)) + { + await existingChannel.Resync(); + return existingChannel; + } + else + { + var data = await Channel.GetChannelData(this, channelId); + if (data == null) + { + return null; + } + else + { + var channel = new Channel(this, channelId, data); + channelWrappers.Add(channelId, channel); + return channel; + } + } + } + + public bool OLD_TryGetChannel(string channelId, out Channel channel) { //Fetching and updating a ThreadChannel if (channelId.Contains("PUBNUB_INTERNAL_THREAD_")) @@ -939,31 +1017,26 @@ public bool TryGetChannel(string channelId, out Channel channel) else { var channelPointer = pn_chat_get_channel(chatPointer, channelId); - return TryGetChannel(channelId, channelPointer, out channel); + return OLD_TryGetChannel(channelId, channelPointer, out channel); } } - - /// - /// 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) + + public async Task OLD_GetChannelAsync(string channelId) { return await Task.Run(() => { - var result = TryGetChannel(channelId, out var channel); + var result = OLD_TryGetChannel(channelId, out var channel); return result ? channel : null; }); } - internal bool TryGetChannel(IntPtr channelPointer, out Channel channel) + internal bool OLD_TryGetChannel(IntPtr channelPointer, out Channel channel) { var channelId = Channel.GetChannelIdFromPtr(channelPointer); - return TryGetChannel(channelId, channelPointer, out channel); + return OLD_TryGetChannel(channelId, channelPointer, out channel); } - internal bool TryGetChannel(string channelId, IntPtr channelPointer, out Channel channel) + internal bool OLD_TryGetChannel(string channelId, IntPtr channelPointer, out Channel channel) { return TryGetWrapper(channelWrappers, channelId, channelPointer, () => new Channel(this, channelId, channelPointer), out channel); @@ -972,12 +1045,12 @@ internal bool TryGetChannel(string channelId, IntPtr channelPointer, out Channel //The TryGetChannel updates the pointer, these methods are for internal logic explicity sake internal void UpdateChannelPointer(IntPtr newPointer) { - TryGetChannel(newPointer, out _); + OLD_TryGetChannel(newPointer, out _); } internal void UpdateChannelPointer(string id, IntPtr newPointer) { - TryGetChannel(id, newPointer, out _); + OLD_TryGetChannel(id, newPointer, out _); } public async Task GetChannels(string filter = "", string sort = "", int limit = 0, @@ -1207,7 +1280,7 @@ public async Task OLD_CreateUser(string userId, ChatUserData additionalDat /// /// /// - public async Task CreateUser(string userId) + public async Task CreateUser(string userId) { return await CreateUser(userId, new ChatUserData()); } @@ -1229,18 +1302,26 @@ public async Task CreateUser(string userId) /// /// /// - public async Task CreateUser(string userId, ChatUserData additionalData) + public async Task CreateUser(string userId, ChatUserData additionalData) { - if (userWrappers.TryGetValue(userId, out var existingUser)) + var existingUser = await GetUserAsync(userId); + if (existingUser != null) { Debug.WriteLine("Trying to create a user with ID that already exists! Returning existing one."); return existingUser; } - - var user = new User(this, userId, additionalData); - await User.UpdateUserData(this, userId, additionalData); - userWrappers.Add(userId, user); - return user; + + var updated = await User.UpdateUserData(this, userId, additionalData); + if (updated) + { + var user = new User(this, userId, additionalData); + userWrappers.Add(userId, user); + return user; + } + else + { + return null; + } } /// @@ -1265,7 +1346,7 @@ public async Task CreateUser(string userId, ChatUserData additionalData) /// public async Task IsPresent(string userId, string channelId) { - if (TryGetChannel(channelId, out var channel)) + if (OLD_TryGetChannel(channelId, out var channel)) { return await channel.IsUserPresent(userId); } @@ -1297,7 +1378,7 @@ public async Task IsPresent(string userId, string channelId) /// public async Task> WhoIsPresent(string channelId) { - if (TryGetChannel(channelId, out var channel)) + if (OLD_TryGetChannel(channelId, out var channel)) { return await channel.WhoIsPresent(); } @@ -1664,7 +1745,7 @@ public async Task GetChannelMemberships(string channelId string sort = "", int limit = 0, Page page = null) { - if (!TryGetChannel(channelId, out var channel)) + if (!OLD_TryGetChannel(channelId, out var channel)) { return new MembersResponseWrapper(); } @@ -1725,7 +1806,7 @@ public async Task GetMessageReportsHistory(string channelI /// public bool TryGetMessage(string channelId, string messageTimeToken, out Message message) { - if (!TryGetChannel(channelId, out var channel)) + if (!OLD_TryGetChannel(channelId, out var channel)) { message = null; return false; @@ -1926,7 +2007,7 @@ public void AddListenerToMessagesUpdate(string channelId, List messageTi public async Task PinMessageToChannel(string channelId, Message message) { - if (TryGetChannel(channelId, out var channel)) + if (OLD_TryGetChannel(channelId, out var channel)) { await channel.PinMessage(message); } @@ -1934,7 +2015,7 @@ public async Task PinMessageToChannel(string channelId, Message message) public async Task UnpinMessageFromChannel(string channelId) { - if (TryGetChannel(channelId, out var channel)) + if (OLD_TryGetChannel(channelId, out var channel)) { await channel.UnpinMessage(); } @@ -1968,7 +2049,7 @@ public async Task> GetChannelMessageHistory(string channelId, stri int count) { var messages = new List(); - if (!TryGetChannel(channelId, out var channel)) + if (!OLD_TryGetChannel(channelId, out var channel)) { Debug.WriteLine("Didn't find the channel for history fetch!"); return messages; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs index 275ee2c..52fc163 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs @@ -1,3 +1,5 @@ +using PubnubApi; + namespace PubnubChatApi.Entities.Data { /// @@ -17,5 +19,22 @@ public class ChatChannelData public string ChannelUpdated { get; set; } = string.Empty; public string ChannelStatus { get; set; } = string.Empty; public string ChannelType { get; set; } = string.Empty; + + public static implicit operator ChatChannelData(PNGetChannelMetadataResult metadataResult) + { + return new ChatChannelData() + { + ChannelName = metadataResult.Name, + ChannelDescription = metadataResult.Description, + ChannelCustomDataJson = metadataResult.Custom.TryGetValue("custom", out var custom) ? custom.ToString() : string.Empty, + ChannelStatus = metadataResult.Custom.TryGetValue("status", out var status) ? status.ToString() : string.Empty, + ChannelUpdated = metadataResult.Custom.TryGetValue("updated", out var updated) + ? updated.ToString() + : string.Empty, + ChannelType = metadataResult.Custom.TryGetValue("type", out var dataType) + ? dataType.ToString() + : string.Empty, + }; + } } } \ 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 b9803a5..7d66bd3 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs @@ -29,11 +29,11 @@ public static implicit operator ChatUserData(PNUuidMetadataResult metadataResult Email = metadataResult.Email, ProfileUrl = metadataResult.ProfileUrl, Username = metadataResult.Name, - Status = metadataResult.Custom.TryGetValue("Status", out var status) ? status.ToString() : string.Empty, - Type = metadataResult.Custom.TryGetValue("DataType", out var dataType) + Status = metadataResult.Custom.TryGetValue("status", out var status) ? status.ToString() : string.Empty, + Type = metadataResult.Custom.TryGetValue("type", out var dataType) ? dataType.ToString() : string.Empty, - CustomDataJson = metadataResult.Custom.TryGetValue("CustomDataJson", out var custom) + CustomDataJson = metadataResult.Custom.TryGetValue("custom", out var custom) ? custom.ToString() : string.Empty, }; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs index 3fb6ce2..737a302 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -457,7 +457,7 @@ public virtual async Task Report(string reason) public virtual async Task Forward(string channelId) { - if (chat.TryGetChannel(channelId, out var channel)) + if (chat.OLD_TryGetChannel(channelId, out var channel)) { await chat.ForwardMessage(this, channel); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs index 1b72a42..468e0de 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -353,7 +353,7 @@ public override async Task Report(string reason) public override async Task Forward(string channelId) { - if (chat.TryGetChannel(channelId, out var channel)) + if (chat.OLD_TryGetChannel(channelId, out var channel)) { await chat.ForwardMessage(this, channel); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index e5b9f9f..9d5fd6f 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -357,9 +357,9 @@ public async Task Update(ChatUserData updatedData) await UpdateUserData(chat, Id, updatedData); } - internal static async Task UpdateUserData(Chat chat, string userId, ChatUserData chatUserData) + internal static async Task UpdateUserData(Chat chat, string userId, ChatUserData chatUserData) { - await chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true) + var result = await chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true) .Uuid(userId) .Name(chatUserData.Username) .Email(chatUserData.Email) @@ -367,11 +367,17 @@ await chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true) .ProfileUrl(chatUserData.ProfileUrl) .Custom(new Dictionary() { - { "Status", chatUserData.Status}, - { "Type", chatUserData.Type}, - { "CustomDataJson", chatUserData.CustomDataJson} + { "status", chatUserData.Status}, + { "type", chatUserData.Type}, + { "custom", chatUserData.CustomDataJson} }) .ExecuteAsync(); + if (result.Status.Error) + { + chat.PubnubInstance.PNConfig.Logger.Error($"Error when trying to update user data for user \"{userId}\": {result.Status.ErrorData.Information}"); + return false; + } + return true; } internal static async Task GetUserData(Chat chat, string userId) @@ -393,8 +399,12 @@ await chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true) } } - internal void UpdateLocalData(ChatUserData newData) + internal void UpdateLocalData(ChatUserData? newData) { + if (newData == null) + { + return; + } userData = newData; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs index 8b6bef5..791833d 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs @@ -61,7 +61,7 @@ internal static List ParseJsonChannelPointers(Chat chat, IntPtr[] chann foreach (var channelPointer in channelPointers) { var id = Channel.GetChannelIdFromPtr(channelPointer); - if (chat.TryGetChannel(id, channelPointer, out var channel)) + if (chat.OLD_TryGetChannel(id, channelPointer, out var channel)) { channels.Add(channel); } From ef33723d6d71846b90e158f1c8d13f5de335c036 Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Tue, 15 Jul 2025 17:48:46 +0200 Subject: [PATCH 05/28] implemented channel creation + associated required memberships logic --- .../PubNubChatApi.Tests/ChannelTests.cs | 12 +- .../PubNubChatApi.Tests/ChatTests.cs | 12 +- .../PubNubChatApi.Tests/MembershipTests.cs | 34 +- .../PubNubChatApi.Tests/UserTests.cs | 2 +- .../PubnubChatApi/Entities/Channel.cs | 169 +++++----- .../PubnubChatApi/Entities/Chat.cs | 300 +++++++++++++++++- .../Entities/Data/ChatChannelData.cs | 17 +- .../Entities/Data/ChatMembershipData.cs | 17 +- .../Entities/Data/ChatUserData.cs | 10 +- .../Entities/Data/CreatedChannelWrapper.cs | 2 +- .../Entities/Data/MembersResponseWrapper.cs | 10 +- .../PubnubChatApi/Entities/Data/Page.cs | 1 + .../PubnubChatApi/Entities/Membership.cs | 101 +++++- .../PubnubChatApi/Entities/User.cs | 4 +- .../Utilities/ChatEnumConverters.cs | 31 ++ .../PubnubChatApi/Utilities/ChatUtils.cs | 15 + .../Utilities/JsonMessageParsers.cs | 12 + .../PubnubChatApi/Utilities/PointerParsers.cs | 2 +- 18 files changed, 603 insertions(+), 148 deletions(-) create mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatEnumConverters.cs create mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs create mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/JsonMessageParsers.cs diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs index d3a5fe6..2b7fdc0 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs @@ -96,17 +96,17 @@ public async Task TestLeaveChannel() await Task.Delay(3000); - var memberships = await channel.GetMemberships(); + var memberships = await channel.OLD_GetMemberships(); - Assert.True(memberships.Memberships.Any(x => x.UserId == currentChatUser.Id), "Join failed, current user not found in channel memberships"); + Assert.True(memberships.Memberships.Any(x => x.OLD_UserId == currentChatUser.Id), "Join failed, current user not found in channel memberships"); channel.Leave(); await Task.Delay(3000); - memberships = await channel.GetMemberships(); + memberships = await channel.OLD_GetMemberships(); - Assert.False(memberships.Memberships.Any(x => x.UserId == currentChatUser.Id), "Leave failed, current user found in channel memberships"); + Assert.False(memberships.Memberships.Any(x => x.OLD_UserId == currentChatUser.Id), "Leave failed, current user found in channel memberships"); } [Test] @@ -118,7 +118,7 @@ public async Task TestGetUserSuggestions() await Task.Delay(5000); var suggestions = await channel.GetUserSuggestions("@Test"); - Assert.True(suggestions.Any(x => x.UserId == user.Id)); + Assert.True(suggestions.Any(x => x.OLD_UserId == user.Id)); } [Test] @@ -127,7 +127,7 @@ public async Task TestGetMemberships() var channel = await chat.OLD_CreatePublicConversation("get_members_test_channel"); channel.Join(); await Task.Delay(3500); - var memberships = await channel.GetMemberships(); + var memberships = await channel.OLD_GetMemberships(); Assert.That(memberships.Memberships.Count, Is.GreaterThanOrEqualTo(1)); } diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs index afcd136..ff96ce7 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -65,7 +65,7 @@ public async Task TestGetCurrentUser() [Test] public async Task TestGetEventHistory() { - await chat.EmitEvent(PubnubChatEventType.Custom, channel.Id, "{\"test\":\"some_nonsense\"}"); + await chat.OLD_EmitEvent(PubnubChatEventType.Custom, channel.Id, "{\"test\":\"some_nonsense\"}"); await Task.Delay(5000); @@ -95,9 +95,9 @@ public async Task TestCreateDirectConversation() var directConversation = await chat.OLD_CreateDirectConversation(convoUser, "direct_conversation_test"); Assert.True(directConversation.CreatedChannel is { Id: "direct_conversation_test" }); - Assert.True(directConversation.HostMembership != null && directConversation.HostMembership.UserId == currentUser.Id); + Assert.True(directConversation.HostMembership != null && directConversation.HostMembership.OLD_UserId == currentUser.Id); Assert.True(directConversation.InviteesMemberships != null && - directConversation.InviteesMemberships.First().UserId == convoUser.Id); + directConversation.InviteesMemberships.First().OLD_UserId == convoUser.Id); } [Test] @@ -109,10 +109,10 @@ public async Task TestCreateGroupConversation() var groupConversation = await chat.OLD_CreateGroupConversation([convoUser1, convoUser2, convoUser3], "group_conversation_test"); Assert.True(groupConversation.CreatedChannel is { Id: "group_conversation_test" }); - Assert.True(groupConversation.HostMembership != null && groupConversation.HostMembership.UserId == currentUser.Id); + Assert.True(groupConversation.HostMembership != null && groupConversation.HostMembership.OLD_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.OLD_UserId == convoUser1.Id && x.OLD_ChannelId == "group_conversation_test")); } [Test] @@ -148,7 +148,7 @@ public async Task TestEmitEvent() }; channel.SetListeningForCustomEvents(true); await Task.Delay(2500); - await chat.EmitEvent(PubnubChatEventType.Custom, channel.Id, "{\"test\":\"some_nonsense\"}"); + await chat.OLD_EmitEvent(PubnubChatEventType.Custom, channel.Id, "{\"test\":\"some_nonsense\"}"); var eventReceived = reportManualEvent.WaitOne(8000); Assert.True(eventReceived); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs index 11b4c37..6d0b67c 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs @@ -42,7 +42,7 @@ public async Task CleanUp() public async Task TestGetMemberships() { var memberships = await user.GetMemberships(); - Assert.True(memberships.Memberships.Any(x => x.ChannelId == channel.Id && x.UserId == user.Id)); + Assert.True(memberships.Memberships.Any(x => x.OLD_ChannelId == channel.Id && x.OLD_UserId == user.Id)); } [Test] @@ -58,22 +58,22 @@ public async Task TestUpdateMemberships() var updateData = new ChatMembershipData() { - CustomDataJson = "{\"key\":\"" + Guid.NewGuid() + "\"}" + OLD_CustomDataJson = "{\"key\":\"" + Guid.NewGuid() + "\"}" }; var manualUpdatedEvent = new ManualResetEvent(false); testMembership.OnMembershipUpdated += membership => { Assert.True(membership.Id == testMembership.Id); - var updatedData = membership.MembershipData.CustomDataJson; - Assert.True(updatedData == updateData.CustomDataJson, $"{updatedData} != {updateData.CustomDataJson}"); + var updatedData = membership.OLD_MembershipData.OLD_CustomDataJson; + Assert.True(updatedData == updateData.OLD_CustomDataJson, $"{updatedData} != {updateData.OLD_CustomDataJson}"); manualUpdatedEvent.Set(); }; testMembership.SetListeningForUpdates(true); await Task.Delay(4000); - await testMembership.Update(updateData); + await testMembership.OLD_Update(updateData); var updated = manualUpdatedEvent.WaitOne(8000); Assert.IsTrue(updated); } @@ -83,8 +83,8 @@ public async Task TestInvite() { var testChannel = (await chat.OLD_CreateGroupConversation([user], "test_invite_group_channel")).CreatedChannel; var testUser = await chat.GetOrCreateUser("test_invite_user"); - var returnedMembership = await testChannel.Invite(testUser); - Assert.True(returnedMembership.ChannelId == testChannel.Id && returnedMembership.UserId == testUser.Id); + var returnedMembership = await testChannel.OLD_Invite(testUser); + Assert.True(returnedMembership.OLD_ChannelId == testChannel.Id && returnedMembership.OLD_UserId == testUser.Id); } [Test] @@ -94,14 +94,14 @@ public async Task TestInviteMultiple() .CreatedChannel; var secondUser = await chat.GetOrCreateUser("second_invite_user"); var thirdUser = await chat.GetOrCreateUser("third_invite_user"); - var returnedMemberships = await testChannel.InviteMultiple([ + var returnedMemberships = await testChannel.OLD_InviteMultiple([ secondUser, thirdUser ]); Assert.True( returnedMemberships.Count == 2 && - returnedMemberships.Any(x => x.UserId == secondUser.Id && x.ChannelId == testChannel.Id) && - returnedMemberships.Any(x => x.UserId == thirdUser.Id && x.ChannelId == testChannel.Id)); + returnedMemberships.Any(x => x.OLD_UserId == secondUser.Id && x.OLD_ChannelId == testChannel.Id) && + returnedMemberships.Any(x => x.OLD_UserId == thirdUser.Id && x.OLD_ChannelId == testChannel.Id)); } [Test] @@ -113,7 +113,7 @@ public async Task TestLastRead() await Task.Delay(4000); var membership = (await user.GetMemberships(limit: 20)).Memberships - .FirstOrDefault(x => x.ChannelId == testChannel.Id); + .FirstOrDefault(x => x.OLD_ChannelId == testChannel.Id); if (membership == null) { Assert.Fail(); @@ -128,13 +128,13 @@ public async Task TestLastRead() await Task.Delay(7000); - var lastTimeToken = membership.GetLastReadMessageTimeToken(); + var lastTimeToken = membership.OLD_GetLastReadMessageTimeToken(); Assert.True(lastTimeToken == message.TimeToken); - await membership.SetLastReadMessageTimeToken("99999999999999999"); + await membership.OLD_SetLastReadMessageTimeToken("99999999999999999"); await Task.Delay(3000); - Assert.True(membership.GetLastReadMessageTimeToken() == "99999999999999999"); + Assert.True(membership.OLD_GetLastReadMessageTimeToken() == "99999999999999999"); messageReceivedManual.Set(); }; await testChannel.SendText("some_message"); @@ -157,8 +157,8 @@ public async Task TestUnreadMessagesCount() await Task.Delay(8000); - var membership = (await unreadChannel.GetMemberships()) - .Memberships.FirstOrDefault(x => x.UserId == user.Id); + var membership = (await unreadChannel.OLD_GetMemberships()) + .Memberships.FirstOrDefault(x => x.OLD_UserId == user.Id); var unreadCount = membership == null ? -1 : await membership.GetUnreadMessagesCount(); Assert.True(unreadCount >= 3, $"Expected >=3 unread but got: {unreadCount}"); } @@ -170,7 +170,7 @@ public async Task TestUnreadCountAfterFetchHistory() { await channel.SendText("some_text"); var membership = (await user.GetMemberships()) - .Memberships.FirstOrDefault(x => x.ChannelId == channel.Id); + .Memberships.FirstOrDefault(x => x.OLD_ChannelId == channel.Id); if (membership == null) { Assert.Fail("Couldn't find membership"); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs index 4c99c88..59af7ea 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs @@ -80,7 +80,7 @@ public async Task TestUserUpdate() await testUser.OLD_Update(new ChatUserData() { Username = newRandomUserName, - CustomDataJson = "{\"some_key\":\"some_value\"}", + OLD_CustomDataJson = "{\"some_key\":\"some_value\"}", Email = "some@guy.com", ExternalId = "xxx_some_guy_420_xxx", ProfileUrl = "www.some.guy", diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index cfc958a..255791e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using System.Timers; using Newtonsoft.Json; +using PubnubApi; using PubnubChatApi.Entities.Data; using PubnubChatApi.Entities.Events; using PubnubChatApi.Enums; @@ -295,6 +296,9 @@ public string OLD_Type private IntPtr typingListeningHandle; private IntPtr presenceListeningHandle; protected IntPtr connectionHandle; + + protected Subscription subscription; + private Dictionary typingIndicators = new(); /// @@ -385,7 +389,6 @@ internal void UpdateLocalData(ChatChannelData? newData) internal static async Task UpdateChannelData(Chat chat, string channelId, ChatChannelData data) { - //chat.PubnubInstance.setmem var result = await chat.PubnubInstance.SetChannelMetadata().IncludeCustom(true) .Channel(channelId) .Name(data.ChannelName) @@ -662,147 +665,147 @@ public MessageDraft CreateMessageDraft(UserSuggestionSource userSuggestionSource } /// - /// 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. - /// - /// + /// Thrown when an error occurs while disconnecting from the channel. + /// /// - public async void Connect() + public void Disconnect() { - if (connectionHandle != IntPtr.Zero) + if (connectionHandle == IntPtr.Zero || pointer == IntPtr.Zero) { return; } - connectionHandle = await SetListening(connectionHandle, true, () => pn_channel_connect(pointer)); + CUtilities.CheckCFunctionResult(pn_channel_disconnect(pointer)); + pn_callback_handle_dispose(connectionHandle); + connectionHandle = IntPtr.Zero; } /// - /// 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. - /// + /// Thrown when an error occurs while leaving the channel. + /// /// /// - public async void Join(ChatMembershipData? membershipData = null) + public async void Leave() { - if (connectionHandle != IntPtr.Zero) + if (connectionHandle == IntPtr.Zero || pointer == IntPtr.Zero) { return; } - if (membershipData == null) - { - connectionHandle = - await SetListening(connectionHandle, true, () => pn_channel_join(pointer, string.Empty)); - } - else + var connectionHandleCopy = connectionHandle; + connectionHandle = IntPtr.Zero; + CUtilities.CheckCFunctionResult(await Task.Run(() => { - connectionHandle = await SetListening(connectionHandle, true, - () => pn_channel_join_with_membership_data(pointer, membershipData.CustomDataJson, - membershipData.Type, membershipData.Status)); - } + if (pointer == IntPtr.Zero) + { + return 0; + } + + pn_channel_leave(pointer); + pn_callback_handle_dispose(connectionHandleCopy); + return 0; + })); } /// - /// 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. - /// + /// Thrown when an error occurs while connecting to the channel. + /// + /// /// - public void Disconnect() + public async void Connect() { - if (connectionHandle == IntPtr.Zero || pointer == IntPtr.Zero) + if (connectionHandle != IntPtr.Zero) { return; } - CUtilities.CheckCFunctionResult(pn_channel_disconnect(pointer)); - pn_callback_handle_dispose(connectionHandle); - connectionHandle = IntPtr.Zero; + connectionHandle = await SetListening(connectionHandle, true, () => pn_channel_connect(pointer)); } - + /// - /// 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. - /// + /// Thrown when an error occurs while joining the channel. + /// /// /// - public async void Leave() + public async void Join(ChatMembershipData? membershipData = null) { - if (connectionHandle == IntPtr.Zero || pointer == IntPtr.Zero) + if (connectionHandle != IntPtr.Zero) { return; } - var connectionHandleCopy = connectionHandle; - connectionHandle = IntPtr.Zero; - CUtilities.CheckCFunctionResult(await Task.Run(() => + if (membershipData == null) { - if (pointer == IntPtr.Zero) - { - return 0; - } - - pn_channel_leave(pointer); - pn_callback_handle_dispose(connectionHandleCopy); - return 0; - })); + 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.OLD_CustomDataJson, + membershipData.Type, membershipData.Status)); + } } /// @@ -1053,11 +1056,17 @@ public async Task> WhoIsPresent() /// /// 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) + public async Task GetMemberships(string filter = "", string sort = "", int limit = 0, + PNPageObject page = null) { return await chat.GetChannelMemberships(Id, filter, sort, limit, page); } + + public async Task OLD_GetMemberships(string filter = "", string sort = "", int limit = 0, + Page page = null) + { + return await chat.OLD_GetChannelMemberships(Id, filter, sort, limit, page); + } /// /// Gets the Message object for the given timetoken. @@ -1099,17 +1108,27 @@ public async Task> GetMessageHistory(string startTimeToken, string { return await chat.GetChannelMessageHistory(Id, startTimeToken, endTimeToken, count); } + + public async Task Invite(User user) + { + return await chat.InviteToChannel(Id, user.Id); + } - public async Task Invite(User user) + public async Task OLD_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); + chat.OLD_TryGetMembership(membershipId, membershipPointer, out var membership); return membership; } - + public async Task> InviteMultiple(List users) + { + return await chat.InviteMultipleToChannel(Id, users); + } + + public async Task> OLD_InviteMultiple(List users) { var buffer = new StringBuilder(8192); CUtilities.CheckCFunctionResult(await Task.Run(() => pn_channel_invite_multiple(pointer, diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 71d516c..ebaf5f5 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -773,7 +773,8 @@ public void AddListenerToChannelsUpdate(List channelIds, Action PubnubInstance.PNConfig.Logger?.Debug("Trying to create a channel with ID that already exists! Returning existing one."); return existingChannel; } - + + additionalData.ChannelType = "public"; var updated = await Channel.UpdateChannelData(this, channelId, additionalData); if (updated) { @@ -787,16 +788,111 @@ public void AddListenerToChannelsUpdate(List channelIds, Action } } - public async Task CreateDirectConversation(User user, string channelId = "", + private async Task CreateConversation( + string type, + List users, + string channelId = "", + ChatChannelData? channelData = null, + ChatMembershipData? membershipData = null) + { + if (string.IsNullOrEmpty(channelId)) + { + channelId = Guid.NewGuid().ToString(); + } + + var existingChannel = await GetChannelAsync(channelId); + if (existingChannel != null) + { + PubnubInstance.PNConfig.Logger?.Warn("Trying to create a channel with ID that already exists! Aborting."); + return null; + } + + channelData ??= new ChatChannelData(); + channelData.ChannelType = type; + var updated = await Channel.UpdateChannelData(this, channelId, channelData); + if (!updated) + { + return null; + } + + 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(); + + if (setMembershipResult.Status.Error) + { + PubnubInstance.PNConfig.Logger?.Error($"Error when trying to set memberships for {type} conversation: {setMembershipResult.Status.Error}"); + return null; + } + + var responseWrapper = new CreatedChannelWrapper(); + if (membershipWrappers.TryGetValue(currentUserId + channelId, out var existingHostMembership)) + { + existingHostMembership.UpdateLocalData(membershipData); + responseWrapper.HostMembership = existingHostMembership; + } + else + { + var hostMembership = new Membership(this, currentUserId, channelId, membershipData); + membershipWrappers.Add(hostMembership.Id, hostMembership); + responseWrapper.HostMembership = hostMembership; + } + + var channel = new Channel(this, channelId, channelData); + channelWrappers.Add(channelId, channel); + + if (type == "direct") + { + var inviteMembership = await InviteToChannel(channelId, users[0].Id); + if (inviteMembership == null) + { + PubnubInstance.PNConfig.Logger?.Error($"Error when trying to invite user \"{users[0].Id}\" to direct conversation \"{channelId}\": {setMembershipResult.Status.Error}"); + return null; + } + responseWrapper.InviteesMemberships = new List() { inviteMembership }; + }else if (type == "group") + { + var inviteMembership = await InviteMultipleToChannel(channelId, users); + if (inviteMembership?.Count == 0) + { + PubnubInstance.PNConfig.Logger?.Error($"Error when trying to invite users to group conversation \"{channelId}\": {setMembershipResult.Status.Error}"); + return null; + } + responseWrapper.InviteesMemberships = new List(inviteMembership); + } + return responseWrapper; + } + + public async Task CreateDirectConversation(User user, string channelId = "", ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) { - throw new NotImplementedException(); + return await CreateConversation("direct", new List() { user }, channelId, channelData, + membershipData); } - public async Task CreateGroupConversation(List users, string channelId = "", + public async Task CreateGroupConversation(List users, string channelId = "", ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) { - throw new NotImplementedException(); + return await CreateConversation("group", users, channelId, channelData, + membershipData); } public async Task OLD_CreatePublicConversation(string channelId = "") @@ -857,7 +953,7 @@ public async Task OLD_CreateDirectConversation(User user, user.Pointer, channelId, channelData.ChannelName, channelData.ChannelDescription, channelData.ChannelCustomDataJson, channelData.ChannelUpdated, - channelData.ChannelStatus, channelData.ChannelType, membershipData.CustomDataJson, + channelData.ChannelStatus, channelData.ChannelType, membershipData.OLD_CustomDataJson, membershipData.Type, membershipData.Status)); } @@ -869,7 +965,7 @@ public async Task OLD_CreateDirectConversation(User user, var hostMembershipPointer = pn_chat_get_created_channel_wrapper_host_membership(wrapperPointer); CUtilities.CheckCFunctionResult(hostMembershipPointer); - TryGetMembership(hostMembershipPointer, out var hostMembership); + OLD_TryGetMembership(hostMembershipPointer, out var hostMembership); var buffer = new StringBuilder(8192); CUtilities.CheckCFunctionResult( @@ -909,7 +1005,7 @@ public async Task OLD_CreateGroupConversation(List chatPointer, users.Select(x => x.Pointer).ToArray(), users.Count, channelId, channelData.ChannelName, channelData.ChannelDescription, channelData.ChannelCustomDataJson, channelData.ChannelUpdated, - channelData.ChannelStatus, channelData.ChannelType, membershipData.CustomDataJson, + channelData.ChannelStatus, channelData.ChannelType, membershipData.OLD_CustomDataJson, membershipData.Type, membershipData.Status)); } @@ -921,7 +1017,7 @@ public async Task OLD_CreateGroupConversation(List var hostMembershipPointer = pn_chat_get_created_channel_wrapper_host_membership(wrapperPointer); CUtilities.CheckCFunctionResult(hostMembershipPointer); - TryGetMembership(hostMembershipPointer, out var hostMembership); + OLD_TryGetMembership(hostMembershipPointer, out var hostMembership); var buffer = new StringBuilder(8192); CUtilities.CheckCFunctionResult( @@ -938,6 +1034,121 @@ public async Task OLD_CreateGroupConversation(List }; } + public async Task InviteToChannel(string channelId, string userId) + { + //Check if already a member first + var members = await GetChannelMemberships(channelId, filter:$"uuid.id == \"{userId}\""); + if (members != null && members.Memberships.Any()) + { + //Already a member, just return current membership + return members.Memberships[0]; + } + + var channel = await GetChannelAsync(channelId); + if (channel == null) + { + PubnubInstance.PNConfig?.Logger.Error($"Error: tried to invite user \"{userId}\" to channel \"{channelId}\" but such channel doesn't exist!"); + return null; + } + + var response = 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(); + + if (response.Status.Error) + { + PubnubInstance.PNConfig?.Logger.Error($"Error when trying to invite user \"{userId}\" to channel \"{channelId}\": {response.Status.ErrorData.Information}"); + return null; + } + + var newMataData = response.Result.Memberships?.FirstOrDefault(x => x.ChannelMetadata.Channel == channelId)? + .ChannelMetadata; + if (newMataData != null) + { + channel.UpdateLocalData(newMataData); + } + + var inviteEventPayload = $"{{\"channelType\": \"{channel.Type}\", \"channelId\": {channelId}}}"; + await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload); + + var newMembership = new Membership(this, userId, channelId, new ChatMembershipData()); + await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()); + membershipWrappers.Add(newMembership.Id, newMembership); + + return newMembership; + } + + public async Task> InviteMultipleToChannel(string channelId, List users) + { + var memberships = new List(); + var channel = await GetChannelAsync(channelId); + if (channel == null) + { + PubnubInstance.PNConfig?.Logger.Error($"Error: tried to invite multiple users to channel \"{channelId}\" but such channel doesn't exist!"); + return memberships; + } + var inviteResponse = await PubnubInstance.SetChannelMembers().Channel(channelId) + .Include( + //TODO: C# FIX, MISSING VALUES + new[] { + PNChannelMemberField.UUID, + PNChannelMemberField.CUSTOM, + PNChannelMemberField.UUID_CUSTOM + }) + //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(); + + if (inviteResponse.Status.Error) + { + PubnubInstance.PNConfig?.Logger.Error($"Error when trying to invite multiple users to channel \"{channelId}\": {inviteResponse.Status.ErrorData.Information}"); + return memberships; + } + + var usersDict = users.ToDictionary(x => x.Id, y => y); + foreach (var channelMember in inviteResponse.Result.ChannelMembers) + { + var userId = channelMember.UuidMetadata.Uuid; + if (membershipWrappers.TryGetValue(userId + channelId, + out var existingMembership)) + { + usersDict[userId].UpdateLocalData(channelMember.UuidMetadata); + existingMembership.UpdateLocalData(channelMember); + memberships.Add(existingMembership); + } + else + { + var newMembership = new Membership(this, userId, channelId, channelMember); + await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()); + membershipWrappers.Add(newMembership.Id, newMembership); + memberships.Add(newMembership); + } + + var inviteEventPayload = $"{{\"channelType\": \"{channel.Type}\", \"channelId\": {channelId}}}"; + await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload); + } + + await channel.Resync(); + + return memberships; + } + /// /// Gets the channel by the provided channel ID. /// @@ -1252,7 +1463,7 @@ public async Task OLD_CreateUser(string userId, ChatUserData additionalDat additionalData.ExternalId, additionalData.ProfileUrl, additionalData.Email, - additionalData.CustomDataJson, + additionalData.OLD_CustomDataJson, additionalData.Status, additionalData.Status)); CUtilities.CheckCFunctionResult(userPointer); @@ -1582,7 +1793,7 @@ public async Task OLD_UpdateUser(string userId, ChatUserData updatedData) updatedData.ExternalId, updatedData.ProfileUrl, updatedData.Email, - updatedData.CustomDataJson, + updatedData.OLD_CustomDataJson, updatedData.Status, updatedData.Type)); CUtilities.CheckCFunctionResult(newPointer); @@ -1741,7 +1952,56 @@ public void AddListenerToMembershipsUpdate(List membershipIds, Action /// /// - public async Task GetChannelMemberships(string channelId, string filter = "", + public async Task GetChannelMemberships(string channelId, string filter = "", + string sort = "", + int limit = 0, PNPageObject page = null) + { + var result = await PubnubInstance.GetChannelMembers().Include( + new[] + { + //TODO: C# FIX + //PNChannelMemberField.CHANNEL_CUSTOM, + PNChannelMemberField.CUSTOM, + //PNChannelMemberField.CHANNEL, + //PNChannelMemberField.STATUS, + }).Channel(channelId).Filter(filter).Sort(new List() { sort }) + .Limit(limit).Page(page).ExecuteAsync(); + if (result.Status.Error) + { + PubnubInstance.PNConfig.Logger?.Error($"Error when trying to get \"{channelId}\" channel members: {result.Status.ErrorData.Information}"); + return null; + } + + var memberships = new List(); + foreach (var channelMemberResult in result.Result.ChannelMembers) + { + var membershipId = channelMemberResult.UuidMetadata.Uuid + channelId; + if (membershipWrappers.TryGetValue(membershipId, out var existingMembershipWrapper)) + { + existingMembershipWrapper.MembershipData.CustomData = channelMemberResult.Custom; + memberships.Add(existingMembershipWrapper); + } + else + { + memberships.Add(new Membership(this, channelMemberResult.UuidMetadata.Uuid, channelId, new ChatMembershipData() + { + CustomData = channelMemberResult.Custom + })); + } + } + return new MembersResponseWrapper() + { + Memberships = memberships, + Page = new Page() + { + Next = result.Result.Page.Next, + Previous = result.Result.Page.Prev + }, + Total = result.Result.TotalCount + }; + } + + public async Task OLD_GetChannelMemberships(string channelId, string filter = "", string sort = "", int limit = 0, Page page = null) { @@ -1760,13 +2020,13 @@ public async Task GetChannelMemberships(string channelId return new MembersResponseWrapper(this, internalWrapper); } - private bool TryGetMembership(IntPtr membershipPointer, out Membership membership) + private bool OLD_TryGetMembership(IntPtr membershipPointer, out Membership membership) { var membershipId = Membership.GetMembershipIdFromPtr(membershipPointer); - return TryGetMembership(membershipId, membershipPointer, out membership); + return OLD_TryGetMembership(membershipId, membershipPointer, out membership); } - internal bool TryGetMembership(string membershipId, IntPtr membershipPointer, out Membership membership) + internal bool OLD_TryGetMembership(string membershipId, IntPtr membershipPointer, out Membership membership) { return TryGetWrapper(membershipWrappers, membershipId, membershipPointer, () => new Membership(this, membershipPointer, membershipId), out membership); @@ -2100,8 +2360,16 @@ public async Task GetEventsHistory(string channelId, strin return JsonConvert.DeserializeObject(wrapperJson); } - + public async Task EmitEvent(PubnubChatEventType type, string channelId, string jsonPayload) + { + jsonPayload = jsonPayload.Remove(0, 1); + jsonPayload = jsonPayload.Remove(jsonPayload.Length - 1); + var fullPayload = $"{{{jsonPayload}, \"type\": {ChatEnumConverters.ChatEventTypeToString(type)}}}"; + await PubnubInstance.Publish().Channel(channelId).Message(fullPayload).ExecuteAsync(); + } + + public async Task OLD_EmitEvent(PubnubChatEventType type, string channelId, string jsonPayload) { CUtilities.CheckCFunctionResult(await Task.Run(() => pn_chat_emit_event(chatPointer, (byte)type, channelId, jsonPayload))); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs index 52fc163..082b1d0 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs @@ -20,7 +20,7 @@ public class ChatChannelData public string ChannelStatus { get; set; } = string.Empty; public string ChannelType { get; set; } = string.Empty; - public static implicit operator ChatChannelData(PNGetChannelMetadataResult metadataResult) + /*public static implicit operator ChatChannelData(PNGetChannelMetadataResult metadataResult) { return new ChatChannelData() { @@ -35,6 +35,21 @@ public static implicit operator ChatChannelData(PNGetChannelMetadataResult metad ? dataType.ToString() : string.Empty, }; + }*/ + + public static implicit operator ChatChannelData(PNChannelMetadataResult metadataResult) + { + return new ChatChannelData() + { + ChannelName = metadataResult.Name, + ChannelDescription = metadataResult.Description, + ChannelCustomDataJson = metadataResult.Custom.TryGetValue("custom", out var custom) ? custom.ToString() : string.Empty, + ChannelStatus = metadataResult.Custom.TryGetValue("status", out var status) ? status.ToString() : string.Empty, + ChannelUpdated = metadataResult.Updated, + ChannelType = metadataResult.Custom.TryGetValue("type", out var dataType) + ? dataType.ToString() + : string.Empty, + }; } } } \ 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..de69ccd 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using PubnubApi; + namespace PubnubChatApi.Entities.Data { /// @@ -11,8 +14,20 @@ namespace PubnubChatApi.Entities.Data /// public class ChatMembershipData { - public string CustomDataJson { get; set; } = string.Empty; + public string OLD_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 ChatMembershipData(PNChannelMembersItemResult membersItem) + { + //TODO: C# FIX, MISSING VALUES + 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/ChatUserData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs index 7d66bd3..9af984b 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using PubnubApi; namespace PubnubChatApi.Entities.Data @@ -17,7 +18,8 @@ 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 string OLD_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; @@ -33,9 +35,11 @@ public static implicit operator ChatUserData(PNUuidMetadataResult metadataResult Type = metadataResult.Custom.TryGetValue("type", out var dataType) ? dataType.ToString() : string.Empty, - CustomDataJson = metadataResult.Custom.TryGetValue("custom", out var custom) - ? custom.ToString() + //TODO: won't work? cause it's not a json string i think. to be removed anyway + OLD_CustomDataJson = metadataResult.Custom.TryGetValue("custom", out var customJson) + ? customJson.ToString() : string.Empty, + CustomData = metadataResult.Custom.TryGetValue("custom", out var custom) ? (Dictionary)custom : new () }; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs index 10a65e0..0e4aee5 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs @@ -3,7 +3,7 @@ namespace PubnubChatApi.Entities.Data { - public struct CreatedChannelWrapper + public class CreatedChannelWrapper { public Channel CreatedChannel; public Membership HostMembership; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs index 63c185f..925afba 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs @@ -5,13 +5,17 @@ namespace PubnubChatApi.Entities.Data { - public struct MembersResponseWrapper + public class MembersResponseWrapper { - public List Memberships; - public Page Page; + public List Memberships = new (); + public Page Page = new (); public int Total; public string Status; + internal MembersResponseWrapper() + { + } + internal MembersResponseWrapper(Chat chat, InternalMembersResponseWrapper internalWrapper) { Page = internalWrapper.Page; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/Page.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/Page.cs index f19b330..30c55d1 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/Page.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/Page.cs @@ -1,5 +1,6 @@ namespace PubnubChatApi.Entities.Data { + //TODO: REMOVE and replace with PNPage public class Page { public string Next = string.Empty; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs index 48184cb..01f4f1f 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; +using PubnubApi; using PubnubChatApi.Entities.Data; using PubnubChatApi.Enums; using PubnubChatApi.Utilities; @@ -72,10 +74,23 @@ private static extern void pn_membership_get_membership_data( #endregion + /// /// The user ID of the user that this membership belongs to. /// - public string UserId + 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.TryGetValue("lastReadMessageTimetoken", out var timeToken) ? timeToken.ToString() : ""; + + public string OLD_UserId { get { @@ -84,11 +99,8 @@ public string UserId return buffer.ToString(); } } - - /// - /// The channel ID of the channel that this membership belongs to. - /// - public string ChannelId + + public string OLD_ChannelId { get { @@ -97,11 +109,8 @@ public string ChannelId return buffer.ToString(); } } - - /// - /// Returns a class with additional Membership data. - /// - public ChatMembershipData MembershipData + + public ChatMembershipData OLD_MembershipData { get { @@ -118,6 +127,8 @@ public ChatMembershipData MembershipData } } + public ChatMembershipData MembershipData { get; private set; } + /// /// Event that is triggered when the membership is updated. /// @@ -133,7 +144,7 @@ public ChatMembershipData MembershipData /// }; /// /// - /// + /// public event Action OnMembershipUpdated; private Chat chat; @@ -144,6 +155,19 @@ internal Membership(Chat chat, IntPtr membershipPointer, string membershipId) : this.chat = chat; } + internal Membership(Chat chat, string userId, string channelId, ChatMembershipData membershipData) : base(userId+channelId) + { + UserId = userId; + ChannelId = channelId; + UpdateLocalData(membershipData); + this.chat = chat; + } + + internal void UpdateLocalData(ChatMembershipData newData) + { + MembershipData = newData; + } + protected override IntPtr StreamUpdates() { return pn_membership_stream_updates(pointer); @@ -183,13 +207,51 @@ internal override void UpdateWithPartialPtr(IntPtr partialPointer) /// public async Task Update(ChatMembershipData membershipData) { - var newPointer = await Task.Run(() => pn_membership_update_dirty(pointer, membershipData.CustomDataJson, + var updateSuccess = await UpdateMembershipData(membershipData); + if (updateSuccess) + { + UpdateLocalData(membershipData); + } + } + + internal async Task UpdateMembershipData(ChatMembershipData membershipData) + { + var updateResponse = await chat.PubnubInstance.SetMemberships().Uuid(UserId).Channels(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 + }).ExecuteAsync(); + + if (updateResponse.Status.Error) + { + chat.PubnubInstance.PNConfig.Logger?.Error($"Error when trying to update membership (channel: {ChannelId}, user: {UserId}): {updateResponse.Status.ErrorData.Information}"); + return false; + } + + return true; + } + + public async Task OLD_Update(ChatMembershipData membershipData) + { + var newPointer = await Task.Run(() => pn_membership_update_dirty(pointer, membershipData.OLD_CustomDataJson, membershipData.Type, membershipData.Status)); CUtilities.CheckCFunctionResult(newPointer); UpdatePointer(newPointer); } - public string GetLastReadMessageTimeToken() + public string OLD_GetLastReadMessageTimeToken() { var buffer = new StringBuilder(128); CUtilities.CheckCFunctionResult(pn_membership_last_read_message_timetoken(pointer, buffer)); @@ -202,8 +264,17 @@ public async Task SetLastReadMessage(Message message) CUtilities.CheckCFunctionResult(newPointer); UpdatePointer(newPointer); } - + public async Task SetLastReadMessageTimeToken(string timeToken) + { + MembershipData.CustomData["lastReadMessageTimetoken"] = timeToken; + if (await UpdateMembershipData(MembershipData)) + { + await chat.EmitEvent(PubnubChatEventType.Receipt, ChannelId, $"{{\"messageTimetoken\": \"{timeToken}\"}}"); + } + } + + public async Task OLD_SetLastReadMessageTimeToken(string timeToken) { var newPointer = await Task.Run(() => pn_membership_set_last_read_message_timetoken(pointer, timeToken)); CUtilities.CheckCFunctionResult(newPointer); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index 9d5fd6f..2ed602c 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -190,7 +190,7 @@ public string OLD_DataType /// This might be any custom data that you want to store for the user. /// /// - public string CustomData => userData.CustomDataJson; + public Dictionary CustomData => userData.CustomData; /// /// The user's status. @@ -369,7 +369,7 @@ internal static async Task UpdateUserData(Chat chat, string userId, ChatUs { { "status", chatUserData.Status}, { "type", chatUserData.Type}, - { "custom", chatUserData.CustomDataJson} + { "custom", chatUserData.OLD_CustomDataJson} }) .ExecuteAsync(); if (result.Status.Error) 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..574bbb7 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatEnumConverters.cs @@ -0,0 +1,31 @@ +using PubnubChatApi.Enums; + +namespace PubnubChatApi.Utilities +{ + 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; + } + } + } +} \ 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..2d921d6 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs @@ -0,0 +1,15 @@ +using System; +using System.Globalization; + +namespace PubnubChatApi.Utilities +{ + 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); + return timeStamp.ToString(CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/JsonMessageParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/JsonMessageParsers.cs new file mode 100644 index 0000000..c706824 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/JsonMessageParsers.cs @@ -0,0 +1,12 @@ +using PubnubApi; + +namespace PubnubChatApi.Utilities +{ + internal static class JsonMessageParsers + { + /*internal static bool IsMessage(Pubnub pubnub, string payload) + { + pubnub.JsonPluggableLibrary.DeserializeToDictionaryOfObject(payload)["type"] + }*/ + } +} \ 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 index 791833d..65fac7d 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs @@ -30,7 +30,7 @@ internal static List ParseJsonMembershipPointers(Chat chat, IntPtr[] foreach (var membershipPointer in membershipPointers) { var id = Membership.GetMembershipIdFromPtr(membershipPointer); - if (chat.TryGetMembership(id, membershipPointer, out var membership)) + if (chat.OLD_TryGetMembership(id, membershipPointer, out var membership)) { memberships.Add(membership); } From 91cd41e59726a2ba8db653bc2510e949eaf875e1 Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Tue, 22 Jul 2025 14:06:54 +0200 Subject: [PATCH 06/28] WIP message receiving --- .../PubNubChatApi.Tests/ChannelTests.cs | 28 +-- .../PubNubChatApi.Tests/ChatEventTests.cs | 4 +- .../PubNubChatApi.Tests/ChatTests.cs | 12 +- .../PubNubChatApi.Tests/MembershipTests.cs | 10 +- .../PubNubChatApi.Tests/MessageDraftTests.cs | 4 +- .../PubNubChatApi.Tests/MessageTests.cs | 42 ++-- .../PubNubChatApi.Tests/ThreadsTests.cs | 22 +- .../PubNubChatApi.Tests/UserTests.cs | 8 +- .../PubnubChatApi/Entities/Channel.cs | 141 +++++++++--- .../PubnubChatApi/Entities/Chat.cs | 26 ++- .../Entities/Data/MentionedUser.cs | 8 + .../PubnubChatApi/Entities/Message.cs | 208 ++++++++++++------ .../PubnubChatApi/Entities/ThreadChannel.cs | 2 +- .../PubnubChatApi/Entities/ThreadMessage.cs | 30 +-- .../Utilities/ChatListenerFactory.cs | 16 ++ .../PubnubChatApi/Utilities/ChatParsers.cs | 26 +++ .../Utilities/DotNetListenerFactory.cs | 20 ++ .../Utilities/JsonMessageParsers.cs | 12 - 18 files changed, 421 insertions(+), 198 deletions(-) create mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MentionedUser.cs create mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatListenerFactory.cs create mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs create mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/DotNetListenerFactory.cs delete mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/JsonMessageParsers.cs diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs index 2b7fdc0..8c79b82 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs @@ -92,7 +92,7 @@ public async Task TestLeaveChannel() Assert.IsNotNull(currentChatUser, "currentChatUser was null"); var channel = await chat.OLD_CreatePublicConversation(); - channel.Join(); + channel.OLD_Join(); await Task.Delay(3000); @@ -100,7 +100,7 @@ public async Task TestLeaveChannel() Assert.True(memberships.Memberships.Any(x => x.OLD_UserId == currentChatUser.Id), "Join failed, current user not found in channel memberships"); - channel.Leave(); + channel.OLD_Leave(); await Task.Delay(3000); @@ -113,7 +113,7 @@ public async Task TestLeaveChannel() public async Task TestGetUserSuggestions() { var channel = await chat.OLD_CreatePublicConversation("user_suggestions_test_channel"); - channel.Join(); + channel.OLD_Join(); await Task.Delay(5000); @@ -125,7 +125,7 @@ public async Task TestGetUserSuggestions() public async Task TestGetMemberships() { var channel = await chat.OLD_CreatePublicConversation("get_members_test_channel"); - channel.Join(); + channel.OLD_Join(); await Task.Delay(3500); var memberships = await channel.OLD_GetMemberships(); Assert.That(memberships.Memberships.Count, Is.GreaterThanOrEqualTo(1)); @@ -135,7 +135,7 @@ public async Task TestGetMemberships() public async Task TestStartTyping() { var channel = (await chat.OLD_CreateDirectConversation(talkUser, "sttc")).CreatedChannel; - channel.Join(); + channel.OLD_Join(); await Task.Delay(2500); channel.SetListeningForTyping(true); @@ -157,7 +157,7 @@ public async Task TestStartTyping() public async Task TestStopTyping() { var channel = (await chat.OLD_CreateDirectConversation(talkUser, "stop_typing_test_channel")).CreatedChannel; - channel.Join(); + channel.OLD_Join(); await Task.Delay(2500); channel.SetListeningForTyping(true); await Task.Delay(2500); @@ -182,7 +182,7 @@ public async Task TestStopTyping() public async Task TestStopTypingFromTimer() { var channel = (await chat.OLD_CreateDirectConversation(talkUser, "stop_typing_timeout_test_channel")).CreatedChannel; - channel.Join(); + channel.OLD_Join(); await Task.Delay(2500); channel.SetListeningForTyping(true); @@ -207,7 +207,7 @@ public async Task TestStopTypingFromTimer() public async Task TestPinMessage() { var channel = await chat.OLD_CreatePublicConversation("pin_message_test_channel_37"); - channel.Join(); + channel.OLD_Join(); await Task.Delay(3500); var receivedManualEvent = new ManualResetEvent(false); @@ -221,7 +221,7 @@ public async Task TestPinMessage() await Task.Delay(2000); - Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.MessageText == "message to pin"); + Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.OLD_MessageText == "message to pin"); receivedManualEvent.Set(); }; await channel.SendText("message to pin"); @@ -234,7 +234,7 @@ public async Task TestPinMessage() public async Task TestUnPinMessage() { var channel = await chat.OLD_CreatePublicConversation("unpin_message_test_channel"); - channel.Join(); + channel.OLD_Join(); await Task.Delay(3500); var receivedManualEvent = new ManualResetEvent(false); channel.OnMessageReceived += async message => @@ -243,7 +243,7 @@ public async Task TestUnPinMessage() await Task.Delay(2000); - Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.MessageText == "message to pin"); + Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.OLD_MessageText == "message to pin"); await channel.UnpinMessage(); await Task.Delay(2000); @@ -276,7 +276,7 @@ public async Task TestCreateMessageDraft() public async Task TestEmitUserMention() { var channel = await chat.OLD_CreatePublicConversation("user_mention_test_channel"); - channel.Join(); + channel.OLD_Join(); await Task.Delay(2500); var receivedManualEvent = new ManualResetEvent(false); user.SetListeningForMentionEvents(true); @@ -295,7 +295,7 @@ public async Task TestEmitUserMention() public async Task TestChannelIsPresent() { var someChannel = await chat.OLD_CreatePublicConversation(); - someChannel.Join(); + someChannel.OLD_Join(); await Task.Delay(4000); @@ -308,7 +308,7 @@ public async Task TestChannelIsPresent() public async Task TestChannelWhoIsPresent() { var someChannel = await chat.OLD_CreatePublicConversation(); - someChannel.Join(); + someChannel.OLD_Join(); await Task.Delay(4000); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs index 80cc7af..17f0864 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs @@ -24,14 +24,14 @@ public async Task Setup() { Assert.Fail(); } - channel.Join(); + channel.OLD_Join(); await Task.Delay(3500); } [TearDown] public async Task CleanUp() { - channel.Leave(); + channel.OLD_Leave(); await Task.Delay(3000); chat.Destroy(); await Task.Delay(3000); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs index ff96ce7..9b3cada 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -23,14 +23,14 @@ public async Task Setup() { Assert.Fail(); } - channel.Join(); + channel.OLD_Join(); await Task.Delay(3500); } [TearDown] public async Task CleanUp() { - channel.Leave(); + channel.OLD_Leave(); await Task.Delay(1000); chat.Destroy(); await Task.Delay(1000); @@ -53,7 +53,7 @@ public async Task TestGetCurrentUserMentions() var mentions = await chat.GetCurrentUserMentions("99999999999999999", "00000000000000000", 10); Assert.True(mentions != null); - Assert.True(mentions.Mentions.Any(x => x.ChannelId == channel.Id && x.Message.MessageText == messageContent)); + Assert.True(mentions.Mentions.Any(x => x.ChannelId == channel.Id && x.Message.OLD_MessageText == messageContent)); } [Test] @@ -123,10 +123,10 @@ public async Task TestForwardMessage() var forwardingChannel = await chat.OLD_CreatePublicConversation("forwarding_channel"); forwardingChannel.OnMessageReceived += message => { - Assert.True(message.MessageText == "message_to_forward"); + Assert.True(message.OLD_MessageText == "message_to_forward"); messageForwardReceivedManualEvent.Set(); }; - forwardingChannel.Join(); + forwardingChannel.OLD_Join(); await Task.Delay(2500); channel.OnMessageReceived += async message => { await chat.ForwardMessage(message, forwardingChannel); }; @@ -196,7 +196,7 @@ public async Task TestReadReceipts() return; } - otherChatChannel.Join(); + otherChatChannel.OLD_Join(); await Task.Delay(2500); otherChatChannel.SetListeningForReadReceiptsEvents(true); await Task.Delay(2500); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs index 6d0b67c..eea6cf8 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs @@ -25,14 +25,14 @@ public async Task Setup() Assert.Fail(); } - channel.Join(); + channel.OLD_Join(); await Task.Delay(3500); } [TearDown] public async Task CleanUp() { - channel.Leave(); + channel.OLD_Leave(); await Task.Delay(3000); chat.Destroy(); await Task.Delay(3000); @@ -108,7 +108,7 @@ public async Task TestInviteMultiple() public async Task TestLastRead() { var testChannel = await chat.OLD_CreatePublicConversation("last_read_test_channel_57"); - testChannel.Join(); + testChannel.OLD_Join(); await Task.Delay(4000); @@ -129,7 +129,7 @@ public async Task TestLastRead() await Task.Delay(7000); var lastTimeToken = membership.OLD_GetLastReadMessageTimeToken(); - Assert.True(lastTimeToken == message.TimeToken); + Assert.True(lastTimeToken == message.OLD_TimeToken); await membership.OLD_SetLastReadMessageTimeToken("99999999999999999"); await Task.Delay(3000); @@ -147,7 +147,7 @@ public async Task TestLastRead() public async Task TestUnreadMessagesCount() { var unreadChannel = await chat.OLD_CreatePublicConversation($"test_channel_{Guid.NewGuid()}"); - unreadChannel.Join(); + unreadChannel.OLD_Join(); await Task.Delay(3500); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs index 1b921b0..8cbe928 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs @@ -28,7 +28,7 @@ public async Task Setup() Assert.Fail(); } - channel.Join(); + channel.OLD_Join(); await Task.Delay(3000); if (!chat.OLD_TryGetUser("mock_user", out dummyUser)) @@ -231,7 +231,7 @@ public async Task TestSend() var successReset = new ManualResetEvent(false); channel.OnMessageReceived += message => { - Assert.True(message.MessageText == "draft_text"); + Assert.True(message.OLD_MessageText == "draft_text"); successReset.Set(); }; var messageDraft = channel.CreateMessageDraft(); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs index 1df5548..8fa1452 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs @@ -24,14 +24,14 @@ public async Task Setup() { Assert.Fail(); } - channel.Join(); + channel.OLD_Join(); await Task.Delay(3500); } [TearDown] public async Task CleanUp() { - channel.Leave(); + channel.OLD_Leave(); await Task.Delay(3000); chat.Destroy(); await Task.Delay(3000); @@ -44,8 +44,8 @@ public async Task TestSendAndReceive() channel.OnMessageReceived += message => { - Assert.True(message.MessageText == "Test message text"); - Assert.True(message.Type == PubnubChatMessageType.Text); + Assert.True(message.OLD_MessageText == "Test message text"); + Assert.True(message.OLD_Type == PubnubChatMessageType.Text); manualReceiveEvent.Set(); }; await channel.SendText("Test message text", new SendTextParams() @@ -61,11 +61,11 @@ public async Task TestReceivingMessageData() { var manualReceiveEvent = new ManualResetEvent(false); var testChannel = await chat.OLD_CreatePublicConversation("message_data_test_channel"); - testChannel.Join(); + testChannel.OLD_Join(); await Task.Delay(2500); testChannel.OnMessageReceived += async message => { - if (message.MessageText == "message_to_be_quoted") + if (message.OLD_MessageText == "message_to_be_quoted") { await testChannel.SendText("message_with_data", new SendTextParams() { @@ -73,11 +73,11 @@ public async Task TestReceivingMessageData() QuotedMessage = message }); } - else if (message.MessageText == "message_with_data") + else if (message.OLD_MessageText == "message_with_data") { - Assert.True(message.MentionedUsers.Any(x => x.Id == user.Id)); + Assert.True(message.OLD_MentionedUsers.Any(x => x.Id == user.Id)); Assert.True(message.TryGetQuotedMessage(out var quotedMessage) && - quotedMessage.MessageText == "message_to_be_quoted"); + quotedMessage.OLD_MessageText == "message_to_be_quoted"); manualReceiveEvent.Set(); } }; @@ -93,9 +93,9 @@ public async Task TestTryGetMessage() var manualReceiveEvent = new ManualResetEvent(false); channel.OnMessageReceived += message => { - if (message.ChannelId == channel.Id) + if (message.OLD_ChannelId == channel.Id) { - Assert.True(chat.TryGetMessage(channel.Id, message.TimeToken, out _)); + Assert.True(chat.TryGetMessage(channel.Id, message.OLD_TimeToken, out _)); manualReceiveEvent.Set(); } }; @@ -117,7 +117,7 @@ public async Task TestEditMessage() message.OnMessageUpdated += updatedMessage => { manualUpdatedEvent.Set(); - Assert.True(updatedMessage.MessageText == "new-text"); + Assert.True(updatedMessage.OLD_MessageText == "new-text"); }; await message.EditMessageText("new-text"); }; @@ -138,7 +138,7 @@ public async Task TestGetOriginalMessageText() await Task.Delay(2000); message.OnMessageUpdated += updatedMessage => { - originalTextAfterUpdate = updatedMessage.OriginalMessageText; + originalTextAfterUpdate = updatedMessage.OLD_OriginalMessageText; manualUpdatedEvent.Set(); }; await message.EditMessageText("new-text"); @@ -163,7 +163,7 @@ public async Task TestDeleteMessage() await Task.Delay(2000); - Assert.True(message.IsDeleted); + Assert.True(message.OLD_IsDeleted); manualReceivedEvent.Set(); }; await channel.SendText("something"); @@ -179,16 +179,16 @@ public async Task TestRestoreMessage() channel.OnMessageReceived += async message => { await message.Delete(true); - Assert.True(message.IsDeleted); + Assert.True(message.OLD_IsDeleted); await Task.Delay(4000); - Assert.True(message.IsDeleted); + Assert.True(message.OLD_IsDeleted); await message.Restore(); await Task.Delay(4000); - Assert.False(message.IsDeleted); + Assert.False(message.OLD_IsDeleted); manualReceivedEvent.Set(); }; await channel.SendText("some text here ladi ladi la"); @@ -201,7 +201,7 @@ public async Task TestRestoreMessage() public async Task TestPinMessage() { var pinTestChannel = await chat.OLD_CreatePublicConversation(); - pinTestChannel.Join(); + pinTestChannel.OLD_Join(); await Task.Delay(2500); pinTestChannel.SetListeningForUpdates(true); await Task.Delay(3000); @@ -214,7 +214,7 @@ public async Task TestPinMessage() await Task.Delay(3000); var got = pinTestChannel.TryGetPinnedMessage(out var pinnedMessage); - Assert.True(got && pinnedMessage.MessageText == "message to pin"); + Assert.True(got && pinnedMessage.OLD_MessageText == "message to pin"); manualReceivedEvent.Set(); }; await pinTestChannel.SendText("message to pin"); @@ -235,7 +235,7 @@ public async Task TestMessageReactions() var has = message.HasUserReaction("happy"); Assert.True(has); - var reactions = message.Reactions; + var reactions = message.OLD_Reactions; Assert.True(reactions.Count == 1 && reactions.Any(x => x.Value == "happy")); manualReset.Set(); }; @@ -273,7 +273,7 @@ public async Task TestCreateThread() { message.SetListeningForUpdates(true); var thread = await message.CreateThread(); - thread.Join(); + thread.OLD_Join(); await Task.Delay(3500); await thread.SendText("thread_init_text"); await Task.Delay(5000); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs index 7ab1409..c9115e6 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs @@ -25,14 +25,14 @@ public async Task Setup() { Assert.Fail(); } - channel.Join(); + channel.OLD_Join(); await Task.Delay(3500); } [TearDown] public async Task CleanUp() { - channel.Leave(); + channel.OLD_Leave(); await Task.Delay(3000); await channel.Delete(); chat.Destroy(); @@ -47,7 +47,7 @@ public async Task TestGetThreadHistory() { message.SetListeningForUpdates(true); var thread = await message.CreateThread(); - thread.Join(); + thread.OLD_Join(); await Task.Delay(5000); @@ -58,7 +58,7 @@ public async Task TestGetThreadHistory() await Task.Delay(10000); var history = await thread.GetThreadHistory("99999999999999999", "00000000000000000", 3); - Assert.True(history.Count == 3 && history.Any(x => x.MessageText == "one")); + Assert.True(history.Count == 3 && history.Any(x => x.OLD_MessageText == "one")); historyReadReset.Set(); }; await channel.SendText("thread_start_message"); @@ -74,7 +74,7 @@ public async Task TestThreadChannelParentChannelPinning() { message.SetListeningForUpdates(true); var thread = await message.CreateThread(); - thread.Join(); + thread.OLD_Join(); await thread.SendText("thread init message"); await Task.Delay(7000); @@ -86,7 +86,7 @@ public async Task TestThreadChannelParentChannelPinning() await Task.Delay(7000); var hasPinned = channel.TryGetPinnedMessage(out var pinnedMessage); - var correctText = hasPinned && pinnedMessage.MessageText == "thread init message"; + var correctText = hasPinned && pinnedMessage.OLD_MessageText == "thread init message"; Assert.True(hasPinned && correctText); await thread.UnPinMessageFromParentChannel(); @@ -107,7 +107,7 @@ public async Task TestThreadChannelEmitUserMention() channel.OnMessageReceived += async message => { var thread = await message.CreateThread(); - thread.Join(); + thread.OLD_Join(); await Task.Delay(2500); user.SetListeningForMentionEvents(true); await Task.Delay(2500); @@ -131,7 +131,7 @@ public async Task TestThreadMessageParentChannelPinning() { message.SetListeningForUpdates(true); var thread = await message.CreateThread(); - thread.Join(); + thread.OLD_Join(); await Task.Delay(3500); @@ -147,7 +147,7 @@ public async Task TestThreadMessageParentChannelPinning() await Task.Delay(5000); - Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.MessageText == threadMessage.MessageText); + Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.OLD_MessageText == threadMessage.OLD_MessageText); await threadMessage.UnPinMessageFromParentChannel(); @@ -169,7 +169,7 @@ public async Task TestThreadMessageUpdate() { message.SetListeningForUpdates(true); var thread = await message.CreateThread(); - thread.Join(); + thread.OLD_Join(); await Task.Delay(3000); @@ -185,7 +185,7 @@ public async Task TestThreadMessageUpdate() threadMessage.SetListeningForUpdates(true); threadMessage.OnThreadMessageUpdated += updatedThreadMessage => { - Assert.True(updatedThreadMessage.MessageText == "new_text"); + Assert.True(updatedThreadMessage.OLD_MessageText == "new_text"); messageUpdatedReset.Set(); }; await threadMessage.EditMessageText("new_text"); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs index 59af7ea..a6eee2f 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs @@ -25,14 +25,14 @@ public async Task Setup() { Assert.Fail(); } - channel.Join(); + channel.OLD_Join(); await Task.Delay(3500); } [TearDown] public async Task CleanUp() { - channel.Leave(); + channel.OLD_Leave(); await Task.Delay(3000); chat.Destroy(); await Task.Delay(3000); @@ -109,7 +109,7 @@ public async Task TestUserDelete() public async Task TestUserWherePresent() { var someChannel = await chat.OLD_CreatePublicConversation(); - someChannel.Join(); + someChannel.OLD_Join(); await Task.Delay(4000); @@ -122,7 +122,7 @@ public async Task TestUserWherePresent() public async Task TestUserIsPresentOn() { var someChannel = await chat.OLD_CreatePublicConversation(); - someChannel.Join(); + someChannel.OLD_Join(); await Task.Delay(4000); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index 255791e..9b10576 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -297,7 +297,7 @@ public string OLD_Type private IntPtr presenceListeningHandle; protected IntPtr connectionHandle; - protected Subscription subscription; + protected Subscription? subscription; private Dictionary typingIndicators = new(); @@ -663,7 +663,7 @@ public MessageDraft CreateMessageDraft(UserSuggestionSource userSuggestionSource CUtilities.CheckCFunctionResult(draftPointer); return new MessageDraft(draftPointer); } - + /// /// Disconnects from the channel. /// @@ -684,14 +684,11 @@ public MessageDraft CreateMessageDraft(UserSuggestionSource userSuggestionSource /// public void Disconnect() { - if (connectionHandle == IntPtr.Zero || pointer == IntPtr.Zero) + if (subscription == null) { return; } - - CUtilities.CheckCFunctionResult(pn_channel_disconnect(pointer)); - pn_callback_handle_dispose(connectionHandle); - connectionHandle = IntPtr.Zero; + subscription.Unsubscribe(); } /// @@ -716,24 +713,18 @@ public void Disconnect() /// public async void Leave() { - if (connectionHandle == IntPtr.Zero || pointer == IntPtr.Zero) + Disconnect(); + var currentUserId = chat.PubnubInstance.GetCurrentUserId(); + var remove = await chat.PubnubInstance.RemoveMemberships().Uuid(currentUserId).Channels(new List() { Id }) + .ExecuteAsync(); + if (remove.Status.Error) { + chat.PubnubInstance.PNConfig.Logger?.Error($"Error when trying to leave channel \"{Id}\": {remove.Status.ErrorData.Information}"); return; } - - var connectionHandleCopy = connectionHandle; - connectionHandle = IntPtr.Zero; - CUtilities.CheckCFunctionResult(await Task.Run(() => - { - if (pointer == IntPtr.Zero) - { - return 0; - } - - pn_channel_leave(pointer); - pn_callback_handle_dispose(connectionHandleCopy); - return 0; - })); + + //TODO: wrappers rethink + chat.membershipWrappers.Remove(currentUserId + Id); } /// @@ -756,14 +747,23 @@ public async void Leave() /// /// /// - public async void Connect() + public void Connect() { - if (connectionHandle != IntPtr.Zero) + if (subscription != null) { return; } - - connectionHandle = await SetListening(connectionHandle, true, () => pn_channel_connect(pointer)); + subscription = chat.PubnubInstance.Channel(Id).Subscription(SubscriptionOptions.ReceivePresenceEvents); + subscription.AddListener(chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseMessageResult(chat, m, out var message)) + { + chat.RegisterMessage(message); + OnMessageReceived?.Invoke(message); + } + })); + subscription.Subscribe(); } /// @@ -789,6 +789,93 @@ public async void Connect() /// /// public async void Join(ChatMembershipData? membershipData = null) + { + membershipData ??= new ChatMembershipData(); + var currentUserId = chat.PubnubInstance.GetCurrentUserId(); + var response = 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(); + if (response.Status.Error) + { + chat.PubnubInstance.PNConfig.Logger?.Error($"Error when trying to Join() to channel \"{Id}\": {response.Status.ErrorData.Information}"); + return; + } + + //TODO: wrappers rethink + if (chat.membershipWrappers.TryGetValue(currentUserId + Id, out var existingHostMembership)) + { + existingHostMembership.UpdateLocalData(membershipData); + } + else + { + var joinMembership = new Membership(chat, currentUserId, Id, membershipData); + chat.membershipWrappers.Add(joinMembership.Id, joinMembership); + } + + Connect(); + } + + public void OLD_Disconnect() + { + if (connectionHandle == IntPtr.Zero || pointer == IntPtr.Zero) + { + return; + } + + CUtilities.CheckCFunctionResult(pn_channel_disconnect(pointer)); + pn_callback_handle_dispose(connectionHandle); + connectionHandle = IntPtr.Zero; + } + + public async void OLD_Leave() + { + if (connectionHandle == IntPtr.Zero || pointer == IntPtr.Zero) + { + return; + } + + var connectionHandleCopy = connectionHandle; + connectionHandle = IntPtr.Zero; + CUtilities.CheckCFunctionResult(await Task.Run(() => + { + if (pointer == IntPtr.Zero) + { + return 0; + } + + pn_channel_leave(pointer); + pn_callback_handle_dispose(connectionHandleCopy); + return 0; + })); + } + + public async void OLD_Connect() + { + if (connectionHandle != IntPtr.Zero) + { + return; + } + + connectionHandle = await SetListening(connectionHandle, true, () => pn_channel_connect(pointer)); + } + + public async void OLD_Join(ChatMembershipData? membershipData = null) { if (connectionHandle != IntPtr.Zero) { @@ -1150,7 +1237,7 @@ protected override async Task CleanupConnectionHandles() () => pn_channel_get_typing(pointer)); presenceListeningHandle = await SetListening(presenceListeningHandle, false, () => pn_channel_stream_presence(pointer)); - Disconnect(); + OLD_Disconnect(); } protected override void DisposePointer() diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index ebaf5f5..b7e3a51 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -309,11 +309,13 @@ string membership_status internal IntPtr Pointer => chatPointer; private Dictionary channelWrappers = new(); private Dictionary userWrappers = new(); - private Dictionary membershipWrappers = new(); + //TODO: wrappers rethink + internal Dictionary membershipWrappers = new(); private Dictionary messageWrappers = new(); private bool fetchUpdates = true; - public Pubnub PubnubInstance { get; private set; } + public Pubnub PubnubInstance { get; } + internal ChatListenerFactory ListenerFactory { get; } public event Action OnAnyEvent; @@ -337,7 +339,7 @@ internal Chat(PubnubChatConfig config) Config = config; ChatAccessManager = new ChatAccessManager(chatPointer); } - + /// /// Initializes a new instance of the class. /// @@ -346,12 +348,14 @@ internal Chat(PubnubChatConfig config) /// /// 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. /// /// The constructor initializes the Chat object with a new Pubnub instance. /// - public Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig) + public Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatListenerFactory? listenerFactory = null) { PubnubInstance = new Pubnub(pubnubConfig); + ListenerFactory = listenerFactory ?? new DotNetListenerFactory(); Config = chatConfig; ChatAccessManager = new ChatAccessManager(chatPointer); } @@ -364,19 +368,21 @@ public Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig) /// /// Config with Chat specific parameters /// An already initialised instance of Pubnub + /// /// Optional injectable listener factory, used in Unity to allow for dispatching Chat callbacks on main thread. /// /// The constructor initializes the Chat object with the provided existing Pubnub instance. /// - public Chat(PubnubChatConfig chatConfig, Pubnub pubnub) + public Chat(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? listenerFactory = null) { Config = chatConfig; PubnubInstance = pubnub; + ListenerFactory = listenerFactory ?? new DotNetListenerFactory(); ChatAccessManager = new ChatAccessManager(chatPointer); } #region Updates handling - //TODO: cancellation token? + //TODO: REMOVE internal async Task FetchUpdatesLoop() { while (fetchUpdates) @@ -395,6 +401,8 @@ internal async Task FetchUpdatesLoop() } } + + //TODO: REMOVE internal void ParseJsonUpdatePointers(string jsonString) { if (string.IsNullOrEmpty(jsonString) || jsonString == "[]") @@ -2036,6 +2044,12 @@ internal bool OLD_TryGetMembership(string membershipId, IntPtr membershipPointer #region Messages + //TODO: wrappers rethink + internal void RegisterMessage(Message message) + { + messageWrappers.TryAdd(message.Id, message); + } + public async Task GetMessageReportsHistory(string channelId, string startTimeToken, string endTimeToken, int count) { 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..2f4f9b2 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MentionedUser.cs @@ -0,0 +1,8 @@ +namespace PubnubChatApi.Entities.Data +{ + public struct MentionedUser + { + public string Id; + public string Name; + } +} \ 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 737a302..cdb883c 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; @@ -77,37 +78,34 @@ public class Message : UniqueChatEntity [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 virtual string OLD_MessageText { get { @@ -117,10 +115,7 @@ public virtual string MessageText } } - /// - /// The original, un-edited text of the message. - /// - public virtual string OriginalMessageText + public virtual string OLD_OriginalMessageText { get { @@ -130,14 +125,7 @@ public virtual string OriginalMessageText } } - /// - /// 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 virtual string TimeToken + public virtual string OLD_TimeToken { get { @@ -147,13 +135,7 @@ public virtual string TimeToken } } - /// - /// 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 virtual string ChannelId + public virtual string OLD_ChannelId { get { @@ -163,14 +145,7 @@ public virtual string ChannelId } } - /// - /// 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 virtual string UserId + public virtual string OLD_UserId { get { @@ -180,14 +155,7 @@ public virtual string UserId } } - /// - /// 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 virtual string Meta + public virtual string OLD_Meta { get { @@ -197,15 +165,7 @@ public virtual string Meta } } - /// - /// 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 virtual bool IsDeleted + public virtual bool OLD_IsDeleted { get { @@ -215,7 +175,7 @@ public virtual bool IsDeleted } } - public virtual List MentionedUsers + public virtual List OLD_MentionedUsers { get { @@ -226,16 +186,18 @@ public virtual List MentionedUsers { 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 virtual List ReferencedChannels + + public virtual List OLD_ReferencedChannels { get { @@ -246,16 +208,18 @@ public virtual List ReferencedChannels { 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 virtual List TextLinks + + public virtual List OLD_TextLinks { get { @@ -266,16 +230,18 @@ public virtual List TextLinks { return new List(); } + var textLinks = JsonConvert.DeserializeObject>>(jsonString); if (textLinks == null || !textLinks.TryGetValue("value", out var links) || links == null) { return new List(); } + return links; } } - protected List DeserializeMessageActions(string json) + protected List OLD_DeserializeMessageActions(string json) { var reactions = new List(); if (CUtilities.IsValidJson(json)) @@ -283,29 +249,117 @@ protected List DeserializeMessageActions(string json) reactions = JsonConvert.DeserializeObject>(json); reactions ??= new List(); } + return reactions; } - - public virtual List MessageActions + + public virtual List OLD_MessageActions { get { var buffer = new StringBuilder(4096); CUtilities.CheckCFunctionResult(pn_message_get_data_message_actions(pointer, buffer)); - return DeserializeMessageActions(buffer.ToString()); + return OLD_DeserializeMessageActions(buffer.ToString()); } } - - public virtual List Reactions + + public virtual List OLD_Reactions { get { var buffer = new StringBuilder(4096); CUtilities.CheckCFunctionResult(pn_message_get_reactions(pointer, buffer)); - return DeserializeMessageActions(buffer.ToString()); + return OLD_DeserializeMessageActions(buffer.ToString()); } } + public virtual PubnubChatMessageType OLD_Type => (PubnubChatMessageType)pn_message_get_data_type(pointer); + + protected Chat chat; + + /// + /// 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 { + 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 virtual 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 virtual 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 virtual 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 virtual 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; } + + /// + /// 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 virtual bool IsDeleted => MessageActions.Any(x => x.Type == PubnubMessageActionType.Deleted); + + public virtual List MentionedUsers { + get + { + var mentioned = new List(); + if (Meta.TryGetValue("mentionedUsers", out var rawMentionedUsers)) + { + //TODO: might break + var deserialized = chat.PubnubInstance.JsonPluggableLibrary.DeserializeToObject>(rawMentionedUsers.ToString()); + mentioned.AddRange(deserialized); + } + return mentioned; + } + } + + public virtual List MessageActions { get; internal set; } + + public virtual List Reactions => + MessageActions.Where(x => x.Type == PubnubMessageActionType.Reaction).ToList(); + /// /// The data type of the message. /// @@ -314,9 +368,8 @@ public virtual List Reactions /// /// /// - public virtual PubnubChatMessageType Type => (PubnubChatMessageType)pn_message_get_data_type(pointer); + public virtual PubnubChatMessageType Type { get; protected set; } - protected Chat chat; /// /// Event that is triggered when the message is updated. @@ -342,7 +395,17 @@ internal Message(Chat chat, IntPtr messagePointer, string timeToken) : base(mess { this.chat = chat; } - + + internal Message(Chat chat, string timeToken,string originalMessageText, string channelId, string userId, Dictionary meta) : base(timeToken) + { + this.chat = chat; + TimeToken = timeToken; + OriginalMessageText = originalMessageText; + ChannelId = channelId; + UserId = userId; + Meta = meta; + } + protected override IntPtr StreamUpdates() { return pn_message_stream_updates(pointer); @@ -361,7 +424,7 @@ internal static string GetChannelIdFromMessagePtr(IntPtr messagePointer) pn_message_get_data_channel_id(messagePointer, buffer); return buffer.ToString(); } - + internal override void UpdateWithPartialPtr(IntPtr partialPointer) { var newFullPointer = pn_message_update_with_base_message(partialPointer, pointer); @@ -405,6 +468,7 @@ public virtual bool TryGetQuotedMessage(out Message quotedMessage) quotedMessage = null; return false; } + return chat.TryGetMessage(quotedMessagePointer, out quotedMessage); } @@ -499,7 +563,7 @@ public virtual async Task Restore() /// message.DeleteMessage(); /// /// - /// + /// /// public virtual async Task Delete(bool soft) { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs index 8957743..9cd1bb4 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -72,7 +72,7 @@ public Message ParentMessage internal static string MessageToThreadChannelId(Message message) { - return $"PUBNUB_INTERNAL_THREAD_{message.ChannelId}_{message.Id}"; + return $"PUBNUB_INTERNAL_THREAD_{message.OLD_ChannelId}_{message.Id}"; } internal ThreadChannel(Chat chat, Message sourceMessage, IntPtr channelPointer) : base(chat, diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs index 468e0de..b4c2df3 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -102,7 +102,7 @@ private static extern IntPtr pn_thread_message_consume_and_upgrade( /// This is the main content of the message. It can be any text that the user wants to send. /// /// - public override string MessageText + public override string OLD_MessageText { get { @@ -115,7 +115,7 @@ public override string MessageText /// /// The original, un-edited text of the thread message. /// - public override string OriginalMessageText + public override string OLD_OriginalMessageText { get { @@ -132,7 +132,7 @@ public override string OriginalMessageText /// It is used to identify the message in the chat. /// /// - public override string TimeToken + public override string OLD_TimeToken { get { @@ -148,7 +148,7 @@ public override string TimeToken /// This is the ID of the channel that the message was sent to. /// /// - public override string ChannelId + public override string OLD_ChannelId { get { @@ -165,7 +165,7 @@ public override string ChannelId /// Do not confuse this with the username of the user. /// /// - public override string UserId + public override string OLD_UserId { get { @@ -175,7 +175,7 @@ public override string UserId } } - public override PubnubChatMessageType Type => (PubnubChatMessageType)pn_thread_message_get_data_type(pointer); + public override PubnubChatMessageType OLD_Type => (PubnubChatMessageType)pn_thread_message_get_data_type(pointer); /// /// The metadata of the message. @@ -184,7 +184,7 @@ public override string UserId /// It can be used to store additional information about the message. /// /// - public override string Meta + public override string OLD_Meta { get { @@ -202,7 +202,7 @@ public override string Meta /// It means that all the deletions are soft deletions. /// /// - public override bool IsDeleted + public override bool OLD_IsDeleted { get { @@ -212,7 +212,7 @@ public override bool IsDeleted } } - public override List MentionedUsers + public override List OLD_MentionedUsers { get { @@ -232,7 +232,7 @@ public override List MentionedUsers } } - public override List ReferencedChannels + public override List OLD_ReferencedChannels { get { @@ -252,7 +252,7 @@ public override List ReferencedChannels } } - public override List TextLinks + public override List OLD_TextLinks { get { @@ -272,23 +272,23 @@ public override List TextLinks } } - public override List MessageActions + public override List OLD_MessageActions { get { var buffer = new StringBuilder(4096); CUtilities.CheckCFunctionResult(pn_thread_message_get_data_message_actions(pointer, buffer)); - return DeserializeMessageActions(buffer.ToString()); + return OLD_DeserializeMessageActions(buffer.ToString()); } } - public override List Reactions + public override List OLD_Reactions { get { var buffer = new StringBuilder(4096); CUtilities.CheckCFunctionResult(pn_thread_message_get_reactions(pointer, buffer)); - return DeserializeMessageActions(buffer.ToString()); + return OLD_DeserializeMessageActions(buffer.ToString()); } } 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..71152ce --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatListenerFactory.cs @@ -0,0 +1,16 @@ +using System; +using PubnubApi; + +namespace PubnubChatApi.Utilities +{ + 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..e692f24 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using PubnubApi; +using PubNubChatAPI.Entities; + +namespace PubnubChatApi.Utilities +{ + internal static class ChatParsers + { + internal static bool TryParseMessageResult(Chat chat, PNMessageResult messageResult, out Message message) + { + try + { + //TODO: don't know if UserMetadata is a JSON string or a Dict so we'll see if this breaks I guess? + var meta = messageResult.UserMetadata as Dictionary; + message = new Message(chat, messageResult.Timetoken.ToString(), messageResult.Message.ToString(), messageResult.Channel, messageResult.Publisher, meta); + return true; + } + catch (Exception e) + { + message = null; + return false; + } + } + } +} \ 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..6d2239f --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/DotNetListenerFactory.cs @@ -0,0 +1,20 @@ +using System; +using PubnubApi; + +namespace PubnubChatApi.Utilities +{ + 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/JsonMessageParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/JsonMessageParsers.cs deleted file mode 100644 index c706824..0000000 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/JsonMessageParsers.cs +++ /dev/null @@ -1,12 +0,0 @@ -using PubnubApi; - -namespace PubnubChatApi.Utilities -{ - internal static class JsonMessageParsers - { - /*internal static bool IsMessage(Pubnub pubnub, string payload) - { - pubnub.JsonPluggableLibrary.DeserializeToDictionaryOfObject(payload)["type"] - }*/ - } -} \ No newline at end of file From fb354f32e7a014d671390614d8e053e7e9650b20 Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Tue, 22 Jul 2025 15:23:09 +0200 Subject: [PATCH 07/28] fully remove CPP interop logic for easier migration --- .../PubNubChatApi.Tests/ChannelTests.cs | 101 +- .../PubNubChatApi.Tests/ChatEventTests.cs | 20 +- .../PubNubChatApi.Tests/ChatTests.cs | 74 +- .../PubNubChatApi.Tests/MembershipTests.cs | 71 +- .../PubNubChatApi.Tests/MessageDraftTests.cs | 28 +- .../PubNubChatApi.Tests/MessageTests.cs | 62 +- .../PubNubChatApi.Tests/RestrictionsTests.cs | 15 +- .../PubNubChatApi.Tests/TestUtils.cs | 4 +- .../PubNubChatApi.Tests/ThreadsTests.cs | 38 +- .../PubNubChatApi.Tests/UserTests.cs | 56 +- .../PubnubChatApi/Entities/Base/ChatEntity.cs | 87 +- .../Entities/Base/UniqueChatEntity.cs | 5 - .../PubnubChatApi/Entities/Channel.cs | 497 +------ .../PubnubChatApi/Entities/Chat.cs | 1292 +---------------- .../Entities/ChatAccessManager.cs | 36 +- .../Entities/Data/ChannelsResponseWrapper.cs | 14 - .../Entities/Data/ChatChannelData.cs | 29 +- .../Entities/Data/ChatMembershipData.cs | 1 - .../Entities/Data/ChatUserData.cs | 8 +- .../Data/MarkMessagesAsReadWrapper.cs | 16 - .../Entities/Data/MembersResponseWrapper.cs | 20 - .../Entities/Data/PubnubChatConfig.cs | 27 - .../Entities/Data/UnreadMessageWrapper.cs | 14 - .../Entities/Data/UserMentionData.cs | 21 - .../Entities/Data/UserMentionsWrapper.cs | 15 - .../Entities/Data/UsersResponseWrapper.cs | 20 - .../PubnubChatApi/Entities/Membership.cs | 153 +- .../PubnubChatApi/Entities/Message.cs | 340 +---- .../PubnubChatApi/Entities/MessageDraft.cs | 100 +- .../PubnubChatApi/Entities/ThreadChannel.cs | 112 +- .../PubnubChatApi/Entities/ThreadMessage.cs | 372 +---- .../PubnubChatApi/Entities/User.cs | 252 +--- .../PubnubChatApi/PubnubChatApi.csproj | 6 - .../PubnubChatApi/Utilities/CUtilities.cs | 55 - .../PubnubChatApi/Utilities/PointerParsers.cs | 103 -- .../PubnubChatApi/pubnub-chat.dll | Bin 1172992 -> 0 bytes 36 files changed, 386 insertions(+), 3678 deletions(-) delete mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/CUtilities.cs delete mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs delete mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/pubnub-chat.dll diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs index 8c79b82..d3c7232 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; @@ -14,16 +15,16 @@ public class ChannelTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "ctuuuuuuuuuuuuuuuuuuuuuuuuuuuuu") - ); - if (!chat.OLD_TryGetCurrentUser(out user)) + chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("ctuuuuuuuuuuuuuuuuuuuuuuuuuuuuu")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + }); + if (!chat.TryGetCurrentUser(out user)) { Assert.Fail(); } - await user.OLD_Update(new ChatUserData() + await user.Update(new ChatUserData() { Username = "Testificate" }); @@ -40,7 +41,7 @@ public async Task CleanUp() [Test] public async Task TestUpdateChannel() { - var channel = await chat.OLD_CreatePublicConversation(); + var channel = await chat.CreatePublicConversation(); channel.SetListeningForUpdates(true); await Task.Delay(3000); @@ -49,18 +50,18 @@ public async Task TestUpdateChannel() var updatedData = new ChatChannelData() { ChannelDescription = "some description", - ChannelCustomDataJson = "{\"key\":\"value\"}", + ChannelCustomData = new Dictionary(){{"key", "value"}}, ChannelName = "some name", ChannelStatus = "yes", ChannelType = "sometype" }; channel.OnChannelUpdate += updatedChannel => { - Assert.True(updatedChannel.OLD_Description == updatedData.ChannelDescription, "updatedChannel.Description != updatedData.ChannelDescription"); - Assert.True(updatedChannel.OLD_CustomDataJson == updatedData.ChannelCustomDataJson, "updatedChannel.CustomDataJson != updatedData.ChannelCustomDataJson"); - Assert.True(updatedChannel.OLD_Name == updatedData.ChannelName, "updatedChannel.Name != updatedData.ChannelDescription"); - Assert.True(updatedChannel.OLD_Status == updatedData.ChannelStatus, "updatedChannel.Status != updatedData.ChannelStatus"); - Assert.True(updatedChannel.OLD_Type == updatedData.ChannelType, "updatedChannel.Type != updatedData.ChannelType"); + Assert.True(updatedChannel.Description == updatedData.ChannelDescription, "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.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"); updateReset.Set(); }; await channel.Update(updatedData); @@ -71,71 +72,71 @@ public async Task TestUpdateChannel() [Test] public async Task TestDeleteChannel() { - var channel = await chat.OLD_CreatePublicConversation(); + var channel = await chat.CreatePublicConversation(); await Task.Delay(3000); - Assert.True(chat.OLD_TryGetChannel(channel.Id, out _), "Couldn't fetch created channel from chat"); + Assert.True(chat.TryGetChannel(channel.Id, out _), "Couldn't fetch created channel from chat"); await channel.Delete(); await Task.Delay(3000); - Assert.False(chat.OLD_TryGetChannel(channel.Id, out _), "Fetched the supposedly-deleted channel from chat"); + Assert.False(chat.TryGetChannel(channel.Id, out _), "Fetched the supposedly-deleted channel from chat"); } [Test] public async Task TestLeaveChannel() { - var currentChatUser = await chat.OLD_GetCurrentUserAsync(); + var currentChatUser = await chat.GetCurrentUserAsync(); Assert.IsNotNull(currentChatUser, "currentChatUser was null"); - var channel = await chat.OLD_CreatePublicConversation(); - channel.OLD_Join(); + var channel = await chat.CreatePublicConversation(); + channel.Join(); await Task.Delay(3000); - var memberships = await channel.OLD_GetMemberships(); + var memberships = await channel.GetMemberships(); - Assert.True(memberships.Memberships.Any(x => x.OLD_UserId == currentChatUser.Id), "Join failed, current user not found in channel memberships"); + Assert.True(memberships.Memberships.Any(x => x.UserId == currentChatUser.Id), "Join failed, current user not found in channel memberships"); - channel.OLD_Leave(); + channel.Leave(); await Task.Delay(3000); - memberships = await channel.OLD_GetMemberships(); + memberships = await channel.GetMemberships(); - Assert.False(memberships.Memberships.Any(x => x.OLD_UserId == currentChatUser.Id), "Leave failed, current user found in channel memberships"); + Assert.False(memberships.Memberships.Any(x => x.UserId == currentChatUser.Id), "Leave failed, current user found in channel memberships"); } [Test] public async Task TestGetUserSuggestions() { - var channel = await chat.OLD_CreatePublicConversation("user_suggestions_test_channel"); - channel.OLD_Join(); + var channel = await chat.CreatePublicConversation("user_suggestions_test_channel"); + channel.Join(); await Task.Delay(5000); var suggestions = await channel.GetUserSuggestions("@Test"); - Assert.True(suggestions.Any(x => x.OLD_UserId == user.Id)); + Assert.True(suggestions.Any(x => x.UserId == user.Id)); } [Test] public async Task TestGetMemberships() { - var channel = await chat.OLD_CreatePublicConversation("get_members_test_channel"); - channel.OLD_Join(); + var channel = await chat.CreatePublicConversation("get_members_test_channel"); + channel.Join(); await Task.Delay(3500); - var memberships = await channel.OLD_GetMemberships(); + var memberships = await channel.GetMemberships(); Assert.That(memberships.Memberships.Count, Is.GreaterThanOrEqualTo(1)); } [Test] public async Task TestStartTyping() { - var channel = (await chat.OLD_CreateDirectConversation(talkUser, "sttc")).CreatedChannel; - channel.OLD_Join(); + var channel = (await chat.CreateDirectConversation(talkUser, "sttc")).CreatedChannel; + channel.Join(); await Task.Delay(2500); channel.SetListeningForTyping(true); @@ -156,8 +157,8 @@ public async Task TestStartTyping() [Test] public async Task TestStopTyping() { - var channel = (await chat.OLD_CreateDirectConversation(talkUser, "stop_typing_test_channel")).CreatedChannel; - channel.OLD_Join(); + var channel = (await chat.CreateDirectConversation(talkUser, "stop_typing_test_channel")).CreatedChannel; + channel.Join(); await Task.Delay(2500); channel.SetListeningForTyping(true); await Task.Delay(2500); @@ -181,8 +182,8 @@ public async Task TestStopTyping() [Test] public async Task TestStopTypingFromTimer() { - var channel = (await chat.OLD_CreateDirectConversation(talkUser, "stop_typing_timeout_test_channel")).CreatedChannel; - channel.OLD_Join(); + var channel = (await chat.CreateDirectConversation(talkUser, "stop_typing_timeout_test_channel")).CreatedChannel; + channel.Join(); await Task.Delay(2500); channel.SetListeningForTyping(true); @@ -206,8 +207,8 @@ public async Task TestStopTypingFromTimer() [Test] public async Task TestPinMessage() { - var channel = await chat.OLD_CreatePublicConversation("pin_message_test_channel_37"); - channel.OLD_Join(); + var channel = await chat.CreatePublicConversation("pin_message_test_channel_37"); + channel.Join(); await Task.Delay(3500); var receivedManualEvent = new ManualResetEvent(false); @@ -221,7 +222,7 @@ public async Task TestPinMessage() await Task.Delay(2000); - Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.OLD_MessageText == "message to pin"); + Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.MessageText == "message to pin"); receivedManualEvent.Set(); }; await channel.SendText("message to pin"); @@ -233,8 +234,8 @@ public async Task TestPinMessage() [Test] public async Task TestUnPinMessage() { - var channel = await chat.OLD_CreatePublicConversation("unpin_message_test_channel"); - channel.OLD_Join(); + var channel = await chat.CreatePublicConversation("unpin_message_test_channel"); + channel.Join(); await Task.Delay(3500); var receivedManualEvent = new ManualResetEvent(false); channel.OnMessageReceived += async message => @@ -243,7 +244,7 @@ public async Task TestUnPinMessage() await Task.Delay(2000); - Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.OLD_MessageText == "message to pin"); + Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.MessageText == "message to pin"); await channel.UnpinMessage(); await Task.Delay(2000); @@ -260,7 +261,7 @@ public async Task TestUnPinMessage() [Test] public async Task TestCreateMessageDraft() { - var channel = await chat.OLD_CreatePublicConversation("message_draft_test_channel"); + var channel = await chat.CreatePublicConversation("message_draft_test_channel"); try { var draft = channel.CreateMessageDraft(); @@ -275,8 +276,8 @@ public async Task TestCreateMessageDraft() [Test] public async Task TestEmitUserMention() { - var channel = await chat.OLD_CreatePublicConversation("user_mention_test_channel"); - channel.OLD_Join(); + var channel = await chat.CreatePublicConversation("user_mention_test_channel"); + channel.Join(); await Task.Delay(2500); var receivedManualEvent = new ManualResetEvent(false); user.SetListeningForMentionEvents(true); @@ -294,8 +295,8 @@ public async Task TestEmitUserMention() [Test] public async Task TestChannelIsPresent() { - var someChannel = await chat.OLD_CreatePublicConversation(); - someChannel.OLD_Join(); + var someChannel = await chat.CreatePublicConversation(); + someChannel.Join(); await Task.Delay(4000); @@ -307,8 +308,8 @@ public async Task TestChannelIsPresent() [Test] public async Task TestChannelWhoIsPresent() { - var someChannel = await chat.OLD_CreatePublicConversation(); - someChannel.OLD_Join(); + var someChannel = await chat.CreatePublicConversation(); + someChannel.Join(); await Task.Delay(4000); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs index 17f0864..f406952 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs @@ -1,6 +1,8 @@ using System.Diagnostics; +using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; +using Channel = PubNubChatAPI.Entities.Channel; namespace PubNubChatApi.Tests; @@ -14,24 +16,24 @@ public class ChatEventTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "event_tests_user") - ); - channel = await chat.OLD_CreatePublicConversation("event_tests_channel"); - if (!chat.OLD_TryGetCurrentUser(out user)) + chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("event_tests_user")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + }); + channel = await chat.CreatePublicConversation("event_tests_channel"); + if (!chat.TryGetCurrentUser(out user)) { Assert.Fail(); } - channel.OLD_Join(); + channel.Join(); await Task.Delay(3500); } [TearDown] public async Task CleanUp() { - channel.OLD_Leave(); + channel.Leave(); await Task.Delay(3000); chat.Destroy(); await Task.Delay(3000); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs index 9b3cada..a2ac9b7 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -1,7 +1,9 @@ using System.Diagnostics; +using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; using PubnubChatApi.Enums; +using Channel = PubNubChatAPI.Entities.Channel; namespace PubNubChatApi.Tests; @@ -14,23 +16,24 @@ 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.OLD_CreatePublicConversation("chat_tests_channel_2"); - if (!chat.OLD_TryGetCurrentUser(out currentUser)) + chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("chats_tests_user_10_no_calkiem_nowy_2")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + }); + channel = await chat.CreatePublicConversation("chat_tests_channel_2"); + if (!chat.TryGetCurrentUser(out currentUser)) { Assert.Fail(); } - channel.OLD_Join(); + channel.Join(); await Task.Delay(3500); } [TearDown] public async Task CleanUp() { - channel.OLD_Leave(); + channel.Leave(); await Task.Delay(1000); chat.Destroy(); await Task.Delay(1000); @@ -53,19 +56,19 @@ public async Task TestGetCurrentUserMentions() var mentions = await chat.GetCurrentUserMentions("99999999999999999", "00000000000000000", 10); Assert.True(mentions != null); - Assert.True(mentions.Mentions.Any(x => x.ChannelId == channel.Id && x.Message.OLD_MessageText == messageContent)); + Assert.True(mentions.Mentions.Any(x => x.ChannelId == channel.Id && x.Message.MessageText == messageContent)); } [Test] public async Task TestGetCurrentUser() { - Assert.True(chat.OLD_TryGetCurrentUser(out var currentUser) && currentUser.Id == this.currentUser.Id); + Assert.True(chat.TryGetCurrentUser(out var currentUser) && currentUser.Id == this.currentUser.Id); } [Test] public async Task TestGetEventHistory() { - await chat.OLD_EmitEvent(PubnubChatEventType.Custom, channel.Id, "{\"test\":\"some_nonsense\"}"); + await chat.EmitEvent(PubnubChatEventType.Custom, channel.Id, "{\"test\":\"some_nonsense\"}"); await Task.Delay(5000); @@ -76,7 +79,7 @@ public async Task TestGetEventHistory() [Test] public async Task TestGetUsers() { - var users = await chat.OLD_GetUsers(); + var users = await chat.GetUsers(); Assert.True(users.Users.Any()); } @@ -93,11 +96,11 @@ public async Task TestCreateDirectConversation() { var convoUser = await chat.GetOrCreateUser("direct_conversation_user"); var directConversation = - await chat.OLD_CreateDirectConversation(convoUser, "direct_conversation_test"); + await chat.CreateDirectConversation(convoUser, "direct_conversation_test"); Assert.True(directConversation.CreatedChannel is { Id: "direct_conversation_test" }); - Assert.True(directConversation.HostMembership != null && directConversation.HostMembership.OLD_UserId == currentUser.Id); + Assert.True(directConversation.HostMembership != null && directConversation.HostMembership.UserId == currentUser.Id); Assert.True(directConversation.InviteesMemberships != null && - directConversation.InviteesMemberships.First().OLD_UserId == convoUser.Id); + directConversation.InviteesMemberships.First().UserId == convoUser.Id); } [Test] @@ -107,12 +110,12 @@ public async Task TestCreateGroupConversation() var convoUser2 = await chat.GetOrCreateUser("group_conversation_user_2"); var convoUser3 = await chat.GetOrCreateUser("group_conversation_user_3"); var groupConversation = await - chat.OLD_CreateGroupConversation([convoUser1, convoUser2, convoUser3], "group_conversation_test"); + chat.CreateGroupConversation([convoUser1, convoUser2, convoUser3], "group_conversation_test"); Assert.True(groupConversation.CreatedChannel is { Id: "group_conversation_test" }); - Assert.True(groupConversation.HostMembership != null && groupConversation.HostMembership.OLD_UserId == currentUser.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.OLD_UserId == convoUser1.Id && x.OLD_ChannelId == "group_conversation_test")); + x.UserId == convoUser1.Id && x.ChannelId == "group_conversation_test")); } [Test] @@ -120,13 +123,13 @@ public async Task TestForwardMessage() { var messageForwardReceivedManualEvent = new ManualResetEvent(false); - var forwardingChannel = await chat.OLD_CreatePublicConversation("forwarding_channel"); + var forwardingChannel = await chat.CreatePublicConversation("forwarding_channel"); forwardingChannel.OnMessageReceived += message => { - Assert.True(message.OLD_MessageText == "message_to_forward"); + Assert.True(message.MessageText == "message_to_forward"); messageForwardReceivedManualEvent.Set(); }; - forwardingChannel.OLD_Join(); + forwardingChannel.Join(); await Task.Delay(2500); channel.OnMessageReceived += async message => { await chat.ForwardMessage(message, forwardingChannel); }; @@ -148,7 +151,7 @@ public async Task TestEmitEvent() }; channel.SetListeningForCustomEvents(true); await Task.Delay(2500); - await chat.OLD_EmitEvent(PubnubChatEventType.Custom, channel.Id, "{\"test\":\"some_nonsense\"}"); + await chat.EmitEvent(PubnubChatEventType.Custom, channel.Id, "{\"test\":\"some_nonsense\"}"); var eventReceived = reportManualEvent.WaitOne(8000); Assert.True(eventReceived); @@ -185,18 +188,18 @@ public async Task TestMarkAllMessagesAsRead() [Test] public async Task TestReadReceipts() { - var otherChat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "other_chat_user") - ); - if (!otherChat.OLD_TryGetChannel(channel.Id, out var otherChatChannel)) + var otherChat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("other_chat_user")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + }); + if (!otherChat.TryGetChannel(channel.Id, out var otherChatChannel)) { Assert.Fail(); return; } - otherChatChannel.OLD_Join(); + otherChatChannel.Join(); await Task.Delay(2500); otherChatChannel.SetListeningForReadReceiptsEvents(true); await Task.Delay(2500); @@ -225,13 +228,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 = new Chat(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 eea6cf8..51e1f67 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs @@ -1,6 +1,8 @@ using System.Diagnostics; +using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; +using Channel = PubNubChatAPI.Entities.Channel; namespace PubNubChatApi.Tests; @@ -14,25 +16,25 @@ 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.OLD_CreatePublicConversation("membership_tests_channel"); - if (!chat.OLD_TryGetCurrentUser(out user)) + chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("membership_tests_user_54")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + }); + channel = await chat.CreatePublicConversation("membership_tests_channel"); + if (!chat.TryGetCurrentUser(out user)) { Assert.Fail(); } - channel.OLD_Join(); + channel.Join(); await Task.Delay(3500); } [TearDown] public async Task CleanUp() { - channel.OLD_Leave(); + channel.Leave(); await Task.Delay(3000); chat.Destroy(); await Task.Delay(3000); @@ -42,7 +44,7 @@ public async Task CleanUp() public async Task TestGetMemberships() { var memberships = await user.GetMemberships(); - Assert.True(memberships.Memberships.Any(x => x.OLD_ChannelId == channel.Id && x.OLD_UserId == user.Id)); + Assert.True(memberships.Memberships.Any(x => x.ChannelId == channel.Id && x.UserId == user.Id)); } [Test] @@ -58,22 +60,25 @@ public async Task TestUpdateMemberships() var updateData = new ChatMembershipData() { - OLD_CustomDataJson = "{\"key\":\"" + Guid.NewGuid() + "\"}" + CustomData = new Dictionary() + { + {"key", Guid.NewGuid().ToString()} + } }; var manualUpdatedEvent = new ManualResetEvent(false); testMembership.OnMembershipUpdated += membership => { Assert.True(membership.Id == testMembership.Id); - var updatedData = membership.OLD_MembershipData.OLD_CustomDataJson; - Assert.True(updatedData == updateData.OLD_CustomDataJson, $"{updatedData} != {updateData.OLD_CustomDataJson}"); + var updatedData = membership.MembershipData.CustomData; + Assert.True(updatedData["key"].ToString() == updateData.CustomData["key"].ToString()); manualUpdatedEvent.Set(); }; testMembership.SetListeningForUpdates(true); await Task.Delay(4000); - await testMembership.OLD_Update(updateData); + await testMembership.Update(updateData); var updated = manualUpdatedEvent.WaitOne(8000); Assert.IsTrue(updated); } @@ -81,39 +86,39 @@ public async Task TestUpdateMemberships() [Test] public async Task TestInvite() { - var testChannel = (await chat.OLD_CreateGroupConversation([user], "test_invite_group_channel")).CreatedChannel; + var testChannel = (await chat.CreateGroupConversation([user], "test_invite_group_channel")).CreatedChannel; var testUser = await chat.GetOrCreateUser("test_invite_user"); - var returnedMembership = await testChannel.OLD_Invite(testUser); - Assert.True(returnedMembership.OLD_ChannelId == testChannel.Id && returnedMembership.OLD_UserId == testUser.Id); + var returnedMembership = await testChannel.Invite(testUser); + Assert.True(returnedMembership.ChannelId == testChannel.Id && returnedMembership.UserId == testUser.Id); } [Test] public async Task TestInviteMultiple() { - var testChannel = (await chat.OLD_CreateGroupConversation([user], "invite_multiple_test_group_channel_3")) + var testChannel = (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.OLD_InviteMultiple([ + var returnedMemberships = await testChannel.InviteMultiple([ secondUser, thirdUser ]); Assert.True( returnedMemberships.Count == 2 && - returnedMemberships.Any(x => x.OLD_UserId == secondUser.Id && x.OLD_ChannelId == testChannel.Id) && - returnedMemberships.Any(x => x.OLD_UserId == thirdUser.Id && x.OLD_ChannelId == testChannel.Id)); + returnedMemberships.Any(x => x.UserId == secondUser.Id && x.ChannelId == testChannel.Id) && + returnedMemberships.Any(x => x.UserId == thirdUser.Id && x.ChannelId == testChannel.Id)); } [Test] public async Task TestLastRead() { - var testChannel = await chat.OLD_CreatePublicConversation("last_read_test_channel_57"); - testChannel.OLD_Join(); + var testChannel = await chat.CreatePublicConversation("last_read_test_channel_57"); + testChannel.Join(); await Task.Delay(4000); var membership = (await user.GetMemberships(limit: 20)).Memberships - .FirstOrDefault(x => x.OLD_ChannelId == testChannel.Id); + .FirstOrDefault(x => x.ChannelId == testChannel.Id); if (membership == null) { Assert.Fail(); @@ -128,13 +133,13 @@ public async Task TestLastRead() await Task.Delay(7000); - var lastTimeToken = membership.OLD_GetLastReadMessageTimeToken(); - Assert.True(lastTimeToken == message.OLD_TimeToken); - await membership.OLD_SetLastReadMessageTimeToken("99999999999999999"); + var lastTimeToken = membership.LastReadMessageTimeToken; + Assert.True(lastTimeToken == message.TimeToken); + await membership.SetLastReadMessageTimeToken("99999999999999999"); await Task.Delay(3000); - Assert.True(membership.OLD_GetLastReadMessageTimeToken() == "99999999999999999"); + Assert.True(membership.LastReadMessageTimeToken == "99999999999999999"); messageReceivedManual.Set(); }; await testChannel.SendText("some_message"); @@ -146,8 +151,8 @@ public async Task TestLastRead() [Test] public async Task TestUnreadMessagesCount() { - var unreadChannel = await chat.OLD_CreatePublicConversation($"test_channel_{Guid.NewGuid()}"); - unreadChannel.OLD_Join(); + var unreadChannel = await chat.CreatePublicConversation($"test_channel_{Guid.NewGuid()}"); + unreadChannel.Join(); await Task.Delay(3500); @@ -157,8 +162,8 @@ public async Task TestUnreadMessagesCount() await Task.Delay(8000); - var membership = (await unreadChannel.OLD_GetMemberships()) - .Memberships.FirstOrDefault(x => x.OLD_UserId == user.Id); + var membership = (await unreadChannel.GetMemberships()) + .Memberships.FirstOrDefault(x => x.UserId == user.Id); var unreadCount = membership == null ? -1 : await membership.GetUnreadMessagesCount(); Assert.True(unreadCount >= 3, $"Expected >=3 unread but got: {unreadCount}"); } @@ -170,7 +175,7 @@ public async Task TestUnreadCountAfterFetchHistory() { await channel.SendText("some_text"); var membership = (await user.GetMemberships()) - .Memberships.FirstOrDefault(x => x.OLD_ChannelId == channel.Id); + .Memberships.FirstOrDefault(x => x.ChannelId == channel.Id); if (membership == null) { Assert.Fail("Couldn't find membership"); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs index 8cbe928..32296b2 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs @@ -1,5 +1,7 @@ +using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; +using Channel = PubNubChatAPI.Entities.Channel; namespace PubNubChatApi.Tests; @@ -14,34 +16,34 @@ public class MessageDraftTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "message_draft_tests_user") - ); - channel = await chat.OLD_CreatePublicConversation("message_draft_tests_channel", new ChatChannelData() + chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("message_draft_tests_user")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + }); + channel = await chat.CreatePublicConversation("message_draft_tests_channel", new ChatChannelData() { ChannelName = "MessageDraftTestingChannel" }); - if (!chat.OLD_TryGetCurrentUser(out var user)) + if (!chat.TryGetCurrentUser(out var user)) { Assert.Fail(); } - channel.OLD_Join(); + channel.Join(); await Task.Delay(3000); - if (!chat.OLD_TryGetUser("mock_user", out dummyUser)) + if (!chat.TryGetUser("mock_user", out dummyUser)) { - dummyUser = await chat.OLD_CreateUser("mock_user", new ChatUserData() + dummyUser = await chat.CreateUser("mock_user", new ChatUserData() { Username = "Mock Usernamiski" }); } - if (!chat.OLD_TryGetChannel("dummy_channel", out dummyChannel)) + if (!chat.TryGetChannel("dummy_channel", out dummyChannel)) { - dummyChannel = await chat.OLD_CreatePublicConversation("dummy_channel"); + dummyChannel = await chat.CreatePublicConversation("dummy_channel"); } } @@ -231,7 +233,7 @@ public async Task TestSend() var successReset = new ManualResetEvent(false); channel.OnMessageReceived += message => { - Assert.True(message.OLD_MessageText == "draft_text"); + Assert.True(message.MessageText == "draft_text"); successReset.Set(); }; var messageDraft = channel.CreateMessageDraft(); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs index 8fa1452..981065b 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs @@ -1,7 +1,9 @@ using System.Diagnostics; +using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; using PubnubChatApi.Enums; +using Channel = PubNubChatAPI.Entities.Channel; namespace PubNubChatApi.Tests; @@ -14,24 +16,24 @@ 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.OLD_CreatePublicConversation("message_tests_channel_2"); - if (!chat.OLD_TryGetCurrentUser(out user)) + chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("message_tests_user_2")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + }); + channel = await chat.CreatePublicConversation("message_tests_channel_2"); + if (!chat.TryGetCurrentUser(out user)) { Assert.Fail(); } - channel.OLD_Join(); + channel.Join(); await Task.Delay(3500); } [TearDown] public async Task CleanUp() { - channel.OLD_Leave(); + channel.Leave(); await Task.Delay(3000); chat.Destroy(); await Task.Delay(3000); @@ -44,8 +46,8 @@ public async Task TestSendAndReceive() channel.OnMessageReceived += message => { - Assert.True(message.OLD_MessageText == "Test message text"); - Assert.True(message.OLD_Type == PubnubChatMessageType.Text); + Assert.True(message.MessageText == "Test message text"); + Assert.True(message.Type == PubnubChatMessageType.Text); manualReceiveEvent.Set(); }; await channel.SendText("Test message text", new SendTextParams() @@ -60,12 +62,12 @@ public async Task TestSendAndReceive() public async Task TestReceivingMessageData() { var manualReceiveEvent = new ManualResetEvent(false); - var testChannel = await chat.OLD_CreatePublicConversation("message_data_test_channel"); - testChannel.OLD_Join(); + var testChannel = await chat.CreatePublicConversation("message_data_test_channel"); + testChannel.Join(); await Task.Delay(2500); testChannel.OnMessageReceived += async message => { - if (message.OLD_MessageText == "message_to_be_quoted") + if (message.MessageText == "message_to_be_quoted") { await testChannel.SendText("message_with_data", new SendTextParams() { @@ -73,11 +75,11 @@ public async Task TestReceivingMessageData() QuotedMessage = message }); } - else if (message.OLD_MessageText == "message_with_data") + else if (message.MessageText == "message_with_data") { - Assert.True(message.OLD_MentionedUsers.Any(x => x.Id == user.Id)); + Assert.True(message.MentionedUsers.Any(x => x.Id == user.Id)); Assert.True(message.TryGetQuotedMessage(out var quotedMessage) && - quotedMessage.OLD_MessageText == "message_to_be_quoted"); + quotedMessage.MessageText == "message_to_be_quoted"); manualReceiveEvent.Set(); } }; @@ -93,9 +95,9 @@ public async Task TestTryGetMessage() var manualReceiveEvent = new ManualResetEvent(false); channel.OnMessageReceived += message => { - if (message.OLD_ChannelId == channel.Id) + if (message.ChannelId == channel.Id) { - Assert.True(chat.TryGetMessage(channel.Id, message.OLD_TimeToken, out _)); + Assert.True(chat.TryGetMessage(channel.Id, message.TimeToken, out _)); manualReceiveEvent.Set(); } }; @@ -117,7 +119,7 @@ public async Task TestEditMessage() message.OnMessageUpdated += updatedMessage => { manualUpdatedEvent.Set(); - Assert.True(updatedMessage.OLD_MessageText == "new-text"); + Assert.True(updatedMessage.MessageText == "new-text"); }; await message.EditMessageText("new-text"); }; @@ -138,7 +140,7 @@ public async Task TestGetOriginalMessageText() await Task.Delay(2000); message.OnMessageUpdated += updatedMessage => { - originalTextAfterUpdate = updatedMessage.OLD_OriginalMessageText; + originalTextAfterUpdate = updatedMessage.OriginalMessageText; manualUpdatedEvent.Set(); }; await message.EditMessageText("new-text"); @@ -163,7 +165,7 @@ public async Task TestDeleteMessage() await Task.Delay(2000); - Assert.True(message.OLD_IsDeleted); + Assert.True(message.IsDeleted); manualReceivedEvent.Set(); }; await channel.SendText("something"); @@ -179,16 +181,16 @@ public async Task TestRestoreMessage() channel.OnMessageReceived += async message => { await message.Delete(true); - Assert.True(message.OLD_IsDeleted); + Assert.True(message.IsDeleted); await Task.Delay(4000); - Assert.True(message.OLD_IsDeleted); + Assert.True(message.IsDeleted); await message.Restore(); await Task.Delay(4000); - Assert.False(message.OLD_IsDeleted); + Assert.False(message.IsDeleted); manualReceivedEvent.Set(); }; await channel.SendText("some text here ladi ladi la"); @@ -200,8 +202,8 @@ public async Task TestRestoreMessage() [Test] public async Task TestPinMessage() { - var pinTestChannel = await chat.OLD_CreatePublicConversation(); - pinTestChannel.OLD_Join(); + var pinTestChannel = await chat.CreatePublicConversation(); + pinTestChannel.Join(); await Task.Delay(2500); pinTestChannel.SetListeningForUpdates(true); await Task.Delay(3000); @@ -214,7 +216,7 @@ public async Task TestPinMessage() await Task.Delay(3000); var got = pinTestChannel.TryGetPinnedMessage(out var pinnedMessage); - Assert.True(got && pinnedMessage.OLD_MessageText == "message to pin"); + Assert.True(got && pinnedMessage.MessageText == "message to pin"); manualReceivedEvent.Set(); }; await pinTestChannel.SendText("message to pin"); @@ -235,7 +237,7 @@ public async Task TestMessageReactions() var has = message.HasUserReaction("happy"); Assert.True(has); - var reactions = message.OLD_Reactions; + var reactions = message.Reactions; Assert.True(reactions.Count == 1 && reactions.Any(x => x.Value == "happy")); manualReset.Set(); }; @@ -273,7 +275,7 @@ public async Task TestCreateThread() { message.SetListeningForUpdates(true); var thread = await message.CreateThread(); - thread.OLD_Join(); + thread.Join(); await Task.Delay(3500); await thread.SendText("thread_init_text"); await Task.Delay(5000); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs index e8502df..4d209a9 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; @@ -12,11 +13,11 @@ public class RestrictionsTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "restrictions_tests_user") - ); + chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("restrictions_tests_user")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + }); } [TearDown] @@ -30,7 +31,7 @@ public async Task CleanUp() public async Task TestSetRestrictions() { var user = await chat.GetOrCreateUser("user123"); - var channel = await chat.OLD_CreatePublicConversation("new_channel"); + var channel = await chat.CreatePublicConversation("new_channel"); await Task.Delay(2000); @@ -59,7 +60,7 @@ public async Task TestSetRestrictions() public async Task TestGetRestrictionsSets() { var user = await chat.GetOrCreateUser("user1234"); - var channel = await chat.OLD_CreatePublicConversation("new_channel_2"); + var channel = await chat.CreatePublicConversation("new_channel_2"); await Task.Delay(4000); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs index 895a4b1..5108a6e 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs @@ -6,13 +6,13 @@ public static class TestUtils { public static async Task GetOrCreateUser(this Chat chat, string userId) { - if (chat.OLD_TryGetUser(userId, out var user)) + if (chat.TryGetUser(userId, out var user)) { return user; } else { - return await chat.OLD_CreateUser(userId); + return await chat.CreateUser(userId); } } } \ 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 c9115e6..ada8e8d 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs @@ -1,6 +1,8 @@ using System.Diagnostics; +using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; +using Channel = PubNubChatAPI.Entities.Channel; namespace PubNubChatApi.Tests; @@ -14,25 +16,25 @@ public class ThreadsTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig( - PubnubTestsParameters.PublishKey, - PubnubTestsParameters.SubscribeKey, - "threads_tests_user_2") - ); + chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("threads_tests_user_2")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + }); var randomId = Guid.NewGuid().ToString()[..10]; - channel = await chat.OLD_CreatePublicConversation(randomId); - if (!chat.OLD_TryGetCurrentUser(out user)) + channel = await chat.CreatePublicConversation(randomId); + if (!chat.TryGetCurrentUser(out user)) { Assert.Fail(); } - channel.OLD_Join(); + channel.Join(); await Task.Delay(3500); } [TearDown] public async Task CleanUp() { - channel.OLD_Leave(); + channel.Leave(); await Task.Delay(3000); await channel.Delete(); chat.Destroy(); @@ -47,7 +49,7 @@ public async Task TestGetThreadHistory() { message.SetListeningForUpdates(true); var thread = await message.CreateThread(); - thread.OLD_Join(); + thread.Join(); await Task.Delay(5000); @@ -58,7 +60,7 @@ public async Task TestGetThreadHistory() await Task.Delay(10000); var history = await thread.GetThreadHistory("99999999999999999", "00000000000000000", 3); - Assert.True(history.Count == 3 && history.Any(x => x.OLD_MessageText == "one")); + Assert.True(history.Count == 3 && history.Any(x => x.MessageText == "one")); historyReadReset.Set(); }; await channel.SendText("thread_start_message"); @@ -74,7 +76,7 @@ public async Task TestThreadChannelParentChannelPinning() { message.SetListeningForUpdates(true); var thread = await message.CreateThread(); - thread.OLD_Join(); + thread.Join(); await thread.SendText("thread init message"); await Task.Delay(7000); @@ -86,7 +88,7 @@ public async Task TestThreadChannelParentChannelPinning() await Task.Delay(7000); var hasPinned = channel.TryGetPinnedMessage(out var pinnedMessage); - var correctText = hasPinned && pinnedMessage.OLD_MessageText == "thread init message"; + var correctText = hasPinned && pinnedMessage.MessageText == "thread init message"; Assert.True(hasPinned && correctText); await thread.UnPinMessageFromParentChannel(); @@ -107,7 +109,7 @@ public async Task TestThreadChannelEmitUserMention() channel.OnMessageReceived += async message => { var thread = await message.CreateThread(); - thread.OLD_Join(); + thread.Join(); await Task.Delay(2500); user.SetListeningForMentionEvents(true); await Task.Delay(2500); @@ -131,7 +133,7 @@ public async Task TestThreadMessageParentChannelPinning() { message.SetListeningForUpdates(true); var thread = await message.CreateThread(); - thread.OLD_Join(); + thread.Join(); await Task.Delay(3500); @@ -147,7 +149,7 @@ public async Task TestThreadMessageParentChannelPinning() await Task.Delay(5000); - Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.OLD_MessageText == threadMessage.OLD_MessageText); + Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.MessageText == threadMessage.MessageText); await threadMessage.UnPinMessageFromParentChannel(); @@ -169,7 +171,7 @@ public async Task TestThreadMessageUpdate() { message.SetListeningForUpdates(true); var thread = await message.CreateThread(); - thread.OLD_Join(); + thread.Join(); await Task.Delay(3000); @@ -185,7 +187,7 @@ public async Task TestThreadMessageUpdate() threadMessage.SetListeningForUpdates(true); threadMessage.OnThreadMessageUpdated += updatedThreadMessage => { - Assert.True(updatedThreadMessage.OLD_MessageText == "new_text"); + Assert.True(updatedThreadMessage.MessageText == "new_text"); messageUpdatedReset.Set(); }; await threadMessage.EditMessageText("new_text"); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs index a6eee2f..09922ab 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs @@ -1,6 +1,8 @@ using System.Diagnostics; +using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; +using Channel = PubNubChatAPI.Entities.Channel; namespace PubNubChatApi.Tests; @@ -14,25 +16,24 @@ 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.OLD_CreatePublicConversation("user_tests_channel"); - if (!chat.OLD_TryGetCurrentUser(out user)) + chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("user_tests_user")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + }); + channel = await chat.CreatePublicConversation("user_tests_channel"); + if (!chat.TryGetCurrentUser(out user)) { Assert.Fail(); } - channel.OLD_Join(); + channel.Join(); await Task.Delay(3500); } [TearDown] public async Task CleanUp() { - channel.OLD_Leave(); + channel.Leave(); await Task.Delay(3000); chat.Destroy(); await Task.Delay(3000); @@ -66,21 +67,24 @@ public async Task TestUserUpdate() var newRandomUserName = Guid.NewGuid().ToString(); testUser.OnUserUpdated += updatedUser => { - Assert.True(updatedUser.OLD_UserName == newRandomUserName); - Assert.True(updatedUser.OLD_CustomData == "{\"some_key\":\"some_value\"}"); - Assert.True(updatedUser.OLD_Email == "some@guy.com"); - Assert.True(updatedUser.OLD_ExternalId == "xxx_some_guy_420_xxx"); - Assert.True(updatedUser.OLD_ProfileUrl == "www.some.guy"); - Assert.True(updatedUser.OLD_Status == "yes"); - Assert.True(updatedUser.OLD_DataType == "someType"); + Assert.True(updatedUser.UserName == newRandomUserName); + 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"); + Assert.True(updatedUser.Status == "yes"); + Assert.True(updatedUser.DataType == "someType"); updatedReset.Set(); }; testUser.SetListeningForUpdates(true); await Task.Delay(3000); - await testUser.OLD_Update(new ChatUserData() + await testUser.Update(new ChatUserData() { Username = newRandomUserName, - OLD_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", @@ -94,22 +98,22 @@ await testUser.OLD_Update(new ChatUserData() [Test] public async Task TestUserDelete() { - var someUser = await chat.OLD_CreateUser(Guid.NewGuid().ToString()); + var someUser = await chat.CreateUser(Guid.NewGuid().ToString()); - Assert.True(chat.OLD_TryGetUser(someUser.Id, out _), "Couldn't get freshly created user"); + Assert.True(chat.TryGetUser(someUser.Id, out _), "Couldn't get freshly created user"); await someUser.DeleteUser(); await Task.Delay(3000); - Assert.False(chat.OLD_TryGetUser(someUser.Id, out _), "Got the freshly deleted user"); + Assert.False(chat.TryGetUser(someUser.Id, out _), "Got the freshly deleted user"); } [Test] public async Task TestUserWherePresent() { - var someChannel = await chat.OLD_CreatePublicConversation(); - someChannel.OLD_Join(); + var someChannel = await chat.CreatePublicConversation(); + someChannel.Join(); await Task.Delay(4000); @@ -121,8 +125,8 @@ public async Task TestUserWherePresent() [Test] public async Task TestUserIsPresentOn() { - var someChannel = await chat.OLD_CreatePublicConversation(); - someChannel.OLD_Join(); + var someChannel = await chat.CreatePublicConversation(); + someChannel.Join(); await Task.Delay(4000); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs index faf7a5c..26f7576 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs @@ -8,92 +8,11 @@ namespace PubNubChatAPI.Entities { public abstract class ChatEntity { - [DllImport("pubnub-chat")] - protected static extern void pn_callback_handle_dispose(IntPtr handle); + public abstract Task Resync(); - [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 ChatEntity() - { - } - - 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) - { - updateListeningHandle = await SetListening(updateListeningHandle, listen, StreamUpdates); - } - - public virtual async Task Resync() - { - } - - internal async Task SetListening(IntPtr callbackHandle, bool listen, Func streamFunction) - { - if (listen) - { - if (callbackHandle != IntPtr.Zero) - { - return callbackHandle; - } - callbackHandle = await Task.Run(streamFunction); - CUtilities.CheckCFunctionResult(callbackHandle); - return callbackHandle; - } - 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; - } - } - - 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(); + throw new NotImplementedException(); } } } \ 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 a51bf66..82be98e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/UniqueChatEntity.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/UniqueChatEntity.cs @@ -10,10 +10,5 @@ internal UniqueChatEntity(string uniqueId) { Id = uniqueId; } - - internal UniqueChatEntity(IntPtr pointer, string uniqueId) : base(pointer) - { - Id = uniqueId; - } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index 9b10576..e105f89 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -1,16 +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 PubnubApi; using PubnubChatApi.Entities.Data; using PubnubChatApi.Entities.Events; -using PubnubChatApi.Enums; using PubnubChatApi.Utilities; namespace PubNubChatAPI.Entities @@ -24,215 +19,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 - - public string OLD_Name - { - get - { - var buffer = new StringBuilder(512); - pn_channel_get_data_channel_name(pointer, buffer); - return buffer.ToString(); - } - } - - public string OLD_Description - { - get - { - var buffer = new StringBuilder(512); - pn_channel_get_data_description(pointer, buffer); - return buffer.ToString(); - } - } - - public string OLD_CustomDataJson - { - get - { - var buffer = new StringBuilder(2048); - pn_channel_get_data_custom_data_json(pointer, buffer); - return buffer.ToString(); - } - } - - public string OLD_Updated - { - get - { - var buffer = new StringBuilder(512); - pn_channel_get_data_updated(pointer, buffer); - return buffer.ToString(); - } - } - - public string OLD_Status - { - get - { - var buffer = new StringBuilder(512); - pn_channel_get_data_status(pointer, buffer); - return buffer.ToString(); - } - } - - public string OLD_Type - { - get - { - var buffer = new StringBuilder(512); - pn_channel_get_data_type(pointer, buffer); - return buffer.ToString(); - } - } - /// /// The name of the channel. /// @@ -258,10 +44,7 @@ public string OLD_Type /// 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 => channelData.ChannelCustomDataJson; + public Dictionary CustomData => channelData.ChannelCustomData; /// /// The information about the last update of the channel. @@ -290,12 +73,6 @@ public string OLD_Type private ChatChannelData channelData; protected Chat chat; - private IntPtr customEventsListeningHandle; - private IntPtr reportEventsListeningHandle; - private IntPtr readReceiptsListeningHandle; - private IntPtr typingListeningHandle; - private IntPtr presenceListeningHandle; - protected IntPtr connectionHandle; protected Subscription? subscription; @@ -367,11 +144,6 @@ public string OLD_Type public event Action OnReportEvent; public event Action OnCustomEvent; - internal Channel(Chat chat, string channelId, IntPtr channelPointer) : base(channelPointer, channelId) - { - this.chat = chat; - } - internal Channel(Chat chat, string channelId, ChatChannelData data) : base(channelId) { this.chat = chat; @@ -393,12 +165,10 @@ internal static async Task UpdateChannelData(Chat chat, string channelId, .Channel(channelId) .Name(data.ChannelName) .Description(data.ChannelDescription) - .Custom(new Dictionary() - { - {"custom", data.ChannelCustomDataJson}, - {"updated", data.ChannelUpdated}, - {"status", data.ChannelStatus}, - }) + //TODO: C# FIX + //.Status(data.ChannelStatus) + //.Updated(data.ChannelUpdated) + .Custom(data.ChannelCustomData) .ExecuteAsync(); if (result.Status.Error) { @@ -435,15 +205,9 @@ public override async Task Resync() UpdateLocalData(newData); } - 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)); + throw new NotImplementedException(); } internal void BroadcastCustomEvent(ChatEvent chatEvent) @@ -453,8 +217,7 @@ internal void BroadcastCustomEvent(ChatEvent chatEvent) public async void SetListeningForReportEvents(bool listen) { - reportEventsListeningHandle = await SetListening(reportEventsListeningHandle, listen, - () => pn_channel_stream_message_reports(pointer)); + throw new NotImplementedException(); } internal void BroadcastReportEvent(ChatEvent chatEvent) @@ -464,29 +227,19 @@ internal void BroadcastReportEvent(ChatEvent chatEvent) public async void SetListeningForReadReceiptsEvents(bool listen) { - readReceiptsListeningHandle = await SetListening(readReceiptsListeningHandle, listen, - () => pn_channel_stream_read_receipts(pointer)); + throw new NotImplementedException(); } public async void SetListeningForTyping(bool listen) { - typingListeningHandle = await SetListening(typingListeningHandle, listen, - () => pn_channel_get_typing(pointer)); + throw new NotImplementedException(); } public async void SetListeningForPresence(bool listen) { - presenceListeningHandle = await SetListening(presenceListeningHandle, listen, - () => pn_channel_stream_presence(pointer)); - } - - internal static string GetChannelIdFromPtr(IntPtr channelPointer) - { - var buffer = new StringBuilder(512); - pn_channel_get_channel_id(channelPointer, buffer); - return buffer.ToString(); + throw new NotImplementedException(); } - + internal void BroadcastMessageReceived(Message message) { OnMessageReceived?.Invoke(message); @@ -497,13 +250,6 @@ internal void BroadcastReadReceipt(Dictionary?> readReceipt OnReadReceiptEvent?.Invoke(readReceiptEventData); } - internal override void UpdateWithPartialPtr(IntPtr partialPointer) - { - var newFullPointer = pn_channel_update_with_base(partialPointer, pointer); - CUtilities.CheckCFunctionResult(newFullPointer); - UpdatePointer(newFullPointer); - } - internal void BroadcastChannelUpdate() { OnChannelUpdate?.Invoke(this); @@ -560,32 +306,27 @@ public async Task ForwardMessage(Message message) public async Task EmitUserMention(string userId, string timeToken, string text) { - CUtilities.CheckCFunctionResult(await Task.Run(() => - pn_channel_emit_user_mention(pointer, userId, timeToken, text))); + throw new NotImplementedException(); } public async Task StartTyping() { - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_channel_start_typing(pointer))); + throw new NotImplementedException(); } public async Task StopTyping() { - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_channel_stop_typing(pointer))); + throw new NotImplementedException(); } public virtual async Task PinMessage(Message message) { - var newPointer = await Task.Run(() => pn_channel_pin_message(pointer, message.Pointer)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + throw new NotImplementedException(); } public virtual async Task UnpinMessage() { - var newPointer = await Task.Run(() => pn_channel_unpin_message(pointer)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + throw new NotImplementedException(); } //TODO: currently same result whether error or no pinned message present @@ -597,21 +338,7 @@ public virtual async Task UnpinMessage() /// public bool TryGetPinnedMessage(out Message pinnedMessage) { - 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; - } + throw new NotImplementedException(); } /// @@ -629,22 +356,7 @@ public bool TryGetPinnedMessage(out Message pinnedMessage) 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 new List(); - } - - var jsonDict = JsonConvert.DeserializeObject>(resultJson); - if (jsonDict == null || !jsonDict.TryGetValue("value", out var pointers) || pointers == null) - { - return new List(); - } - - return PointerParsers.ParseJsonMembershipPointers(chat, pointers); + throw new NotImplementedException(); } /// @@ -658,10 +370,7 @@ public async Task> GetUserSuggestions(string text, int limit = public MessageDraft CreateMessageDraft(UserSuggestionSource userSuggestionSource = UserSuggestionSource.GLOBAL, bool isTypingIndicatorTriggered = true, int userLimit = 10, int channelLimit = 10) { - var draftPointer = pn_channel_create_message_draft_dirty( - pointer, (int)userSuggestionSource, isTypingIndicatorTriggered, userLimit, channelLimit); - CUtilities.CheckCFunctionResult(draftPointer); - return new MessageDraft(draftPointer); + throw new NotImplementedException(); } /// @@ -830,70 +539,6 @@ public async void Join(ChatMembershipData? membershipData = null) Connect(); } - - public void OLD_Disconnect() - { - if (connectionHandle == IntPtr.Zero || pointer == IntPtr.Zero) - { - return; - } - - CUtilities.CheckCFunctionResult(pn_channel_disconnect(pointer)); - pn_callback_handle_dispose(connectionHandle); - connectionHandle = IntPtr.Zero; - } - - public async void OLD_Leave() - { - if (connectionHandle == IntPtr.Zero || pointer == IntPtr.Zero) - { - return; - } - - var connectionHandleCopy = connectionHandle; - connectionHandle = IntPtr.Zero; - CUtilities.CheckCFunctionResult(await Task.Run(() => - { - if (pointer == IntPtr.Zero) - { - return 0; - } - - pn_channel_leave(pointer); - pn_callback_handle_dispose(connectionHandleCopy); - return 0; - })); - } - - public async void OLD_Connect() - { - if (connectionHandle != IntPtr.Zero) - { - return; - } - - connectionHandle = await SetListening(connectionHandle, true, () => pn_channel_connect(pointer)); - } - - public async void OLD_Join(ChatMembershipData? membershipData = null) - { - 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.OLD_CustomDataJson, - membershipData.Type, membershipData.Status)); - } - } /// /// Sets the restrictions for the user. @@ -916,9 +561,7 @@ public async void OLD_Join(ChatMembershipData? membershipData = null) /// 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))); + throw new NotImplementedException(); } public async Task SetRestrictions(string userId, Restriction restriction) @@ -949,16 +592,7 @@ public virtual async Task SendText(string message) 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))); + throw new NotImplementedException(); } /// @@ -1032,36 +666,13 @@ public async Task Delete() /// 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)) - { - restriction = JsonConvert.DeserializeObject(restrictionJson); - } - - return restriction; + throw new NotImplementedException(); } public async Task GetUsersRestrictions(string sort = "", int limit = 0, Page 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)) - { - return new UsersRestrictionsWrapper(); - } - - var wrapper = JsonConvert.DeserializeObject(restrictionsJson); - wrapper ??= new UsersRestrictionsWrapper(); - return wrapper; + throw new NotImplementedException(); } /// @@ -1083,9 +694,7 @@ await Task.Run(() => /// public async Task IsUserPresent(string userId) { - var result = await Task.Run(() => pn_channel_is_present(pointer, userId)); - CUtilities.CheckCFunctionResult(result); - return result == 1; + throw new NotImplementedException(); } /// @@ -1108,17 +717,7 @@ public async Task IsUserPresent(string userId) /// 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)) - { - ret = JsonConvert.DeserializeObject>(jsonResult); - ret ??= new List(); - } - - return ret; + throw new NotImplementedException(); } /// @@ -1148,12 +747,6 @@ public async Task> WhoIsPresent() { return await chat.GetChannelMemberships(Id, filter, sort, limit, page); } - - public async Task OLD_GetMemberships(string filter = "", string sort = "", int limit = 0, - Page page = null) - { - return await chat.OLD_GetChannelMemberships(Id, filter, sort, limit, page); - } /// /// Gets the Message object for the given timetoken. @@ -1200,50 +793,10 @@ public async Task> GetMessageHistory(string startTimeToken, string { return await chat.InviteToChannel(Id, user.Id); } - - public async Task OLD_Invite(User user) - { - var membershipPointer = await Task.Run(() => pn_channel_invite_user(pointer, user.Pointer)); - CUtilities.CheckCFunctionResult(membershipPointer); - var membershipId = Membership.GetMembershipIdFromPtr(membershipPointer); - chat.OLD_TryGetMembership(membershipId, membershipPointer, out var membership); - return membership; - } public async Task> InviteMultiple(List users) { return await chat.InviteMultipleToChannel(Id, users); } - - public async Task> OLD_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() - { - 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)); - OLD_Disconnect(); - } - - protected override void DisposePointer() - { - pn_channel_delete(pointer); - pointer = IntPtr.Zero; - } } } \ 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 b7e3a51..38e5b8f 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -29,284 +29,6 @@ namespace PubNubChatAPI.Entities /// public class Chat { - #region DLL Imports - - //TODO: REMOVE - [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); - - //TODO: REMOVE - [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(); //TODO: wrappers rethink @@ -321,24 +43,6 @@ string membership_status public ChatAccessManager ChatAccessManager { get; } public PubnubChatConfig Config { get; } - - //TODO: REMOVE - public static async Task CreateInstance(PubnubChatConfig config) - { - var chat = await Task.Run(() => new Chat(config)); - chat.FetchUpdatesLoop(); - return chat; - } - //TODO: REMOVE - internal Chat(PubnubChatConfig config) - { - chatPointer = pn_chat_new(config.OLD_PublishKey, config.OLD_SubscribehKey, config.OLD_UserId, config.OLD_AuthhKey, - config.TypingTimeout, config.TypingTimeoutDifference, config.StoreUserActivityInterval, - config.StoreUserActivityTimestamp); - CUtilities.CheckCFunctionResult(chatPointer); - Config = config; - ChatAccessManager = new ChatAccessManager(chatPointer); - } /// /// Initializes a new instance of the class. @@ -357,7 +61,7 @@ public Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatListe PubnubInstance = new Pubnub(pubnubConfig); ListenerFactory = listenerFactory ?? new DotNetListenerFactory(); Config = chatConfig; - ChatAccessManager = new ChatAccessManager(chatPointer); + ChatAccessManager = new ChatAccessManager(this); } /// @@ -377,346 +81,16 @@ public Chat(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? lis Config = chatConfig; PubnubInstance = pubnub; ListenerFactory = listenerFactory ?? new DotNetListenerFactory(); - ChatAccessManager = new ChatAccessManager(chatPointer); - } - - #region Updates handling - - //TODO: REMOVE - internal async Task FetchUpdatesLoop() - { - while (fetchUpdates) - { - var updates = GetUpdates(); - try - { - ParseJsonUpdatePointers(updates); - } - catch (Exception e) - { - Debug.WriteLine($"Error when parsing JSON updates: {e}"); - } - - await Task.Delay(200); - } + ChatAccessManager = new ChatAccessManager(this); } - - //TODO: REMOVE - internal void ParseJsonUpdatePointers(string jsonString) - { - if (string.IsNullOrEmpty(jsonString) || jsonString == "[]") - { - return; - } - - 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) - { - 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 (!OLD_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 (OLD_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 (OLD_TryGetChannel(properChannelId, out var reportChannel)) - { - reportChannel.BroadcastReportEvent(chatEvent); - invoked = true; - } - - break; - case PubnubChatEventType.Mention: - if (OLD_TryGetUser(chatEvent.UserId, out var mentionedUser)) - { - mentionedUser.BroadcastMentionEvent(chatEvent); - invoked = true; - } - - break; - case PubnubChatEventType.Invite: - if (OLD_TryGetUser(chatEvent.UserId, out var invitedUser)) - { - invitedUser.BroadcastInviteEvent(chatEvent); - invoked = true; - } - - break; - case PubnubChatEventType.Custom: - if (OLD_TryGetChannel(chatEvent.ChannelId, out var customEventChannel)) - { - customEventChannel.BroadcastCustomEvent(chatEvent); - invoked = true; - } - - break; - case PubnubChatEventType.Moderation: - if (OLD_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 - OLD_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 (OLD_TryGetChannel(channelId, out var channel)) - { - channel.BroadcastPresenceUpdate(); - } - } - - break; - default: - Debug.WriteLine("Wasn't able to deserialize incoming pointer into any known type!"); - break; - } - } - } - } - - internal string GetUpdates() - { - var messagesBuffer = new StringBuilder(32768); - CUtilities.CheckCFunctionResult(pn_c_consume_response_buffer(chatPointer, messagesBuffer)); - return messagesBuffer.ToString(); - } - - #endregion - #region Channels public void AddListenerToChannelsUpdate(List channelIds, Action listener) { foreach (var channelId in channelIds) { - if (OLD_TryGetChannel(channelId, out var channel)) + if (TryGetChannel(channelId, out var channel)) { channel.OnChannelUpdate += listener; } @@ -902,145 +276,6 @@ public void AddListenerToChannelsUpdate(List channelIds, Action return await CreateConversation("group", users, channelId, channelData, membershipData); } - - public async Task OLD_CreatePublicConversation(string channelId = "") - { - if (string.IsNullOrEmpty(channelId)) - { - channelId = Guid.NewGuid().ToString(); - } - - return await OLD_CreatePublicConversation(channelId, new ChatChannelData()); - } - - public async Task OLD_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; - } - - public async Task OLD_CreateDirectConversation(User user, string channelId = "", - ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) - { - if (string.IsNullOrEmpty(channelId)) - { - channelId = Guid.NewGuid().ToString(); - } - - channelData ??= new ChatChannelData(); - - IntPtr wrapperPointer; - - if (membershipData == null) - { - 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)); - } - else - { - 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.OLD_CustomDataJson, - membershipData.Type, membershipData.Status)); - } - - CUtilities.CheckCFunctionResult(wrapperPointer); - - var createdChannelPointer = pn_chat_get_created_channel_wrapper_channel(wrapperPointer); - CUtilities.CheckCFunctionResult(createdChannelPointer); - OLD_TryGetChannel(createdChannelPointer, out var createdChannel); - - var hostMembershipPointer = pn_chat_get_created_channel_wrapper_host_membership(wrapperPointer); - CUtilities.CheckCFunctionResult(hostMembershipPointer); - OLD_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() - { - CreatedChannel = createdChannel, - HostMembership = hostMembership, - InviteesMemberships = new List() { inviteeMembership } - }; - } - - public async Task OLD_CreateGroupConversation(List users, string channelId = "", - ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) - { - if (string.IsNullOrEmpty(channelId)) - { - channelId = Guid.NewGuid().ToString(); - } - channelData ??= new ChatChannelData(); - - IntPtr wrapperPointer; - if (membershipData == null) - { - 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)); - } - else - { - 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.OLD_CustomDataJson, - membershipData.Type, membershipData.Status)); - } - - CUtilities.CheckCFunctionResult(wrapperPointer); - - var createdChannelPointer = pn_chat_get_created_channel_wrapper_channel(wrapperPointer); - CUtilities.CheckCFunctionResult(createdChannelPointer); - OLD_TryGetChannel(createdChannelPointer, out var createdChannel); - - var hostMembershipPointer = pn_chat_get_created_channel_wrapper_host_membership(wrapperPointer); - CUtilities.CheckCFunctionResult(hostMembershipPointer); - OLD_TryGetMembership(hostMembershipPointer, out var hostMembership); - - var buffer = new StringBuilder(8192); - CUtilities.CheckCFunctionResult( - pn_chat_get_created_channel_wrapper_invited_memberships(wrapperPointer, buffer)); - var inviteeMemberships = PointerParsers.ParseJsonMembershipPointers(this, buffer.ToString()); - - pn_chat_dispose_created_channel_wrapper(wrapperPointer); - - return new CreatedChannelWrapper() - { - CreatedChannel = createdChannel, - HostMembership = hostMembership, - InviteesMemberships = inviteeMemberships - }; - } public async Task InviteToChannel(string channelId, string userId) { @@ -1209,81 +444,11 @@ public bool TryGetChannel(string channelId, out Channel channel) } } } - - public bool OLD_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; - } - } - //Fetching and updating a regular channel - else - { - var channelPointer = pn_chat_get_channel(chatPointer, channelId); - return OLD_TryGetChannel(channelId, channelPointer, out channel); - } - } - - public async Task OLD_GetChannelAsync(string channelId) - { - return await Task.Run(() => - { - var result = OLD_TryGetChannel(channelId, out var channel); - return result ? channel : null; - }); - } - - internal bool OLD_TryGetChannel(IntPtr channelPointer, out Channel channel) - { - var channelId = Channel.GetChannelIdFromPtr(channelPointer); - return OLD_TryGetChannel(channelId, channelPointer, out channel); - } - - internal bool OLD_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) - { - OLD_TryGetChannel(newPointer, out _); - } - - internal void UpdateChannelPointer(string id, IntPtr newPointer) - { - OLD_TryGetChannel(id, newPointer, out _); - } public async Task GetChannels(string filter = "", string sort = "", int limit = 0, Page 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); + throw new NotImplementedException(); } /// @@ -1307,22 +472,7 @@ public async Task GetChannels(string filter = "", strin /// 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)); - } + throw new NotImplementedException(); } /// @@ -1341,11 +491,7 @@ public async Task UpdateChannel(string channelId, ChatChannelData updatedData) /// public async Task DeleteChannel(string channelId) { - if (channelWrappers.ContainsKey(channelId)) - { - channelWrappers.Remove(channelId); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_chat_delete_channel(chatPointer, channelId))); - } + throw new NotImplementedException(); } #endregion @@ -1355,23 +501,7 @@ public async Task DeleteChannel(string channelId) public async Task GetCurrentUserMentions(string startTimeToken, string endTimeToken, int count) { - 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)) - { - return emptyResponse; - } - - var internalWrapper = JsonConvert.DeserializeObject(internalWrapperJson); - if (internalWrapper == null) - { - return emptyResponse; - } - - return new UserMentionsWrapper(this, internalWrapper); + throw new NotImplementedException(); } /// @@ -1395,22 +525,6 @@ public bool TryGetCurrentUser(out User user) var userId = PubnubInstance.GetCurrentUserId(); return await GetUserAsync(userId); } - - public bool OLD_TryGetCurrentUser(out User user) - { - var userPointer = pn_chat_current_user(chatPointer); - CUtilities.CheckCFunctionResult(userPointer); - return OLD_TryGetUser(userPointer, out user); - } - - public async Task OLD_GetCurrentUserAsync() - { - return await Task.Run(() => - { - var result = OLD_TryGetCurrentUser(out var currentUser); - return result ? currentUser : null; - }); - } /// /// Sets the restrictions for the user with the provided user ID. @@ -1432,9 +546,7 @@ public bool OLD_TryGetCurrentUser(out User user) /// 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))); + throw new NotImplementedException(); } public async Task SetRestriction(string userId, string channelId, Restriction restriction) @@ -1446,40 +558,13 @@ public void AddListenerToUsersUpdate(List userIds, Action listener { foreach (var userId in userIds) { - if (OLD_TryGetUser(userId, out var user)) + if (TryGetUser(userId, out var user)) { user.OnUserUpdated += listener; } } } - public async Task OLD_CreateUser(string userId) - { - return await OLD_CreateUser(userId, new ChatUserData()); - } - - public async Task OLD_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.OLD_CustomDataJson, - additionalData.Status, - additionalData.Status)); - CUtilities.CheckCFunctionResult(userPointer); - var user = new User(this, userId, userPointer); - userWrappers.Add(userId, user); - return user; - } - /// /// Creates a new user with the provided user ID. /// @@ -1565,7 +650,7 @@ public async Task OLD_CreateUser(string userId, ChatUserData additionalDat /// public async Task IsPresent(string userId, string channelId) { - if (OLD_TryGetChannel(channelId, out var channel)) + if (TryGetChannel(channelId, out var channel)) { return await channel.IsUserPresent(userId); } @@ -1597,7 +682,7 @@ public async Task IsPresent(string userId, string channelId) /// public async Task> WhoIsPresent(string channelId) { - if (OLD_TryGetChannel(channelId, out var channel)) + if (TryGetChannel(channelId, out var channel)) { return await channel.WhoIsPresent(); } @@ -1629,7 +714,7 @@ public async Task> WhoIsPresent(string channelId) /// public async Task> WherePresent(string userId) { - if (OLD_TryGetUser(userId, out var user)) + if (TryGetUser(userId, out var user)) { return await user.WherePresent(); } @@ -1658,7 +743,7 @@ public async Task> WherePresent(string userId) /// /// /// - /// + /// public bool TryGetUser(string userId, out User user) { user = GetUserAsync(userId).Result; @@ -1692,33 +777,6 @@ public bool TryGetUser(string userId, out User user) } } } - - public bool OLD_TryGetUser(string userId, out User user) - { - var userPointer = pn_chat_get_user(chatPointer, userId); - return OLD_TryGetUser(userId, userPointer, out user); - } - - public async Task OLD_GetUserAsync(string userId) - { - return await Task.Run(() => - { - var result = OLD_TryGetUser(userId, out var user); - return result ? user : null; - }); - } - - internal bool OLD_TryGetUser(IntPtr userPointer, out User user) - { - var id = User.GetUserIdFromPtr(userPointer); - return OLD_TryGetUser(id, userPointer, out user); - } - - internal bool OLD_TryGetUser(string userId, IntPtr userPointer, out User user) - { - return TryGetWrapper(userWrappers, userId, userPointer, () => new User(this, userId, userPointer), - out user); - } /// /// Gets the list of users with the provided parameters. @@ -1781,41 +839,6 @@ public async Task GetUsers(string filter = "", string sort return response; } - public async Task OLD_GetUsers(string filter = "", string sort = "", int limit = 0, - Page 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); - } - - public async Task OLD_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.OLD_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)); - } - } - /// /// Updates the user with the provided user ID. /// @@ -1863,11 +886,7 @@ public async Task UpdateUser(string userId, ChatUserData updatedData) /// public async Task DeleteUser(string userId) { - if (userWrappers.ContainsKey(userId)) - { - userWrappers.Remove(userId); - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_chat_delete_user(chatPointer, userId))); - } + throw new NotImplementedException(); } #endregion @@ -1906,19 +925,7 @@ public async Task GetUserMemberships(string userId, stri string sort = "", int limit = 0, Page page = null) { - if (!OLD_TryGetUser(userId, out var user)) - { - return new MembersResponseWrapper(); - } - - 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); + throw new NotImplementedException(); } public void AddListenerToMembershipsUpdate(List membershipIds, Action listener) @@ -2008,37 +1015,6 @@ public void AddListenerToMembershipsUpdate(List membershipIds, Action OLD_GetChannelMemberships(string channelId, string filter = "", - string sort = "", - int limit = 0, Page page = null) - { - if (!OLD_TryGetChannel(channelId, out var channel)) - { - return new MembersResponseWrapper(); - } - - 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 OLD_TryGetMembership(IntPtr membershipPointer, out Membership membership) - { - var membershipId = Membership.GetMembershipIdFromPtr(membershipPointer); - return OLD_TryGetMembership(membershipId, membershipPointer, out membership); - } - - internal bool OLD_TryGetMembership(string membershipId, IntPtr membershipPointer, out Membership membership) - { - return TryGetWrapper(membershipWrappers, membershipId, membershipPointer, - () => new Membership(this, membershipPointer, membershipId), out membership); - } #endregion @@ -2080,14 +1056,7 @@ public async Task GetMessageReportsHistory(string channelI /// public bool TryGetMessage(string channelId, string messageTimeToken, out Message message) { - if (!OLD_TryGetChannel(channelId, out var channel)) - { - message = null; - return false; - } - - var messagePointer = pn_channel_get_message(channel.Pointer, messageTimeToken); - return TryGetMessage(messageTimeToken, messagePointer, out message); + throw new NotImplementedException(); } /// @@ -2109,20 +1078,7 @@ public async Task MarkAllMessagesAsRead(string filter 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); + throw new NotImplementedException(); } internal bool TryGetAnyMessage(string timeToken, out Message message) @@ -2130,78 +1086,21 @@ internal bool TryGetAnyMessage(string timeToken, out Message message) return messageWrappers.TryGetValue(timeToken, out message); } - internal bool TryGetThreadMessage(string timeToken, IntPtr threadMessagePointer, - out ThreadMessage threadMessage) - { - var found = TryGetWrapper(messageWrappers, timeToken, threadMessagePointer, - () => new ThreadMessage(this, threadMessagePointer, timeToken), out var foundMessage); - if (!found || foundMessage is not ThreadMessage foundThreadMessage) - { - threadMessage = null; - return false; - } - else - { - threadMessage = foundThreadMessage; - return true; - } - } - - 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 = "", int limit = 0, Page 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) - { - return returnWrappers; - } - - foreach (var internalWrapper in internalWrappersList) - { - returnWrappers.Add(new UnreadMessageWrapper(this, internalWrapper)); - } - - return returnWrappers; + throw new NotImplementedException(); } public async Task CreateThreadChannel(Message message) { - if (TryGetThreadChannel(message, out var existingThread)) - { - return existingThread; - } - - 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; + throw new NotImplementedException(); } public async Task RemoveThreadChannel(Message message) { - if (!TryGetThreadChannel(message, out var existingThread)) - { - return; - } - - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_message_remove_thread(message.Pointer))); - channelWrappers.Remove(existingThread.Id); + throw new NotImplementedException(); } /// @@ -2213,38 +1112,7 @@ public async Task RemoveThreadChannel(Message message) /// 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) - { - channelWrappers.Remove(threadId); - threadChannel = null; - return false; - } - - //Existing thread pointer but not cached as a wrapper - if (!channelWrappers.TryGetValue(threadId, out var existingChannel)) - { - CUtilities.CheckCFunctionResult(threadPointer); - threadChannel = new ThreadChannel(this, message, threadPointer); - channelWrappers.Add(threadChannel.Id, threadChannel); - return true; - } - - //Existing wrapper - if (existingChannel is ThreadChannel existingThreadChannel) - { - threadChannel = existingThreadChannel; - threadChannel.UpdatePointer(threadPointer); - return true; - } - else - { - throw new Exception("Chat wrapper error: cached ThreadChannel was of the wrong type!"); - } + throw new NotImplementedException(); } /// @@ -2263,8 +1131,7 @@ public bool TryGetThreadChannel(Message message, out ThreadChannel threadChannel public async Task ForwardMessage(Message message, Channel channel) { - CUtilities.CheckCFunctionResult(await Task.Run(() => - pn_chat_forward_message(chatPointer, message.Pointer, channel.Pointer))); + throw new NotImplementedException(); } public void AddListenerToMessagesUpdate(string channelId, List messageTimeTokens, @@ -2281,7 +1148,7 @@ public void AddListenerToMessagesUpdate(string channelId, List messageTi public async Task PinMessageToChannel(string channelId, Message message) { - if (OLD_TryGetChannel(channelId, out var channel)) + if (TryGetChannel(channelId, out var channel)) { await channel.PinMessage(message); } @@ -2289,7 +1156,7 @@ public async Task PinMessageToChannel(string channelId, Message message) public async Task UnpinMessageFromChannel(string channelId) { - if (OLD_TryGetChannel(channelId, out var channel)) + if (TryGetChannel(channelId, out var channel)) { await channel.UnpinMessage(); } @@ -2322,35 +1189,7 @@ public async Task> GetChannelMessageHistory(string channelId, stri string endTimeToken, int count) { - var messages = new List(); - if (!OLD_TryGetChannel(channelId, out var channel)) - { - 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) - { - return returnMessages; - } - - foreach (var messagePointer in messagePointers) - { - var id = Message.GetMessageIdFromPtr(messagePointer); - if (TryGetMessage(id, messagePointer, out var message)) - { - returnMessages.Add(message); - } - } - - return returnMessages; + throw new NotImplementedException(); } #endregion @@ -2361,18 +1200,7 @@ public async Task GetEventsHistory(string channelId, strin 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)) - { - return new EventsHistoryWrapper(); - ; - } - - return JsonConvert.DeserializeObject(wrapperJson); + throw new NotImplementedException(); } public async Task EmitEvent(PubnubChatEventType type, string channelId, string jsonPayload) @@ -2383,77 +1211,11 @@ public async Task EmitEvent(PubnubChatEventType type, string channelId, string j await PubnubInstance.Publish().Channel(channelId).Message(fullPayload).ExecuteAsync(); } - public async Task OLD_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)) - { - throw new ArgumentException("Channel ID cannot be null or empty."); - } - - 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)) - { - //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) - { - wrappers[id] = createWrapper(); - wrapper = wrappers[id]; - return true; - } - //Updating existing wrapper with updated pointer - else - { - wrapper.UpdatePointer(pointer); - return true; - } - } - //Adding new user to wrappers cache - else if (pointer != IntPtr.Zero) - { - wrapper = createWrapper(); - wrappers.Add(id, wrapper); - return true; - } - else - { - Debug.WriteLine(CUtilities.GetErrorMessage()); - return false; - } - } - 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); + PubnubInstance.Destroy(); } ~Chat() diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs index ef4137e..e44466c 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs @@ -1,42 +1,22 @@ using System; -using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; using PubnubChatApi.Enums; -using PubnubChatApi.Utilities; namespace PubNubChatAPI.Entities { 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); - - [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; + private Chat chat; - 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; + throw new NotImplementedException(); } /// @@ -44,7 +24,7 @@ public async Task CanI(PubnubAccessPermission permission, PubnubAccessReso /// public void SetAuthToken(string token) { - CUtilities.CheckCFunctionResult(pn_pam_set_auth_token(chatPointer, token)); + throw new NotImplementedException(); } /// @@ -53,9 +33,7 @@ public void SetAuthToken(string 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(); + throw new NotImplementedException(); } /// @@ -64,7 +42,7 @@ public string ParseToken(string token) /// public void SetPubnubOrigin(string origin) { - CUtilities.CheckCFunctionResult(pn_pam_set_pubnub_origin(chatPointer, origin)); + throw new NotImplementedException(); } } } \ 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..5ea7f57 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs @@ -10,19 +10,5 @@ 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 int Total; } } \ 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 082b1d0..9f0fe47 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using PubnubApi; namespace PubnubChatApi.Entities.Data @@ -15,40 +16,22 @@ 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 Dictionary ChannelCustomData { get; set; } = new (); public string ChannelUpdated { get; set; } = string.Empty; public string ChannelStatus { get; set; } = string.Empty; public string ChannelType { get; set; } = string.Empty; - /*public static implicit operator ChatChannelData(PNGetChannelMetadataResult metadataResult) - { - return new ChatChannelData() - { - ChannelName = metadataResult.Name, - ChannelDescription = metadataResult.Description, - ChannelCustomDataJson = metadataResult.Custom.TryGetValue("custom", out var custom) ? custom.ToString() : string.Empty, - ChannelStatus = metadataResult.Custom.TryGetValue("status", out var status) ? status.ToString() : string.Empty, - ChannelUpdated = metadataResult.Custom.TryGetValue("updated", out var updated) - ? updated.ToString() - : string.Empty, - ChannelType = metadataResult.Custom.TryGetValue("type", out var dataType) - ? dataType.ToString() - : string.Empty, - }; - }*/ - public static implicit operator ChatChannelData(PNChannelMetadataResult metadataResult) { return new ChatChannelData() { + //TODO: C# FIX ChannelName = metadataResult.Name, ChannelDescription = metadataResult.Description, - ChannelCustomDataJson = metadataResult.Custom.TryGetValue("custom", out var custom) ? custom.ToString() : string.Empty, - ChannelStatus = metadataResult.Custom.TryGetValue("status", out var status) ? status.ToString() : string.Empty, + ChannelCustomData = metadataResult.Custom, + //ChannelStatus = metadataResult.Status, ChannelUpdated = metadataResult.Updated, - ChannelType = metadataResult.Custom.TryGetValue("type", out var dataType) - ? dataType.ToString() - : string.Empty, + //ChannelType = metadataResult.Type }; } } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs index de69ccd..fd34ae2 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs @@ -14,7 +14,6 @@ namespace PubnubChatApi.Entities.Data /// public class ChatMembershipData { - public string OLD_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; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs index 9af984b..3905f66 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs @@ -18,7 +18,6 @@ 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 OLD_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; @@ -35,11 +34,8 @@ public static implicit operator ChatUserData(PNUuidMetadataResult metadataResult Type = metadataResult.Custom.TryGetValue("type", out var dataType) ? dataType.ToString() : string.Empty, - //TODO: won't work? cause it's not a json string i think. to be removed anyway - OLD_CustomDataJson = metadataResult.Custom.TryGetValue("custom", out var customJson) - ? customJson.ToString() - : string.Empty, - CustomData = metadataResult.Custom.TryGetValue("custom", out var custom) ? (Dictionary)custom : new () + //TODO: I think this is correct? + CustomData = metadataResult.Custom//.TryGetValue("custom", out var custom) ? (Dictionary)custom : new () }; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs index 24eee1c..5c322dd 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs @@ -11,21 +11,5 @@ public struct MarkMessagesAsReadWrapper public int Total; public int 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 925afba..ab0aeba 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs @@ -11,25 +11,5 @@ public class MembersResponseWrapper public Page Page = new (); public int Total; public string Status; - - internal MembersResponseWrapper() - { - } - - 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 int Total; - public string Status; } } \ 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 c55a70e..57076ec 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs @@ -13,39 +13,12 @@ public class RateLimitPerChannel public int UnknownConversation; } - //TODO: REMOVE - public string OLD_PublishKey { get; set; } - //TODO: REMOVE - public string OLD_SubscribehKey { get; set; } - //TODO: REMOVE - public string OLD_AuthhKey { get; set; } - //TODO: REMOVE - public string OLD_UserId { get; set; } - public int TypingTimeout { get; } public int TypingTimeoutDifference { get; } public int RateLimitFactor { get; } public RateLimitPerChannel RateLimitsPerChannel { get; } public bool StoreUserActivityTimestamp { get; } public int StoreUserActivityInterval { get; } - - //TODO: REMOVE - public PubnubChatConfig(string publishKey, string subscribeKey, string userId, string authKey = "", - int typingTimeout = 5000, int typingTimeoutDifference = 1000, int rateLimitFactor = 2, - RateLimitPerChannel rateLimitPerChannel = null, bool storeUserActivityTimestamp = false, - int storeUserActivityInterval = 60000) - { - OLD_PublishKey = publishKey; - OLD_SubscribehKey = subscribeKey; - OLD_AuthhKey = authKey; - OLD_UserId = userId; - RateLimitsPerChannel = rateLimitPerChannel; - RateLimitFactor = rateLimitFactor; - StoreUserActivityTimestamp = storeUserActivityTimestamp; - StoreUserActivityInterval = storeUserActivityInterval; - TypingTimeout = typingTimeout; - TypingTimeoutDifference = typingTimeoutDifference; - } public PubnubChatConfig(int typingTimeout = 5000, int typingTimeoutDifference = 1000, int rateLimitFactor = 2, RateLimitPerChannel rateLimitPerChannel = null, bool storeUserActivityTimestamp = false, diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs index 861a65a..ab011de 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs @@ -9,19 +9,5 @@ public struct UnreadMessageWrapper public Channel Channel; 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..bccfa12 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionData.cs @@ -13,26 +13,5 @@ public class UserMentionData 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; } } \ 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..9d8833b 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs @@ -7,20 +7,5 @@ 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 12dd604..9916b77 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs @@ -11,25 +11,5 @@ public struct UsersResponseWrapper public List Users; public PNPageObject Page; public int Total; - - //TODO: REMOVE - internal UsersResponseWrapper(Chat chat, InternalUsersResponseWrapper internalWrapper) - { - Page = new PNPageObject() - { - Next = internalWrapper.Page.Next, - Prev = internalWrapper.Page.Previous - }; - Total = internalWrapper.Total; - Users = PointerParsers.ParseJsonUserPointers(chat, internalWrapper.Users); - } - } - - //TODO: REMOVE - internal struct InternalUsersResponseWrapper - { - public IntPtr[] Users; - public Page Page; - public int Total; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs index 01f4f1f..19506fa 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs @@ -26,55 +26,6 @@ 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 - - /// /// The user ID of the user that this membership belongs to. /// @@ -89,43 +40,6 @@ private static extern void pn_membership_get_membership_data( /// The string time token of last read message on the membership channel. /// public string LastReadMessageTimeToken => MembershipData.CustomData.TryGetValue("lastReadMessageTimetoken", out var timeToken) ? timeToken.ToString() : ""; - - public string OLD_UserId - { - get - { - var buffer = new StringBuilder(512); - pn_membership_get_user_id(pointer, buffer); - return buffer.ToString(); - } - } - - public string OLD_ChannelId - { - get - { - var buffer = new StringBuilder(512); - pn_membership_get_channel_id(pointer, buffer); - return buffer.ToString(); - } - } - - public ChatMembershipData OLD_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); - } - - return data; - } - } public ChatMembershipData MembershipData { get; private set; } @@ -144,17 +58,11 @@ public ChatMembershipData OLD_MembershipData /// }; /// /// - /// + /// public event Action OnMembershipUpdated; private Chat chat; - internal Membership(Chat chat, IntPtr membershipPointer, string membershipId) : base(membershipPointer, - membershipId) - { - this.chat = chat; - } - internal Membership(Chat chat, string userId, string channelId, ChatMembershipData membershipData) : base(userId+channelId) { UserId = userId; @@ -168,34 +76,11 @@ internal void UpdateLocalData(ChatMembershipData newData) MembershipData = newData; } - protected override IntPtr StreamUpdates() - { - return pn_membership_stream_updates(pointer); - } - - internal static string GetMembershipIdFromPtr(IntPtr membershipPointer) - { - 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); - } - /// /// Updates the membership with a ChatMembershipData object. /// @@ -242,27 +127,10 @@ internal async Task UpdateMembershipData(ChatMembershipData membershipData return true; } - - public async Task OLD_Update(ChatMembershipData membershipData) - { - var newPointer = await Task.Run(() => pn_membership_update_dirty(pointer, membershipData.OLD_CustomDataJson, - membershipData.Type, membershipData.Status)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); - } - - public string OLD_GetLastReadMessageTimeToken() - { - var buffer = new StringBuilder(128); - CUtilities.CheckCFunctionResult(pn_membership_last_read_message_timetoken(pointer, buffer)); - return buffer.ToString(); - } public async Task SetLastReadMessage(Message message) { - var newPointer = await Task.Run(() => pn_membership_set_last_read_message(pointer, message.Pointer)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + throw new NotImplementedException(); } public async Task SetLastReadMessageTimeToken(string timeToken) @@ -273,24 +141,15 @@ public async Task SetLastReadMessageTimeToken(string timeToken) await chat.EmitEvent(PubnubChatEventType.Receipt, ChannelId, $"{{\"messageTimetoken\": \"{timeToken}\"}}"); } } - - public async Task OLD_SetLastReadMessageTimeToken(string timeToken) - { - var newPointer = await Task.Run(() => pn_membership_set_last_read_message_timetoken(pointer, timeToken)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); - } - + public async Task GetUnreadMessagesCount() { - var result = await Task.Run(() => pn_membership_get_unread_messages_count(pointer)); - CUtilities.CheckCFunctionResult(result); - return result; + throw new NotImplementedException(); } - protected override void DisposePointer() + public override Task Resync() { - pn_membership_delete(pointer); + throw new NotImplementedException(); } } } \ 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 cdb883c..da554aa 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -23,258 +23,6 @@ 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 - - public virtual string OLD_MessageText - { - get - { - var buffer = new StringBuilder(32768); - CUtilities.CheckCFunctionResult(pn_message_text(pointer, buffer)); - return buffer.ToString(); - } - } - - public virtual string OLD_OriginalMessageText - { - get - { - var buffer = new StringBuilder(32768); - pn_message_get_data_text(pointer, buffer); - return buffer.ToString(); - } - } - - public virtual string OLD_TimeToken - { - get - { - var buffer = new StringBuilder(512); - pn_message_get_timetoken(pointer, buffer); - return buffer.ToString(); - } - } - - public virtual string OLD_ChannelId - { - get - { - var buffer = new StringBuilder(512); - pn_message_get_data_channel_id(pointer, buffer); - return buffer.ToString(); - } - } - - public virtual string OLD_UserId - { - get - { - var buffer = new StringBuilder(512); - pn_message_get_data_user_id(pointer, buffer); - return buffer.ToString(); - } - } - - public virtual string OLD_Meta - { - get - { - var buffer = new StringBuilder(4096); - pn_message_get_data_meta(pointer, buffer); - return buffer.ToString(); - } - } - - public virtual bool OLD_IsDeleted - { - get - { - var result = pn_message_deleted(pointer); - CUtilities.CheckCFunctionResult(result); - return result == 1; - } - } - - public virtual List OLD_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)) - { - 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 virtual List OLD_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)) - { - 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 virtual List OLD_TextLinks - { - get - { - var buffer = new StringBuilder(2048); - CUtilities.CheckCFunctionResult(pn_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; - } - } - - protected List OLD_DeserializeMessageActions(string json) - { - var reactions = new List(); - if (CUtilities.IsValidJson(json)) - { - reactions = JsonConvert.DeserializeObject>(json); - reactions ??= new List(); - } - - return reactions; - } - - public virtual List OLD_MessageActions - { - get - { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(pn_message_get_data_message_actions(pointer, buffer)); - return OLD_DeserializeMessageActions(buffer.ToString()); - } - } - - public virtual List OLD_Reactions - { - get - { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(pn_message_get_reactions(pointer, buffer)); - return OLD_DeserializeMessageActions(buffer.ToString()); - } - } - - public virtual PubnubChatMessageType OLD_Type => (PubnubChatMessageType)pn_message_get_data_type(pointer); - protected Chat chat; /// @@ -391,11 +139,6 @@ public virtual List MentionedUsers { /// public event Action OnMessageUpdated; - internal Message(Chat chat, IntPtr messagePointer, string timeToken) : base(messagePointer, timeToken) - { - this.chat = chat; - } - internal Message(Chat chat, string timeToken,string originalMessageText, string channelId, string userId, Dictionary meta) : base(timeToken) { this.chat = chat; @@ -406,32 +149,6 @@ internal Message(Chat chat, string timeToken,string originalMessageText, string Meta = meta; } - 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(); - } - - 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) - { - var newFullPointer = pn_message_update_with_base_message(partialPointer, pointer); - CUtilities.CheckCFunctionResult(newFullPointer); - UpdatePointer(newFullPointer); - } - internal virtual void BroadcastMessageUpdate() { OnMessageUpdated?.Invoke(this); @@ -454,29 +171,17 @@ internal virtual void BroadcastMessageUpdate() /// public virtual async Task EditMessageText(string newText) { - var newPointer = await Task.Run(() => pn_message_edit_text(pointer, newText)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + throw new NotImplementedException(); } public virtual bool TryGetQuotedMessage(out Message quotedMessage) { - var quotedMessagePointer = pn_message_quoted_message(pointer); - if (quotedMessagePointer == IntPtr.Zero) - { - Debug.WriteLine(CUtilities.GetErrorMessage()); - quotedMessage = null; - return false; - } - - return chat.TryGetMessage(quotedMessagePointer, out quotedMessage); + throw new NotImplementedException(); } public bool HasThread() { - var result = pn_message_has_thread(pointer); - CUtilities.CheckCFunctionResult(result); - return result == 1; + throw new NotImplementedException(); } public async Task CreateThread() @@ -511,17 +216,18 @@ public async Task RemoveThread() public async Task Pin() { - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_message_pin(pointer))); + throw new NotImplementedException(); } public virtual async Task Report(string reason) { - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_message_report(pointer, reason))); + throw new NotImplementedException(); } public virtual async Task Forward(string channelId) { - if (chat.OLD_TryGetChannel(channelId, out var channel)) + var channel = await chat.GetChannelAsync(channelId); + if (channel != null) { await chat.ForwardMessage(this, channel); } @@ -529,23 +235,17 @@ public virtual async Task Forward(string channelId) public virtual bool HasUserReaction(string reactionValue) { - var result = pn_message_has_user_reaction(pointer, reactionValue); - CUtilities.CheckCFunctionResult(result); - return result == 1; + throw new NotImplementedException(); } public virtual async Task ToggleReaction(string reactionValue) { - var newPointer = await Task.Run(() => pn_message_toggle_reaction(pointer, reactionValue)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + throw new NotImplementedException(); } public virtual async Task Restore() { - var newPointer = await Task.Run(() => pn_message_restore(pointer)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + throw new NotImplementedException(); } /// @@ -563,28 +263,16 @@ public virtual async Task Restore() /// message.DeleteMessage(); /// /// - /// + /// /// public virtual async Task Delete(bool soft) { - await Task.Run(() => - { - if (soft) - { - var newPointer = pn_message_delete_message(pointer); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); - } - else - { - CUtilities.CheckCFunctionResult(pn_message_delete_message_hard(pointer)); - } - }); + throw new NotImplementedException(); } - protected override void DisposePointer() + public override Task Resync() { - pn_message_delete(pointer); + throw new NotImplementedException(); } } } \ 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..e2138b9 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs @@ -63,53 +63,6 @@ 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); - - [DllImport("pubnub-chat")] - private static extern int pn_message_draft_remove_text(IntPtr message_draft, int position, int length); - - [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); - - [DllImport("pubnub-chat")] - private static extern int pn_message_draft_add_mention(IntPtr message_draft, int start, int length, - string target); - - [DllImport("pubnub-chat")] - private static extern int pn_message_draft_remove_mention(IntPtr message_draft, int start); - - [DllImport("pubnub-chat")] - private static extern int pn_message_draft_update(IntPtr message_draft, string text); - - [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); - - [DllImport("pubnub-chat")] - private static extern int pn_message_draft_consume_callback_data(IntPtr message_draft, StringBuilder data); - - [DllImport("pubnub-chat")] - private static extern void pn_message_draft_set_search_for_suggestions(IntPtr message_draft, - bool search_for_suggestions); - - #endregion - - private IntPtr pointer; - public event Action, List> OnDraftUpdated; //TODO: will see if these stay non-accessible @@ -139,22 +92,9 @@ private static extern void pn_message_draft_set_search_for_suggestions(IntPtr me /// public Message QuotedMessage { get; }*/ - internal MessageDraft(IntPtr pointer) - { - this.pointer = pointer; - } - private void BroadcastDraftUpdate() { - 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) - { - return; - } - OnDraftUpdated?.Invoke(callbackData.MessageElements, callbackData.SuggestedMentions); + throw new NotImplementedException(); } /// @@ -164,8 +104,7 @@ 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)); - BroadcastDraftUpdate(); + throw new NotImplementedException(); } /// @@ -175,8 +114,7 @@ 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(); + throw new NotImplementedException(); } /// @@ -187,10 +125,7 @@ public void RemoveText(int offset, int length) /// 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)); - BroadcastDraftUpdate(); + throw new NotImplementedException(); } /// @@ -201,9 +136,7 @@ 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)); - BroadcastDraftUpdate(); + throw new NotImplementedException(); } /// @@ -212,8 +145,7 @@ 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)); - BroadcastDraftUpdate(); + throw new NotImplementedException(); } /// @@ -226,8 +158,7 @@ public void RemoveMention(int offset) /// public void Update(string text) { - CUtilities.CheckCFunctionResult(pn_message_draft_update(pointer, text)); - BroadcastDraftUpdate(); + throw new NotImplementedException(); } /// @@ -244,25 +175,12 @@ public async Task Send() /// Additional parameters for sending the message. 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))); + throw new NotImplementedException(); } public void SetSearchForSuggestions(bool searchForSuggestions) { - pn_message_draft_set_search_for_suggestions(pointer, searchForSuggestions); - } - - ~MessageDraft() - { - pn_message_draft_delete(pointer); + throw new NotImplementedException(); } } } \ 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 9cd1bb4..edaa5cf 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -5,57 +5,18 @@ using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; +using PubnubChatApi.Entities.Data; using PubnubChatApi.Utilities; namespace PubNubChatAPI.Entities { public class ThreadChannel : Channel { - #region DLL Imports - - [DllImport("pubnub-chat")] - private static extern void pn_thread_channel_dispose( - IntPtr thread_channel); - - [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 { get { - var buffer = new StringBuilder(128); - CUtilities.CheckCFunctionResult(pn_thread_channel_get_parent_channel_id(pointer, buffer)); - return buffer.ToString(); + throw new NotImplementedException(); } } @@ -63,89 +24,42 @@ public Message ParentMessage { get { - var parentMessagePointer = pn_thread_channel_parent_message(pointer); - CUtilities.CheckCFunctionResult(parentMessagePointer); - chat.TryGetMessage(parentMessagePointer, out var message); - return message; + throw new NotImplementedException(); } } - internal static string MessageToThreadChannelId(Message message) + internal ThreadChannel(Chat chat, string channelId, ChatChannelData data) : base(chat, channelId, data) { - return $"PUBNUB_INTERNAL_THREAD_{message.OLD_ChannelId}_{message.Id}"; } - - internal ThreadChannel(Chat chat, Message sourceMessage, IntPtr channelPointer) : base(chat, - MessageToThreadChannelId(sourceMessage), - channelPointer) + + internal static string MessageToThreadChannelId(Message message) { + return $"PUBNUB_INTERNAL_THREAD_{message.ChannelId}_{message.Id}"; } - + 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); + throw new NotImplementedException(); } public override async Task UnpinMessage() { - var newPointer = await Task.Run(() => pn_thread_channel_unpin_message_from_thread(pointer)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + throw new NotImplementedException(); } 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)) - { - return history; - } - - var messagePointers = JsonConvert.DeserializeObject(messagesPointersJson); - if (messagePointers == null) - { - return history; - } - - foreach (var threadMessagePointer in messagePointers) - { - 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!"); - } - } - return history; + throw new NotImplementedException(); } public async Task PinMessageToParentChannel(ThreadMessage message) { - var newChannelPointer = await Task.Run(() => pn_thread_channel_pin_message_to_parent_channel(pointer, message.Pointer)); - CUtilities.CheckCFunctionResult(newChannelPointer); - chat.UpdateChannelPointer(ParentChannelId, newChannelPointer); + throw new NotImplementedException(); } public async Task UnPinMessageFromParentChannel() { - var newChannelPointer = await Task.Run(() => pn_thread_channel_unpin_message_from_parent_channel(pointer)); - CUtilities.CheckCFunctionResult(newChannelPointer); - chat.UpdateChannelPointer(ParentChannelId, newChannelPointer); - } - - protected override void DisposePointer() - { - pn_thread_channel_dispose(pointer); + throw new NotImplementedException(); } } } \ 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 b4c2df3..efada2d 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -13,303 +13,18 @@ namespace PubNubChatAPI.Entities { 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 OLD_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 OLD_OriginalMessageText - { - get - { - var buffer = new StringBuilder(32768); - pn_thread_message_get_data_text(pointer, buffer); - return buffer.ToString(); - } - } - - /// - /// 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 OLD_TimeToken - { - get - { - var buffer = new StringBuilder(512); - pn_thread_message_get_timetoken(pointer, buffer); - return buffer.ToString(); - } - } - - /// - /// 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 OLD_ChannelId - { - 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 OLD_UserId - { - get - { - var buffer = new StringBuilder(512); - pn_thread_message_get_data_user_id(pointer, buffer); - return buffer.ToString(); - } - } - - public override PubnubChatMessageType OLD_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 OLD_Meta - { - get - { - var buffer = new StringBuilder(4096); - pn_thread_message_get_data_meta(pointer, buffer); - return buffer.ToString(); - } - } - - /// - /// 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 override bool OLD_IsDeleted - { - get - { - var result = pn_thread_message_deleted(pointer); - CUtilities.CheckCFunctionResult(result); - return result == 1; - } - } - - public override List OLD_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 OLD_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 OLD_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 OLD_MessageActions - { - get - { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(pn_thread_message_get_data_message_actions(pointer, buffer)); - return OLD_DeserializeMessageActions(buffer.ToString()); - } - } - - public override List OLD_Reactions - { - get - { - var buffer = new StringBuilder(4096); - CUtilities.CheckCFunctionResult(pn_thread_message_get_reactions(pointer, buffer)); - return OLD_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(); + throw new NotImplementedException(); } } - - internal ThreadMessage(Chat chat, IntPtr messagePointer, string timeToken) : base(chat, messagePointer, - timeToken) - { - } - - protected override IntPtr StreamUpdates() + + internal ThreadMessage(Chat chat, string timeToken, string originalMessageText, string channelId, string userId, Dictionary meta) : base(chat, timeToken, originalMessageText, channelId, userId, meta) { - return pn_thread_message_stream_updates(pointer); } /// @@ -329,72 +44,42 @@ protected override IntPtr StreamUpdates() /// public override async Task EditMessageText(string newText) { - var newPointer = await Task.Run(() => pn_thread_message_edit_text(pointer, newText)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + throw new NotImplementedException(); } 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); + throw new NotImplementedException(); } public override async Task Report(string reason) { - CUtilities.CheckCFunctionResult(await Task.Run(() => pn_thread_message_report(pointer, reason))); + throw new NotImplementedException(); } public override async Task Forward(string channelId) { - if (chat.OLD_TryGetChannel(channelId, out var channel)) - { - await chat.ForwardMessage(this, channel); - } + throw new NotImplementedException(); } public override bool HasUserReaction(string reactionValue) { - var result = pn_thread_message_has_user_reaction(pointer, reactionValue); - CUtilities.CheckCFunctionResult(result); - return result == 1; + throw new NotImplementedException(); } public override async Task ToggleReaction(string reactionValue) { - var newPointer = await Task.Run(() => pn_thread_message_toggle_reaction(pointer, reactionValue)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + throw new NotImplementedException(); } public override async Task Restore() { - var newPointer = await Task.Run(() => pn_thread_message_restore(pointer)); - CUtilities.CheckCFunctionResult(newPointer); - UpdatePointer(newPointer); + throw new NotImplementedException(); } 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)); - } - }); + throw new NotImplementedException(); } internal override void BroadcastMessageUpdate() @@ -403,44 +88,15 @@ internal override void 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); + throw new NotImplementedException(); } 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() - { - pn_thread_message_dispose(pointer); + throw new NotImplementedException(); } + } } \ 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 2ed602c..e2f0910 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -21,135 +21,6 @@ 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 - - public string OLD_UserName - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_user_name(pointer, buffer); - return buffer.ToString(); - } - } - - public string OLD_ExternalId - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_external_id(pointer, buffer); - return buffer.ToString(); - } - } - - public string OLD_ProfileUrl - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_profile_url(pointer, buffer); - return buffer.ToString(); - } - } - - public string OLD_Email - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_email(pointer, buffer); - return buffer.ToString(); - } - } - - public string OLD_CustomData - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_custom_data(pointer, buffer); - return buffer.ToString(); - } - } - - public string OLD_Status - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_status(pointer, buffer); - return buffer.ToString(); - } - } - - public string OLD_DataType - { - get - { - var buffer = new StringBuilder(512); - pn_user_get_data_type(pointer, buffer); - return buffer.ToString(); - } - } - private ChatUserData userData; /// @@ -212,9 +83,7 @@ public bool Active { get { - var result = pn_user_active(pointer); - CUtilities.CheckCFunctionResult(result); - return result == 1; + throw new NotImplementedException(); } } @@ -222,16 +91,11 @@ public string LastActiveTimeStamp { get { - var buffer = new StringBuilder(64); - CUtilities.CheckCFunctionResult(pn_user_last_active_timestamp(pointer, buffer)); - return buffer.ToString(); + throw new NotImplementedException(); } } 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. @@ -249,20 +113,14 @@ public string LastActiveTimeStamp /// }; /// /// - /// + /// /// public event Action OnUserUpdated; public event Action OnMentionEvent; public event Action OnInviteEvent; public event Action OnModerationEvent; - - //TODO: REMOVE - internal User(Chat chat, string userId, IntPtr userPointer) : base(userPointer, userId) - { - this.chat = chat; - } - + internal User(Chat chat, string userId, ChatUserData chatUserData) : base(userId) { UpdateLocalData(chatUserData); @@ -271,8 +129,7 @@ internal User(Chat chat, string userId, ChatUserData chatUserData) : base(userId public async void SetListeningForMentionEvents(bool listen) { - mentionsListeningHandle = await SetListening(mentionsListeningHandle, listen, - () => chat.ListenForEvents(Id, PubnubChatEventType.Mention)); + throw new NotImplementedException(); } internal void BroadcastMentionEvent(ChatEvent chatEvent) @@ -282,8 +139,7 @@ internal void BroadcastMentionEvent(ChatEvent chatEvent) public async void SetListeningForInviteEvents(bool listen) { - invitesListeningHandle = await SetListening(invitesListeningHandle, listen, - () => chat.ListenForEvents(Id, PubnubChatEventType.Invite)); + throw new NotImplementedException(); } internal void BroadcastInviteEvent(ChatEvent chatEvent) @@ -293,8 +149,7 @@ internal void BroadcastInviteEvent(ChatEvent chatEvent) public async void SetListeningForModerationEvents(bool listen) { - moderationListeningHandle = await SetListening(moderationListeningHandle, listen, - () => chat.ListenForEvents($"PUBNUB_INTERNAL_MODERATION.{Id}", PubnubChatEventType.Moderation)); + throw new NotImplementedException(); } internal void BroadcastModerationEvent(ChatEvent chatEvent) @@ -302,35 +157,11 @@ internal void BroadcastModerationEvent(ChatEvent chatEvent) OnModerationEvent?.Invoke(chatEvent); } - protected override IntPtr StreamUpdates() - { - return pn_user_stream_updates(pointer); - } - - internal static string GetUserIdFromPtr(IntPtr userPointer) - { - var buffer = new StringBuilder(512); - pn_user_get_user_id(userPointer, buffer); - return buffer.ToString(); - } - - internal override void UpdateWithPartialPtr(IntPtr partialPointer) - { - var newFullPointer = pn_user_update_with_base(partialPointer, pointer); - CUtilities.CheckCFunctionResult(newFullPointer); - UpdatePointer(newFullPointer); - } - internal void BroadcastUserUpdate() { OnUserUpdated?.Invoke(this); } - public async Task OLD_Update(ChatUserData updatedData) - { - await chat.OLD_UpdateUser(Id, updatedData); - } - /// /// Updates the user. /// @@ -365,12 +196,10 @@ internal static async Task UpdateUserData(Chat chat, string userId, ChatUs .Email(chatUserData.Email) .ExternalId(chatUserData.ExternalId) .ProfileUrl(chatUserData.ProfileUrl) - .Custom(new Dictionary() - { - { "status", chatUserData.Status}, - { "type", chatUserData.Type}, - { "custom", chatUserData.OLD_CustomDataJson} - }) + //TODO: C# FIX + //.Status(chatUserData.Status) + //.Type(chatUserData.Type) + .Custom(chatUserData.CustomData) .ExecuteAsync(); if (result.Status.Error) { @@ -484,35 +313,13 @@ public async Task SetRestriction(string channelId, Restriction restriction) /// 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)) - { - restriction = JsonConvert.DeserializeObject(restrictionJson); - } - - return restriction; + throw new NotImplementedException(); } public async Task GetChannelsRestrictions(string sort = "", int limit = 0, Page 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)) - { - return new ChannelsRestrictionsWrapper(); - } - - var wrapper = JsonConvert.DeserializeObject(restrictionsJson); - wrapper ??= new ChannelsRestrictionsWrapper(); - return wrapper; + throw new NotImplementedException(); } /// @@ -538,9 +345,7 @@ public async Task GetChannelsRestrictions(string so /// public async Task IsPresentOn(string channelId) { - var result = await Task.Run(() => pn_user_is_present_on(pointer, channelId)); - CUtilities.CheckCFunctionResult(result); - return result == 1; + throw new NotImplementedException(); } /// @@ -569,17 +374,7 @@ public async Task IsPresentOn(string channelId) /// 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 != "{}") - { - channelIds = JsonConvert.DeserializeObject>(jsonChannelIds); - channelIds ??= new List(); - } - - return channelIds; + throw new NotImplementedException(); } /// @@ -613,22 +408,5 @@ public async Task GetMemberships(string filter = "", str { 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() - { - pn_user_destroy(pointer); - pointer = IntPtr.Zero; - } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj b/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj index 117b2d3..f4b9fd3 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj @@ -12,12 +12,6 @@ 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/PointerParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/PointerParsers.cs deleted file mode 100644 index 65fac7d..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.OLD_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.OLD_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.OLD_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/pubnub-chat.dll b/c-sharp-chat/PubnubChatApi/PubnubChatApi/pubnub-chat.dll deleted file mode 100644 index 0dba6f92f49abe20c081681589b8f6c358bc5ab9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1172992 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≈S+KzJkH?{ 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%0DBtVPg-PqX&gDY_2)F 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%0A(6D&!%U9S1!%iJEZs2Oo=w-e*W*Xc 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%0A@rW$I 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�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 iWWIAt6YKzf Date: Thu, 24 Jul 2025 15:26:17 +0200 Subject: [PATCH 08/28] working basic message sending and receiving, fixed missing status and type fields in metadata getters/setters --- .../PubNubChatApi.Tests/ChannelTests.cs | 2 +- .../PubNubChatApi.Tests/ChatEventTests.cs | 2 +- .../PubNubChatApi.Tests/ChatTests.cs | 14 +- .../PubNubChatApi.Tests/MembershipTests.cs | 2 +- .../PubNubChatApi.Tests/MessageDraftTests.cs | 2 +- .../PubNubChatApi.Tests/MessageTests.cs | 19 +- .../PubnubTestsParameters.cs | 4 +- .../PubNubChatApi.Tests/RestrictionsTests.cs | 2 +- .../PubNubChatApi.Tests/ThreadsTests.cs | 2 +- .../PubNubChatApi.Tests/UserTests.cs | 2 +- c-sharp-chat/PubnubChatApi/PubnubChatApi.sln | 4 +- .../PubnubChatApi/Entities/Channel.cs | 73 +++++- .../PubnubChatApi/Entities/Chat.cs | 68 +++-- .../Entities/Data/ChatChannelData.cs | 5 +- .../Entities/Data/ChatMembershipData.cs | 5 +- .../Entities/Data/MentionedUser.cs | 4 + .../Entities/Data/PubnubChatConfig.cs | 2 +- .../Entities/Data/SendTextParams.cs | 2 +- .../PubnubChatApi/Entities/Membership.cs | 2 +- .../PubnubChatApi/Entities/Message.cs | 9 +- .../PubnubChatApi/Entities/ThreadMessage.cs | 2 +- .../PubnubChatApi/Entities/User.cs | 43 ++- .../PubnubChatApi/PubnubChatApi.csproj | 2 +- .../PubnubChatApi/Utilities/ChatParsers.cs | 27 +- .../Utilities/ExponentialRateLimiter.cs | 246 ++++++++++++++++++ 25 files changed, 467 insertions(+), 78 deletions(-) create mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ExponentialRateLimiter.cs diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs index d3c7232..1c4e525 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs @@ -15,7 +15,7 @@ public class ChannelTests [SetUp] public async Task Setup() { - chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("ctuuuuuuuuuuuuuuuuuuuuuuuuuuuuu")) + chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("ctuuuuuuuuuuuuuuuuuuuuuuuuuuuuu")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs index f406952..7040d8c 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs @@ -16,7 +16,7 @@ public class ChatEventTests [SetUp] public async Task Setup() { - chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("event_tests_user")) + chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("event_tests_user")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs index a2ac9b7..da86129 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -16,7 +16,7 @@ public class ChatTests [SetUp] public async Task Setup() { - chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("chats_tests_user_10_no_calkiem_nowy_2")) + chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("chats_tests_user_10_no_calkiem_nowy_2")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey @@ -45,9 +45,13 @@ 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 + }} } }); @@ -188,7 +192,7 @@ public async Task TestMarkAllMessagesAsRead() [Test] public async Task TestReadReceipts() { - var otherChat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("other_chat_user")) + var otherChat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("other_chat_user")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey @@ -228,7 +232,7 @@ public async Task TestCanI() { await Task.Delay(4000); - var accessChat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("can_i_test_user")) + var accessChat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("can_i_test_user")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey, diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs index 51e1f67..2f28694 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs @@ -16,7 +16,7 @@ public class MembershipTests [SetUp] public async Task Setup() { - chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("membership_tests_user_54")) + chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("membership_tests_user_54")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs index 32296b2..5019d42 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs @@ -16,7 +16,7 @@ public class MessageDraftTests [SetUp] public async Task Setup() { - chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("message_draft_tests_user")) + chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("message_draft_tests_user")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs index 981065b..2ed9bcc 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs @@ -16,12 +16,16 @@ public class MessageTests [SetUp] public async Task Setup() { - chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("message_tests_user_2")) + chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("message_tests_user_2")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey }); channel = await chat.CreatePublicConversation("message_tests_channel_2"); + if (channel == null) + { + Assert.Fail(); + } if (!chat.TryGetCurrentUser(out user)) { Assert.Fail(); @@ -52,7 +56,12 @@ public async Task TestSendAndReceive() }; await channel.SendText("Test message text", new SendTextParams() { - MentionedUsers = new Dictionary() { { 0, user } }, + //TODO: C# FIX, re-enable as soon as UserMetadata is in correct format + /*MentionedUsers = new Dictionary() { { 0, new MentionedUser() + { + Id = user.Id, + Name = user.UserName + } } },*/ }); var received = manualReceiveEvent.WaitOne(6000); Assert.IsTrue(received); @@ -71,7 +80,11 @@ 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 }); } 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 4d209a9..c3f9640 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs @@ -13,7 +13,7 @@ public class RestrictionsTests [SetUp] public async Task Setup() { - chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("restrictions_tests_user")) + chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("restrictions_tests_user")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs index ada8e8d..565f16b 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs @@ -16,7 +16,7 @@ public class ThreadsTests [SetUp] public async Task Setup() { - chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("threads_tests_user_2")) + chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("threads_tests_user_2")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs index 09922ab..785b583 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs @@ -16,7 +16,7 @@ public class UserTests [SetUp] public async Task Setup() { - chat = new Chat(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("user_tests_user")) + chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("user_tests_user")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi.sln b/c-sharp-chat/PubnubChatApi/PubnubChatApi.sln index e64a13e..5c36ac5 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi.sln +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi.sln @@ -14,8 +14,8 @@ Global GlobalSection(ProjectConfigurationPlatforms) = postSolution {8D3851D4-6FD7-4DE5-9960-DA386442603E}.Release|Any CPU.ActiveCfg = Release|Any CPU {8D3851D4-6FD7-4DE5-9960-DA386442603E}.Release|Any CPU.Build.0 = Release|Any CPU - {8D3851D4-6FD7-4DE5-9960-DA386442603E}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {8D3851D4-6FD7-4DE5-9960-DA386442603E}.Debug|Any CPU.Build.0 = Release|Any CPU + {8D3851D4-6FD7-4DE5-9960-DA386442603E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D3851D4-6FD7-4DE5-9960-DA386442603E}.Debug|Any CPU.Build.0 = Debug|Any CPU {54ACBC4B-510A-499F-9494-24F9F90F7B67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54ACBC4B-510A-499F-9494-24F9F90F7B67}.Debug|Any CPU.Build.0 = Debug|Any CPU {54ACBC4B-510A-499F-9494-24F9F90F7B67}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index e105f89..a7ed8a8 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -165,14 +165,12 @@ internal static async Task UpdateChannelData(Chat chat, string channelId, .Channel(channelId) .Name(data.ChannelName) .Description(data.ChannelDescription) - //TODO: C# FIX - //.Status(data.ChannelStatus) - //.Updated(data.ChannelUpdated) + .Status(data.ChannelStatus) .Custom(data.ChannelCustomData) .ExecuteAsync(); if (result.Status.Error) { - chat.PubnubInstance.PNConfig.Logger?.Error($"Error when trying to set data for channel \"{channelId}\": {result.Status.ErrorData.Information}"); + chat.Logger.Error($"Error when trying to set data for channel \"{channelId}\": {result.Status.ErrorData.Information}"); return false; } return true; @@ -185,7 +183,7 @@ internal static async Task UpdateChannelData(Chat chat, string channelId, .ExecuteAsync(); if (result.Status.Error) { - chat.PubnubInstance.PNConfig.Logger?.Error($"Error when trying to get data for channel \"{channelId}\": {result.Status.ErrorData.Information}"); + chat.Logger.Error($"Error when trying to get data for channel \"{channelId}\": {result.Status.ErrorData.Information}"); return null; } try @@ -428,7 +426,7 @@ public async void Leave() .ExecuteAsync(); if (remove.Status.Error) { - chat.PubnubInstance.PNConfig.Logger?.Error($"Error when trying to leave channel \"{Id}\": {remove.Status.ErrorData.Information}"); + chat.Logger.Error($"Error when trying to leave channel \"{Id}\": {remove.Status.ErrorData.Information}"); return; } @@ -472,7 +470,7 @@ public void Connect() OnMessageReceived?.Invoke(message); } })); - subscription.Subscribe(); + subscription.Subscribe(); } /// @@ -522,10 +520,9 @@ public async void Join(ChatMembershipData? membershipData = null) }).ExecuteAsync(); if (response.Status.Error) { - chat.PubnubInstance.PNConfig.Logger?.Error($"Error when trying to Join() to channel \"{Id}\": {response.Status.ErrorData.Information}"); + chat.Logger.Error($"Error when trying to Join() to channel \"{Id}\": {response.Status.ErrorData.Information}"); return; } - //TODO: wrappers rethink if (chat.membershipWrappers.TryGetValue(currentUserId + Id, out var existingHostMembership)) { @@ -534,6 +531,7 @@ public async void Join(ChatMembershipData? membershipData = null) else { var joinMembership = new Membership(chat, currentUserId, Id, membershipData); + await joinMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()); chat.membershipWrappers.Add(joinMembership.Id, joinMembership); } @@ -592,7 +590,62 @@ public virtual async Task SendText(string message) public virtual async Task SendText(string message, SendTextParams sendTextParams) { - throw new NotImplementedException(); + //TODO: maybe move this to a method in config? + 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"} + }; + //TODO: "meta" here too? + var 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); + } + return await chat.PubnubInstance.Publish() + .Channel(Id) + .ShouldStore(sendTextParams.StoreInHistory) + .UsePOST(sendTextParams.SendByPost) + .Message(chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(messageDict)) + .Meta(meta) + .ExecuteAsync(); + }, response => + { + if (response is PNResult result && result.Status.Error) + { + chat.Logger.Error($"Error occured when trying to SendText(): {result.Status.ErrorData.Information}"); + } + completionSource.SetResult(true); + }, exception => + { + chat.Logger.Error($"Error occured when trying to SendText(): {exception.Message}"); + completionSource.SetResult(true); + }); + + await completionSource.Task; } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 38e5b8f..fad69c1 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -2,11 +2,7 @@ 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 PubnubApi; using PubnubChatApi.Entities.Data; using PubnubChatApi.Entities.Events; @@ -37,12 +33,15 @@ public class Chat private bool fetchUpdates = true; 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; } /// /// Initializes a new instance of the class. @@ -56,14 +55,26 @@ public class Chat /// /// The constructor initializes the Chat object with a new Pubnub instance. /// - public Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatListenerFactory? listenerFactory = null) + public static async Task CreateInstance(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatListenerFactory? listenerFactory = null) + { + var chat = new Chat(chatConfig, pubnubConfig, listenerFactory); + var user = await chat.GetCurrentUserAsync(); + if (user == null) + { + await chat.CreateUser(chat.PubnubInstance.GetCurrentUserId()); + } + return chat; + } + + 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); } - + /// /// Initializes a new instance of the class. /// @@ -76,12 +87,24 @@ public Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatListe /// /// The constructor initializes the Chat object with the provided existing Pubnub instance. /// - public Chat(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? listenerFactory = null) + public static async Task CreateInstance(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? listenerFactory = null) + { + var chat = new Chat(chatConfig, pubnub, listenerFactory); + var user = await chat.GetCurrentUserAsync(); + if (user == null) + { + await chat.CreateUser(chat.PubnubInstance.GetCurrentUserId()); + } + return chat; + } + + 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 @@ -152,7 +175,7 @@ public void AddListenerToChannelsUpdate(List channelIds, Action var existingChannel = await GetChannelAsync(channelId); if (existingChannel != null) { - PubnubInstance.PNConfig.Logger?.Debug("Trying to create a channel with ID that already exists! Returning existing one."); + Logger.Debug("Trying to create a channel with ID that already exists! Returning existing one."); return existingChannel; } @@ -185,7 +208,7 @@ public void AddListenerToChannelsUpdate(List channelIds, Action var existingChannel = await GetChannelAsync(channelId); if (existingChannel != null) { - PubnubInstance.PNConfig.Logger?.Warn("Trying to create a channel with ID that already exists! Aborting."); + Logger.Warn("Trying to create a channel with ID that already exists! Aborting."); return null; } @@ -221,7 +244,7 @@ public void AddListenerToChannelsUpdate(List channelIds, Action if (setMembershipResult.Status.Error) { - PubnubInstance.PNConfig.Logger?.Error($"Error when trying to set memberships for {type} conversation: {setMembershipResult.Status.Error}"); + Logger.Error($"Error when trying to set memberships for {type} conversation: {setMembershipResult.Status.Error}"); return null; } @@ -246,7 +269,7 @@ public void AddListenerToChannelsUpdate(List channelIds, Action var inviteMembership = await InviteToChannel(channelId, users[0].Id); if (inviteMembership == null) { - PubnubInstance.PNConfig.Logger?.Error($"Error when trying to invite user \"{users[0].Id}\" to direct conversation \"{channelId}\": {setMembershipResult.Status.Error}"); + Logger.Error($"Error when trying to invite user \"{users[0].Id}\" to direct conversation \"{channelId}\": {setMembershipResult.Status.Error}"); return null; } responseWrapper.InviteesMemberships = new List() { inviteMembership }; @@ -255,7 +278,7 @@ public void AddListenerToChannelsUpdate(List channelIds, Action var inviteMembership = await InviteMultipleToChannel(channelId, users); if (inviteMembership?.Count == 0) { - PubnubInstance.PNConfig.Logger?.Error($"Error when trying to invite users to group conversation \"{channelId}\": {setMembershipResult.Status.Error}"); + Logger.Error($"Error when trying to invite users to group conversation \"{channelId}\": {setMembershipResult.Status.Error}"); return null; } responseWrapper.InviteesMemberships = new List(inviteMembership); @@ -348,11 +371,14 @@ public async Task> InviteMultipleToChannel(string channelId, Li } var inviteResponse = await PubnubInstance.SetChannelMembers().Channel(channelId) .Include( - //TODO: C# FIX, MISSING VALUES new[] { PNChannelMemberField.UUID, PNChannelMemberField.CUSTOM, - PNChannelMemberField.UUID_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()) @@ -812,7 +838,7 @@ public async Task GetUsers(string filter = "", string sort .Limit(limit).Page(page).ExecuteAsync(); if (result.Status.Error) { - PubnubInstance.PNConfig.Logger?.Error($"Error when trying to GetUsers(): {result.Status.ErrorData.Information}"); + Logger.Error($"Error when trying to GetUsers(): {result.Status.ErrorData.Information}"); return default; } @@ -974,16 +1000,17 @@ public void AddListenerToMembershipsUpdate(List membershipIds, Action() { sort }) .Limit(limit).Page(page).ExecuteAsync(); if (result.Status.Error) { - PubnubInstance.PNConfig.Logger?.Error($"Error when trying to get \"{channelId}\" channel members: {result.Status.ErrorData.Information}"); + Logger.Error($"Error when trying to get \"{channelId}\" channel members: {result.Status.ErrorData.Information}"); return null; } @@ -1216,6 +1243,7 @@ public async Task EmitEvent(PubnubChatEventType type, string channelId, string j public void Destroy() { PubnubInstance.Destroy(); + RateLimiter.Dispose(); } ~Chat() diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs index 9f0fe47..52f7db1 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs @@ -25,13 +25,12 @@ public static implicit operator ChatChannelData(PNChannelMetadataResult metadata { return new ChatChannelData() { - //TODO: C# FIX ChannelName = metadataResult.Name, ChannelDescription = metadataResult.Description, ChannelCustomData = metadataResult.Custom, - //ChannelStatus = metadataResult.Status, + ChannelStatus = metadataResult.Status, ChannelUpdated = metadataResult.Updated, - //ChannelType = metadataResult.Type + ChannelType = metadataResult.Type }; } } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs index fd34ae2..45ea749 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs @@ -20,12 +20,11 @@ public class ChatMembershipData public static implicit operator ChatMembershipData(PNChannelMembersItemResult membersItem) { - //TODO: C# FIX, MISSING VALUES return new ChatMembershipData() { CustomData = membersItem.Custom, - //Status = membersItem.Status, - //Type = membersItem.Type + Status = membersItem.Status, + Type = membersItem.Type }; } } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MentionedUser.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MentionedUser.cs index 2f4f9b2..207a195 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MentionedUser.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MentionedUser.cs @@ -1,8 +1,12 @@ +using Newtonsoft.Json; + namespace PubnubChatApi.Entities.Data { 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/PubnubChatConfig.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs index 57076ec..9f53116 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs @@ -24,7 +24,7 @@ public PubnubChatConfig(int typingTimeout = 5000, int typingTimeoutDifference = RateLimitPerChannel rateLimitPerChannel = null, bool storeUserActivityTimestamp = false, int storeUserActivityInterval = 60000) { - RateLimitsPerChannel = rateLimitPerChannel; + RateLimitsPerChannel = rateLimitPerChannel ?? new RateLimitPerChannel(); RateLimitFactor = rateLimitFactor; StoreUserActivityTimestamp = storeUserActivityTimestamp; StoreUserActivityInterval = storeUserActivityInterval; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs index 0a466bc..5ca7315 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs @@ -8,7 +8,7 @@ public class SendTextParams public bool StoreInHistory = true; public bool SendByPost = false; public string Meta = string.Empty; - public Dictionary MentionedUsers = new(); + public Dictionary MentionedUsers = new(); public Message QuotedMessage = null; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs index 19506fa..7059c89 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs @@ -121,7 +121,7 @@ internal async Task UpdateMembershipData(ChatMembershipData membershipData if (updateResponse.Status.Error) { - chat.PubnubInstance.PNConfig.Logger?.Error($"Error when trying to update membership (channel: {ChannelId}, user: {UserId}): {updateResponse.Status.ErrorData.Information}"); + chat.Logger.Error($"Error when trying to update membership (channel: {ChannelId}, user: {UserId}): {updateResponse.Status.ErrorData.Information}"); return false; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs index da554aa..a6ff598 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -77,7 +77,7 @@ public virtual string MessageText { /// It can be used to store additional information about the message. /// /// - public Dictionary Meta { get; internal set; } + public Dictionary Meta { get; internal set; } = new (); /// /// Whether the message has been deleted. @@ -103,7 +103,7 @@ public virtual List MentionedUsers { } } - public virtual List MessageActions { get; internal set; } + public virtual List MessageActions { get; internal set; } = new(); public virtual List Reactions => MessageActions.Where(x => x.Type == PubnubMessageActionType.Reaction).ToList(); @@ -116,7 +116,7 @@ public virtual List MentionedUsers { /// /// /// - public virtual PubnubChatMessageType Type { get; protected set; } + public virtual PubnubChatMessageType Type { get; internal set; } /// @@ -139,13 +139,14 @@ public virtual List MentionedUsers { /// public event Action OnMessageUpdated; - internal Message(Chat chat, string timeToken,string originalMessageText, string channelId, string userId, Dictionary meta) : base(timeToken) + internal Message(Chat chat, string timeToken,string originalMessageText, string channelId, string userId, PubnubChatMessageType type, Dictionary meta) : base(timeToken) { this.chat = chat; TimeToken = timeToken; OriginalMessageText = originalMessageText; ChannelId = channelId; UserId = userId; + Type = type; Meta = meta; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs index efada2d..3b36d23 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -23,7 +23,7 @@ public string ParentChannelId } } - internal ThreadMessage(Chat chat, string timeToken, string originalMessageText, string channelId, string userId, Dictionary meta) : base(chat, timeToken, originalMessageText, channelId, userId, meta) + internal ThreadMessage(Chat chat, string timeToken, string originalMessageText, string channelId, string userId, PubnubChatMessageType type, Dictionary meta) : base(chat, timeToken, originalMessageText, channelId, userId, type, meta) { } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index e2f0910..9ff3978 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; @@ -190,17 +191,37 @@ public async Task Update(ChatUserData updatedData) internal static async Task UpdateUserData(Chat chat, string userId, ChatUserData chatUserData) { - var result = await chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true) - .Uuid(userId) - .Name(chatUserData.Username) - .Email(chatUserData.Email) - .ExternalId(chatUserData.ExternalId) - .ProfileUrl(chatUserData.ProfileUrl) - //TODO: C# FIX - //.Status(chatUserData.Status) - //.Type(chatUserData.Type) - .Custom(chatUserData.CustomData) - .ExecuteAsync(); + //TODO: Create a better way to do this + var operation = chat.PubnubInstance.SetUuidMetadata().IncludeCustom(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.Any()) + { + operation = operation.Custom(chatUserData.CustomData); + } + var result = await operation.ExecuteAsync(); if (result.Status.Error) { chat.PubnubInstance.PNConfig.Logger.Error($"Error when trying to update user data for user \"{userId}\": {result.Status.ErrorData.Information}"); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj b/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj index f4b9fd3..967191e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj @@ -14,7 +14,7 @@ - + diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs index e692f24..e6620de 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using Newtonsoft.Json.Linq; using PubnubApi; using PubNubChatAPI.Entities; +using PubnubChatApi.Enums; namespace PubnubChatApi.Utilities { @@ -11,13 +13,32 @@ internal static bool TryParseMessageResult(Chat chat, PNMessageResult me { try { - //TODO: don't know if UserMetadata is a JSON string or a Dict so we'll see if this breaks I guess? - var meta = messageResult.UserMetadata as Dictionary; - message = new Message(chat, messageResult.Timetoken.ToString(), messageResult.Message.ToString(), messageResult.Channel, messageResult.Publisher, meta); + var messageDict = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(messageResult.Message + .ToString()); + + //TODO: later more types I guess? + var type = PubnubChatMessageType.Text; //messageDict["type"].ToString(); + var text = messageDict["text"].ToString(); + + //TODO: C# FIX, USER METADATA SHOULD BE A DICTIONARY + var meta = new Dictionary(); + if (messageResult.UserMetadata != null) + { + //TODO: REMOVE AS SOON AS C# FIX + var metaJObject = (JObject)messageResult.UserMetadata; + foreach (var kvp in metaJObject) + { + meta.Add(kvp.Key, kvp.Value.ToString()); + } + } + + message = new Message(chat, messageResult.Timetoken.ToString(), text, messageResult.Channel, messageResult.Publisher, type, meta); 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; } 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..88c2810 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ExponentialRateLimiter.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace PubnubChatApi.Utilities +{ + 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 void RunWithinLimits(string id, int baseIntervalMs, Func> task, Action callback, Action errorCallback) + { + if (baseIntervalMs == 0) + { + // Execute immediately for zero interval + _ = Task.Run(async () => + { + try + { + var result = await task(); + 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); + } + else + { + toSleep -= slept; + } + + slept = Math.Min(toSleep, ThreadsMaxSleepMs); + + if (slept > 0) + { + await Task.Delay(slept, _cancellationTokenSource.Token); + } + } + } + + 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(id, 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); + } + + // Remove finished limiters + foreach (var id in itemsToRemove) + { + _limiters.TryRemove(id, out _); + } + + if (_limiters.IsEmpty) + { + return NoSleepRequired; + } + + return toSleep; + } + + private async Task ProcessLimiterAsync(string id, 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(); + 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 From 39a085f6035a99a51a3ffdbf9067549867ea3ed3 Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Tue, 19 Aug 2025 16:48:13 +0200 Subject: [PATCH 09/28] implement message draft --- .../PubNubChatApi.Tests/ChannelTests.cs | 12 - .../PubNubChatApi.Tests/MembershipTests.cs | 10 +- .../PubNubChatApi.Tests/MessageDraftTests.cs | 71 +- .../PubnubChatApi/Entities/Base/ChatEntity.cs | 7 +- .../PubnubChatApi/Entities/Channel.cs | 34 +- .../PubnubChatApi/Entities/Chat.cs | 142 +- .../Entities/Data/ChannelsResponseWrapper.cs | 5 +- .../Entities/Data/ChatMembershipData.cs | 6 +- .../Entities/Data/ChatUserData.cs | 6 +- .../Entities/Data/MembersResponseWrapper.cs | 3 +- .../Entities/Data/ReferencedChannel.cs | 12 + .../Entities/Data/SendTextParams.cs | 2 +- .../PubnubChatApi/Entities/Data/TextLink.cs | 7 +- .../PubnubChatApi/Entities/Membership.cs | 58 +- .../PubnubChatApi/Entities/Message.cs | 73 +- .../PubnubChatApi/Entities/MessageDraft.cs | 591 ++++- .../PubnubChatApi/Entities/User.cs | 10 +- .../PubnubChatApi/PubnubChatApi.csproj | 2 +- .../PubnubChatApi/Utilities/ChatParsers.cs | 44 +- .../PubnubChatApi/Utilities/DiffMatchPatch.cs | 2296 +++++++++++++++++ 20 files changed, 3223 insertions(+), 168 deletions(-) create mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ReferencedChannel.cs create mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/DiffMatchPatch.cs diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs index 1c4e525..193558f 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs @@ -109,18 +109,6 @@ public async Task TestLeaveChannel() Assert.False(memberships.Memberships.Any(x => x.UserId == currentChatUser.Id), "Leave failed, current user found in channel memberships"); } - - [Test] - public async Task TestGetUserSuggestions() - { - var channel = await chat.CreatePublicConversation("user_suggestions_test_channel"); - channel.Join(); - - await Task.Delay(5000); - - var suggestions = await channel.GetUserSuggestions("@Test"); - Assert.True(suggestions.Any(x => x.UserId == user.Id)); - } [Test] public async Task TestGetMemberships() diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs index 2f28694..e3c0a96 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs @@ -22,6 +22,10 @@ public async Task Setup() SubscribeKey = PubnubTestsParameters.SubscribeKey }); channel = await chat.CreatePublicConversation("membership_tests_channel"); + if (channel == null) + { + Assert.Fail(); + } if (!chat.TryGetCurrentUser(out user)) { Assert.Fail(); @@ -34,6 +38,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(); @@ -63,7 +69,9 @@ public async Task TestUpdateMemberships() CustomData = new Dictionary() { {"key", Guid.NewGuid().ToString()} - } + }, + Type = "some_membership", + Status = "active" }; var manualUpdatedEvent = new ManualResetEvent(false); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs index 5019d42..ff2197c 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs @@ -77,10 +77,9 @@ void InsertDelegateCallback(List elements, List @@ -101,8 +100,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 }); @@ -113,9 +111,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; @@ -124,57 +126,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/Entities/Base/ChatEntity.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs index 26f7576..706986f 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs @@ -2,17 +2,20 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading.Tasks; +using PubnubApi; using PubnubChatApi.Utilities; namespace PubNubChatAPI.Entities { public abstract class ChatEntity { - public abstract Task Resync(); - + protected Subscription? updateSubscription; + public virtual void SetListeningForUpdates(bool listen) { throw new NotImplementedException(); } + + public abstract Task Resync(); } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index a7ed8a8..da30ae9 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -6,6 +6,7 @@ using PubnubApi; using PubnubChatApi.Entities.Data; using PubnubChatApi.Entities.Events; +using PubnubChatApi.Enums; using PubnubChatApi.Utilities; namespace PubNubChatAPI.Entities @@ -309,12 +310,12 @@ public async Task EmitUserMention(string userId, string timeToken, string text) public async Task StartTyping() { - throw new NotImplementedException(); + await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":true}}"); } public async Task StopTyping() { - throw new NotImplementedException(); + await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":false}}"); } public virtual async Task PinMessage(Message message) @@ -352,11 +353,6 @@ public bool TryGetPinnedMessage(out Message pinnedMessage) }); } - public async Task> GetUserSuggestions(string text, int limit = 10) - { - throw new NotImplementedException(); - } - /// /// Creates a new MessageDraft. /// @@ -364,11 +360,12 @@ 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. /// 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) { - throw new NotImplementedException(); + return new MessageDraft(chat, this, userSuggestionSource, isTypingIndicatorTriggered, userLimit, channelLimit, shouldSearchForSuggestions); } /// @@ -607,8 +604,7 @@ public virtual async Task SendText(string message, SendTextParams sendTextParams {"text", message}, {"type", "text"} }; - //TODO: "meta" here too? - var meta = new Dictionary(); + var meta = sendTextParams.Meta ?? new Dictionary(); if (sendTextParams.QuotedMessage != null) { //TODO: may create some "ToJSON()" methods for chat entities @@ -770,7 +766,21 @@ public async Task IsUserPresent(string userId) /// public async Task> WhoIsPresent() { - throw new NotImplementedException(); + var result = new List(); + var response = await chat.PubnubInstance.HereNow().Channels(new[] { Id }).IncludeState(true) + .IncludeUUIDs(true).ExecuteAsync(); + if (response.Status.Error) + { + chat.Logger.Error($"Error when trying to perform WhoIsPresent(): {response.Status.ErrorData.Information}"); + return result; + } + + foreach (var occupant in response.Result.Channels[Id].Occupants) + { + result.Add(occupant.Uuid); + } + + return result; } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index fad69c1..4bf88bf 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -472,9 +472,53 @@ public bool TryGetChannel(string channelId, out Channel channel) } public async Task GetChannels(string filter = "", string sort = "", int limit = 0, - Page page = null) + PNPageObject page = null) { - throw new NotImplementedException(); + 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(); + + 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) + { + if (channelWrappers.TryGetValue(resultMetadata.Channel, out var existingChannelWrapper)) + { + existingChannelWrapper.UpdateLocalData(resultMetadata); + wrapper.Channels.Add(existingChannelWrapper); + } + else + { + var channel = new Channel(this, resultMetadata.Channel, resultMetadata); + channelWrappers.Add(channel.Id, channel); + wrapper.Channels.Add(channel); + } + } + return wrapper; } /// @@ -834,8 +878,25 @@ public bool TryGetUser(string userId, out User user) public async Task GetUsers(string filter = "", string sort = "", int limit = 0, PNPageObject page = null) { - var result = await PubnubInstance.GetAllUuidMetadata().Filter(filter).Sort(new List() { sort }) - .Limit(limit).Page(page).ExecuteAsync(); + 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 result = await operation.ExecuteAsync(); + if (result.Status.Error) { Logger.Error($"Error when trying to GetUsers(): {result.Status.ErrorData.Information}"); @@ -949,9 +1010,68 @@ public async Task DeleteUser(string userId) /// public async Task GetUserMemberships(string userId, string filter = "", string sort = "", - int limit = 0, Page page = null) + int limit = 0, PNPageObject page = null) { - throw new NotImplementedException(); + + //TODO: here also, has to be a better way to structure this arguments -> builder pattern + 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 result = await operation.ExecuteAsync(); + if (result.Status.Error) + { + Logger.Error($"Error when trying to get \"{userId}\" user memberships: {result.Status.ErrorData.Information}"); + return null; + } + + var memberships = new List(); + foreach (var membershipResult in result.Result.Memberships) + { + var membershipId = userId + membershipResult.ChannelMetadata.Channel; + if (membershipWrappers.TryGetValue(membershipId, out var existingMembershipWrapper)) + { + existingMembershipWrapper.MembershipData.CustomData = membershipResult.Custom; + memberships.Add(existingMembershipWrapper); + } + else + { + memberships.Add(new Membership(this, userId, membershipResult.ChannelMetadata.Channel, new ChatMembershipData() + { + CustomData = membershipResult.Custom, + Status = membershipResult.Status, + Type = membershipResult.Type + })); + } + } + return new MembersResponseWrapper() + { + Memberships = memberships, + Page = result.Result.Page, + Total = result.Result.TotalCount + }; } public void AddListenerToMembershipsUpdate(List membershipIds, Action listener) @@ -1027,18 +1147,16 @@ public void AddListenerToMembershipsUpdate(List membershipIds, Action 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/ChatMembershipData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs index 45ea749..0e76957 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatMembershipData.cs @@ -14,9 +14,9 @@ namespace PubnubChatApi.Entities.Data /// public class ChatMembershipData { - public Dictionary CustomData { get; set; } = new (); - 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) { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs index 3905f66..3a0ed40 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs @@ -30,10 +30,8 @@ public static implicit operator ChatUserData(PNUuidMetadataResult metadataResult Email = metadataResult.Email, ProfileUrl = metadataResult.ProfileUrl, Username = metadataResult.Name, - Status = metadataResult.Custom.TryGetValue("status", out var status) ? status.ToString() : string.Empty, - Type = metadataResult.Custom.TryGetValue("type", out var dataType) - ? dataType.ToString() - : string.Empty, + Status = metadataResult.Status, + Type = metadataResult.Type, //TODO: I think this is correct? CustomData = metadataResult.Custom//.TryGetValue("custom", out var custom) ? (Dictionary)custom : new () }; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs index ab0aeba..47b41fe 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Utilities; @@ -8,7 +9,7 @@ namespace PubnubChatApi.Entities.Data public class MembersResponseWrapper { public List Memberships = new (); - public Page Page = new (); + public PNPageObject Page = new (); public int Total; public string Status; } 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..85a4625 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ReferencedChannel.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace PubnubChatApi.Entities.Data +{ + 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/SendTextParams.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs index 5ca7315..83ed1fd 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs @@ -7,7 +7,7 @@ public class SendTextParams { public bool StoreInHistory = true; public bool SendByPost = false; - public string Meta = string.Empty; + public Dictionary Meta = new(); public Dictionary MentionedUsers = new(); public Message QuotedMessage = null; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/TextLink.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/TextLink.cs index 54cf3f7..fae4f6a 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 @@ +using Newtonsoft.Json; + namespace PubnubChatApi.Entities.Data { 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/Membership.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs index 7059c89..2037083 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json; using PubnubApi; using PubnubChatApi.Entities.Data; using PubnubChatApi.Enums; @@ -26,6 +23,9 @@ namespace PubNubChatAPI.Entities /// public class Membership : UniqueChatEntity { + //Message counts requires a valid timetoken, so this one will be like "0", from beginning of the channel + private const long EMPTY_TIMETOKEN = 17000000000000000; + /// /// The user ID of the user that this membership belongs to. /// @@ -76,9 +76,30 @@ internal void UpdateLocalData(ChatMembershipData newData) MembershipData = newData; } - internal void BroadcastMembershipUpdate() + public override void SetListeningForUpdates(bool listen) { - OnMembershipUpdated?.Invoke(this); + if (listen) + { + if (updateSubscription != null) + { + return; + } + updateSubscription = chat.PubnubInstance.Channel(ChannelId).Subscription(SubscriptionOptions.ReceivePresenceEvents); + updateSubscription.AddListener(chat.ListenerFactory.ProduceListener(objectEventCallback: + delegate(Pubnub pn, PNObjectEventResult e) + { + if (ChatParsers.TryParseMembershipUpdate(chat, this, e, out var updatedData)) + { + UpdateLocalData(updatedData); + OnMembershipUpdated?.Invoke(this); + } + })); + updateSubscription.Subscribe(); + } + else + { + updateSubscription?.Unsubscribe(); + } } /// @@ -116,7 +137,9 @@ internal async Task UpdateMembershipData(ChatMembershipData membershipData PNMembershipField.CUSTOM, PNMembershipField.STATUS, PNMembershipField.CHANNEL, - PNMembershipField.CHANNEL_CUSTOM + PNMembershipField.CHANNEL_CUSTOM, + PNMembershipField.CHANNEL_TYPE, + PNMembershipField.CHANNEL_STATUS }).ExecuteAsync(); if (updateResponse.Status.Error) @@ -130,7 +153,7 @@ internal async Task UpdateMembershipData(ChatMembershipData membershipData public async Task SetLastReadMessage(Message message) { - throw new NotImplementedException(); + await SetLastReadMessageTimeToken(message.TimeToken); } public async Task SetLastReadMessageTimeToken(string timeToken) @@ -142,14 +165,27 @@ public async Task SetLastReadMessageTimeToken(string timeToken) } } - public async Task GetUnreadMessagesCount() + public async Task GetUnreadMessagesCount() { - throw new NotImplementedException(); + if (!long.TryParse(LastReadMessageTimeToken, out var lastRead)) + { + chat.Logger.Error("LastReadMessageTimeToken is not a valid time token!"); + return -1; + } + lastRead = lastRead == 0 ? EMPTY_TIMETOKEN : lastRead; + var countsResponse = await chat.PubnubInstance.MessageCounts().Channels(new[] { ChannelId }) + .ChannelsTimetoken(new[] { lastRead }).ExecuteAsync(); + if (countsResponse.Status.Error) + { + chat.Logger.Error($"Error when trying to get message counts on channel \"{ChannelId}\": {countsResponse.Status.ErrorData}"); + return -1; + } + return countsResponse.Result.Channels[ChannelId]; } - public override Task Resync() + public override async Task Resync() { - throw new NotImplementedException(); + await chat.GetChannelMemberships(ChannelId, filter:$"uuid.id == \"{UserId}\""); } } } \ 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 a6ff598..9e2352e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -93,15 +93,80 @@ public virtual List MentionedUsers { get { var mentioned = new List(); - if (Meta.TryGetValue("mentionedUsers", out var rawMentionedUsers)) + if (!Meta.TryGetValue("mentionedUsers", out var rawMentionedUsers)) { - //TODO: might break - var deserialized = chat.PubnubInstance.JsonPluggableLibrary.DeserializeToObject>(rawMentionedUsers.ToString()); - mentioned.AddRange(deserialized); + 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; } } + + public virtual 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; + } + } + + public virtual 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; + } + } public virtual List MessageActions { get; internal set; } = new(); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs index e2138b9..94fba24 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs @@ -1,12 +1,11 @@ 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 { @@ -31,7 +30,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,38 +81,186 @@ private class DraftCallbackDataHelper public List SuggestedMentions; } - public event Action, List> OnDraftUpdated; + // 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; - //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]. + /// Gets the current text of the draft /// - private UserSuggestionSource UserSuggestionSourceSetting { get; } + public string Text => _value; + /// - /// Whether modifying the message text triggers the typing indicator on [channel]. + /// Gets the current message elements /// - public bool IsTypingIndicatorTriggered { get; } + 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() : new List(); + OnDraftUpdated?.Invoke(messageElements, suggestedMentions); + } + catch (Exception e) + { + chat.Logger.Error($"Error has occured when trying to broadcast MessageDraft update: {e.Message}"); + } + } + /// - /// The limit on the number of users returned when searching for users to mention. + /// Generates suggested mentions based on current text patterns /// - public int UserLimit { get; } + 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); + if (usersWrapper.Users != null && usersWrapper.Users.Any()) + { + var user = usersWrapper.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)) + { + continue; + } + } + else + { + continue; + } + break; + case MentionType.Channel: + var channelsWrapper = await chat.GetChannels(filter: $"name LIKE \"{rawMention.Target.Target}*\"", + limit: channelLimit); + 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; + } + suggestions.Add(suggestion); + } + + return suggestions; + } + /// - /// The limit on the number of channels returned when searching for channels to reference. + /// Suggests raw mentions based on regex patterns in the text /// - public int ChannelLimit { get; } + 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; + } + /// - /// Can be used to set a [Message] to quote when sending this [MessageDraft]. + /// Insert mention into the MessageDraft according to SuggestedMention.Offset, SuggestedMention.ReplaceFrom and + /// SuggestedMention.target. /// - public Message QuotedMessage { get; }*/ - - 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) { - throw new NotImplementedException(); + 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(); } /// @@ -104,7 +270,38 @@ private void BroadcastDraftUpdate() /// Text the text to insert at the given offset public void InsertText(int offset, string text) { - throw new NotImplementedException(); + 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(); } /// @@ -114,18 +311,41 @@ 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) { - throw new NotImplementedException(); - } + 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) - { - throw new NotImplementedException(); + 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(); } /// @@ -136,7 +356,15 @@ public void InsertSuggestedMention(SuggestedMention mention, string text) /// The target of the mention public void AddMention(int offset, int length, MentionTarget target) { - throw new NotImplementedException(); + 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(); } /// @@ -145,7 +373,13 @@ public void AddMention(int offset, int length, MentionTarget target) /// Offset the start of the mention to remove public void RemoveMention(int offset) { - throw new NotImplementedException(); + // 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(); } /// @@ -158,7 +392,109 @@ public void RemoveMention(int offset) /// public void Update(string text) { - throw new NotImplementedException(); + if (text == null) 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; + } + } + + BroadcastDraftUpdate(); + } + + /// + /// Internal method to apply text insertion without triggering broadcasts + /// + private void ApplyInsertTextInternal(int offset, string text) + { + if (text == null) 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; } /// @@ -170,17 +506,186 @@ public async Task Send() } /// - /// Send the MessageDraft, along with its quotedMessage if any, on the channel. + /// Send the rendered MessageDraft on the channel. /// /// Additional parameters for sending the message. public async Task Send(SendTextParams sendTextParams) { - throw new NotImplementedException(); + 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; + await channel.SendText(Render(), sendTextParams); + } + + /// + /// 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 + /// + 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; } - public void SetSearchForSuggestions(bool searchForSuggestions) + /// + /// Renders the message with markdown-style links + /// + 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; + } + } + } + + return result.ToString(); + } + + private async void TriggerTypingIndicator() + { + if (isTypingIndicatorTriggered && channel.Type == "public") + { + await channel.StartTyping(); + } + } + + /// + /// 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) { - throw new NotImplementedException(); + return url?.Replace("\\", "\\\\").Replace(")", "\\)") ?? string.Empty; } } } \ 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 9ff3978..e834ab1 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -366,7 +366,13 @@ public async Task GetChannelsRestrictions(string so /// public async Task IsPresentOn(string channelId) { - throw new NotImplementedException(); + var response = await chat.PubnubInstance.WhereNow().Uuid(Id).ExecuteAsync(); + if (response.Status.Error) + { + chat.Logger.Error($"Error when trying to perform IsPresentOn(): {response.Status.ErrorData.Information}"); + return false; + } + return response.Result.Channels.Contains(channelId); } /// @@ -425,7 +431,7 @@ public async Task> WherePresent() /// /// public async Task GetMemberships(string filter = "", string sort = "", int limit = 0, - Page page = null) + PNPageObject page = null) { return await chat.GetUserMemberships(Id, filter, sort, limit, page); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj b/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj index 967191e..5a10b29 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj @@ -14,7 +14,7 @@ - + diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs index e6620de..e57922a 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json.Linq; using PubnubApi; using PubNubChatAPI.Entities; +using PubnubChatApi.Entities.Data; using PubnubChatApi.Enums; namespace PubnubChatApi.Utilities @@ -21,17 +22,7 @@ internal static bool TryParseMessageResult(Chat chat, PNMessageResult me var type = PubnubChatMessageType.Text; //messageDict["type"].ToString(); var text = messageDict["text"].ToString(); - //TODO: C# FIX, USER METADATA SHOULD BE A DICTIONARY - var meta = new Dictionary(); - if (messageResult.UserMetadata != null) - { - //TODO: REMOVE AS SOON AS C# FIX - var metaJObject = (JObject)messageResult.UserMetadata; - foreach (var kvp in metaJObject) - { - meta.Add(kvp.Key, kvp.Value.ToString()); - } - } + var meta = messageResult.UserMetadata ?? new Dictionary(); message = new Message(chat, messageResult.Timetoken.ToString(), text, messageResult.Channel, messageResult.Publisher, type, meta); return true; @@ -43,5 +34,36 @@ internal static bool TryParseMessageResult(Chat chat, PNMessageResult me return false; } } + + internal static bool TryParseMembershipUpdate(Chat chat, Membership membership, PNObjectEventResult objectEvent, out ChatMembershipData updatedData) + { + try + { + var channel = objectEvent.ChannelMetadata.Channel; + var user = objectEvent.UuidMetadata.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; + } + } } } \ 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..58b08b8 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/DiffMatchPatch.cs @@ -0,0 +1,2296 @@ +/* + * 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; + } + 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 From 5fd0f9842bbd93da04b4d1f8075971f92756edbe Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Wed, 20 Aug 2025 17:28:16 +0200 Subject: [PATCH 10/28] implement restrictions getting/setting, start conversion to returning ChatOperationResult in methods --- .../PubNubChatApi.Tests/ChannelTests.cs | 2 +- .../PubNubChatApi.Tests/RestrictionsTests.cs | 64 ++- .../PubNubChatApi.Tests/TestUtils.cs | 17 +- .../PubnubChatApi/Entities/Channel.cs | 155 ++++-- .../PubnubChatApi/Entities/Chat.cs | 507 ++++++++++-------- .../Data/ChannelsRestrictionsWrapper.cs | 4 +- .../Entities/Data/ChatOperationResult.cs | 45 ++ .../Entities/Data/UsersRestrictionsWrapper.cs | 4 +- .../PubnubChatApi/Entities/Message.cs | 11 +- .../PubnubChatApi/Entities/ThreadChannel.cs | 4 +- .../PubnubChatApi/Entities/ThreadMessage.cs | 2 +- .../PubnubChatApi/Entities/User.cs | 131 +++-- 12 files changed, 602 insertions(+), 344 deletions(-) create mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatOperationResult.cs diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs index 193558f..324e520 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs @@ -88,7 +88,7 @@ public async Task TestDeleteChannel() [Test] public async Task TestLeaveChannel() { - var currentChatUser = await chat.GetCurrentUserAsync(); + var currentChatUser = await chat.GetCurrentUser(); Assert.IsNotNull(currentChatUser, "currentChatUser was null"); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs index c3f9640..ce41f54 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; @@ -13,11 +12,16 @@ public class RestrictionsTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("restrictions_tests_user")) + var createChat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("restrictions_tests_user")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey }); + if (createChat.Error) + { + Assert.Fail($"Failed to create chat! Error: {createChat.Exception.Message}"); + } + chat = createChat.Result; } [TearDown] @@ -31,8 +35,13 @@ public async Task CleanUp() public async Task TestSetRestrictions() { var user = await chat.GetOrCreateUser("user123"); - var channel = await chat.CreatePublicConversation("new_channel"); - + var createChannel = await chat.CreatePublicConversation("new_channel"); + if (createChannel.Error) + { + Assert.Fail($"Failed to create channel, error: {createChannel.Exception.Message}"); + } + var channel = createChannel.Result; + await Task.Delay(2000); var restriction = new Restriction() @@ -41,16 +50,30 @@ public async Task TestSetRestrictions() Mute = true, Reason = "Some Reason" }; - await channel.SetRestrictions(user.Id, restriction); + var setRestrictions = await channel.SetRestrictions(user.Id, restriction); + if (setRestrictions.Error) + { + Assert.Fail($"Failed to set restrictions, error: {setRestrictions.Exception.Message}"); + } await Task.Delay(3000); - var fetchedRestriction = await channel.GetUserRestrictions(user); + var getUser = await channel.GetUserRestrictions(user); + if (getUser.Error) + { + Assert.Fail($"Failed to fetch User restrictions. Exception: {getUser.Exception}"); + } + var fetchedRestriction = getUser.Result; Assert.True(restriction.Ban == fetchedRestriction.Ban && restriction.Mute == fetchedRestriction.Mute && restriction.Reason == fetchedRestriction.Reason); - var restrictionFromUser = await user.GetChannelRestrictions(channel); + var getChannel = await user.GetChannelRestrictions(channel); + if (getChannel.Error) + { + Assert.Fail($"Failed to fetch Channel restrictions. Exception: {getChannel.Exception}"); + } + var restrictionFromUser = getChannel.Result; Assert.True(restriction.Ban == restrictionFromUser.Ban && restriction.Mute == restrictionFromUser.Mute && restriction.Reason == restrictionFromUser.Reason); @@ -60,7 +83,12 @@ public async Task TestSetRestrictions() public async Task TestGetRestrictionsSets() { var user = await chat.GetOrCreateUser("user1234"); - var channel = await chat.CreatePublicConversation("new_channel_2"); + var createChannel = await chat.CreatePublicConversation("new_channel"); + if (createChannel.Error) + { + Assert.Fail($"Failed to create channel, error: {createChannel.Exception.Message}"); + } + var channel = createChannel.Result; await Task.Delay(4000); @@ -70,14 +98,26 @@ public async Task TestGetRestrictionsSets() Mute = true, Reason = "Some Reason" }; - await channel.SetRestrictions(user.Id, restriction); - + var setRestrictions = await channel.SetRestrictions(user.Id, restriction); + if (setRestrictions.Error) + { + Assert.Fail($"Failed to set restrictions, error: {setRestrictions.Exception.Message}"); + } + await Task.Delay(4000); var a = await channel.GetUsersRestrictions(); + if (a.Error) + { + Assert.Fail($"Failed to fetch Users restrictions. Exception: {a.Exception}"); + } var b = await user.GetChannelsRestrictions(); + if (b.Error) + { + Assert.Fail($"Failed to fetch Channels restrictions. Exception: {b.Exception}"); + } - Assert.True(a.Restrictions.Any(x => x.UserId == user.Id)); - Assert.True(b.Restrictions.Any(x => x.ChannelId == channel.Id)); + Assert.True(a.Result.Restrictions.Any(x => x.UserId == user.Id)); + Assert.True(b.Result.Restrictions.Any(x => x.ChannelId == channel.Id)); } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs index 5108a6e..a97710a 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs @@ -6,13 +6,18 @@ public static class TestUtils { public static async Task GetOrCreateUser(this Chat chat, string userId) { - if (chat.TryGetUser(userId, out var user)) + var getUser = await chat.GetUser(userId); + if (getUser.Error) { - return user; - } - else - { - return await chat.CreateUser(userId); + var createUser = await chat.CreateUser(userId); + if (createUser.Error) + { + Assert.Fail($"Failed to create User! Error: {createUser.Exception.Message}"); + }else + { + return createUser.Result; + } } + return getUser.Result; } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index da30ae9..8d6573b 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -160,48 +160,31 @@ internal void UpdateLocalData(ChatChannelData? newData) channelData = newData; } - internal static async Task UpdateChannelData(Chat chat, string channelId, ChatChannelData data) + internal static async Task> UpdateChannelData(Chat chat, string channelId, ChatChannelData data) { - var result = await chat.PubnubInstance.SetChannelMetadata().IncludeCustom(true) + return await chat.PubnubInstance.SetChannelMetadata().IncludeCustom(true) .Channel(channelId) .Name(data.ChannelName) .Description(data.ChannelDescription) .Status(data.ChannelStatus) .Custom(data.ChannelCustomData) .ExecuteAsync(); - if (result.Status.Error) - { - chat.Logger.Error($"Error when trying to set data for channel \"{channelId}\": {result.Status.ErrorData.Information}"); - return false; - } - return true; } - internal static async Task GetChannelData(Chat chat, string channelId) + internal static async Task> GetChannelData(Chat chat, string channelId) { - var result = await chat.PubnubInstance.GetChannelMetadata().IncludeCustom(true) + return await chat.PubnubInstance.GetChannelMetadata().IncludeCustom(true) .Channel(channelId) .ExecuteAsync(); - if (result.Status.Error) - { - chat.Logger.Error($"Error when trying to get data for channel \"{channelId}\": {result.Status.ErrorData.Information}"); - return null; - } - try - { - return (ChatChannelData)result.Result; - } - catch (Exception e) - { - chat.PubnubInstance.PNConfig.Logger.Error($"Error when trying to parse data for Channel \"{channelId}\": {e.Message}"); - return null; - } } public override async Task Resync() { - var newData = await GetChannelData(chat, Id); - UpdateLocalData(newData); + var getResult = await GetChannelData(chat, Id); + if (!getResult.Status.Error) + { + UpdateLocalData(getResult.Result); + } } public async void SetListeningForCustomEvents(bool listen) @@ -256,7 +239,15 @@ internal void BroadcastChannelUpdate() internal async void BroadcastPresenceUpdate() { - OnPresenceUpdate?.Invoke(await WhoIsPresent()); + var whoIs = await WhoIsPresent(); + if (whoIs.Error) + { + chat.Logger.Error($"Error when trying to broadcast presence update after WhoIs(): {whoIs.Exception.Message}"); + } + else + { + OnPresenceUpdate?.Invoke(whoIs.Result); + } } internal void TryParseAndBroadcastTypingEvent(List userIds) @@ -318,12 +309,12 @@ public async Task StopTyping() await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":false}}"); } - public virtual async Task PinMessage(Message message) + public virtual async Task PinMessage(Message message) { throw new NotImplementedException(); } - public virtual async Task UnpinMessage() + public virtual async Task UnpinMessage() { throw new NotImplementedException(); } @@ -552,16 +543,15 @@ public async void Join(ChatMembershipData? membershipData = null) /// 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) { - throw new NotImplementedException(); + return await chat.SetRestriction(userId, Id, banUser, muteUser, reason); } - public async Task SetRestrictions(string userId, Restriction restriction) + 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); } /// @@ -713,15 +703,84 @@ public async Task Delete() /// /// Thrown when an error occurs while getting the user restrictions. /// - public async Task GetUserRestrictions(User user) + public async Task> GetUserRestrictions(User user) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + var membershipsResult = await chat.PubnubInstance.GetMemberships().Uuid(user.Id).Include(new[] + { + PNMembershipField.CUSTOM + }).Filter($"channel.id == \"{Chat.INTERNAL_MODERATION_PREFIX}_{Id}\"").IncludeCount(true).ExecuteAsync(); + 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; } - public async Task GetUsersRestrictions(string sort = "", int limit = 0, - Page page = null) + public async Task> GetUsersRestrictions(string sort = "", int limit = 0, + PNPageObject page = null) { - throw new NotImplementedException(); + var result = new ChatOperationResult(){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(); + 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; } /// @@ -741,7 +800,7 @@ public async Task GetUsersRestrictions(string sort = " /// /// Thrown when an error occurs while checking the presence of the user. /// - public async Task IsUserPresent(string userId) + public async Task> IsUserPresent(string userId) { throw new NotImplementedException(); } @@ -764,22 +823,20 @@ public async Task IsUserPresent(string userId) /// /// Thrown when an error occurs while getting the list of users present in the channel. /// - public async Task> WhoIsPresent() + public async Task>> WhoIsPresent() { - var result = new List(); + var result = new ChatOperationResult>() { Result = new List() }; var response = await chat.PubnubInstance.HereNow().Channels(new[] { Id }).IncludeState(true) .IncludeUUIDs(true).ExecuteAsync(); - if (response.Status.Error) + if (result.RegisterOperation(response)) { - chat.Logger.Error($"Error when trying to perform WhoIsPresent(): {response.Status.ErrorData.Information}"); return result; } foreach (var occupant in response.Result.Channels[Id].Occupants) { - result.Add(occupant.Uuid); + result.Result.Add(occupant.Uuid); } - return result; } @@ -805,7 +862,7 @@ public async Task> WhoIsPresent() /// /// Thrown when an error occurs while getting the list of memberships. /// - public async Task GetMemberships(string filter = "", string sort = "", int limit = 0, + public async Task> GetMemberships(string filter = "", string sort = "", int limit = 0, PNPageObject page = null) { return await chat.GetChannelMemberships(Id, filter, sort, limit, page); @@ -852,12 +909,12 @@ public async Task> GetMessageHistory(string startTimeToken, string return await chat.GetChannelMessageHistory(Id, startTimeToken, endTimeToken, count); } - public async Task Invite(User user) + public async Task> Invite(User user) { return await chat.InviteToChannel(Id, user.Id); } - public async Task> InviteMultiple(List users) + public async Task>> InviteMultiple(List users) { return await chat.InviteMultipleToChannel(Id, users); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 4bf88bf..77a492e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -25,6 +25,11 @@ namespace PubNubChatAPI.Entities /// public class Chat { + internal const string INTERNAL_MODERATION_PREFIX = "PUBNUB_INTERNAL_MODERATION"; + internal const string INTERNAL_ADMIN_CHANNEL = "PUBNUB_INTERNAL_ADMIN_CHANNEL"; + internal const string MESSAGE_THREAD_ID_PREFIX = "PUBNUB_INTERNAL_THREAD"; + internal const string ERROR_LOGGER_KEY_PREFIX = "PUBNUB_INTERNAL_ERROR_LOGGER"; + private Dictionary channelWrappers = new(); private Dictionary userWrappers = new(); //TODO: wrappers rethink @@ -55,15 +60,16 @@ public class Chat /// /// The constructor initializes the Chat object with a new Pubnub instance. /// - public static async Task CreateInstance(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatListenerFactory? listenerFactory = null) + public static async Task> CreateInstance(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatListenerFactory? listenerFactory = null) { var chat = new Chat(chatConfig, pubnubConfig, listenerFactory); - var user = await chat.GetCurrentUserAsync(); - if (user == null) + var result = new ChatOperationResult(){Result = chat}; + var getUser = await chat.GetCurrentUser(); + if (result.RegisterOperation(getUser)) { - await chat.CreateUser(chat.PubnubInstance.GetCurrentUserId()); + result.RegisterOperation(await chat.CreateUser(chat.PubnubInstance.GetCurrentUserId())); } - return chat; + return result; } internal Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatListenerFactory? listenerFactory = null) @@ -90,7 +96,7 @@ internal Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatLis public static async Task CreateInstance(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? listenerFactory = null) { var chat = new Chat(chatConfig, pubnub, listenerFactory); - var user = await chat.GetCurrentUserAsync(); + var user = await chat.GetCurrentUser(); if (user == null) { await chat.CreateUser(chat.PubnubInstance.GetCurrentUserId()); @@ -109,13 +115,14 @@ internal Chat(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? l #region Channels - public void AddListenerToChannelsUpdate(List channelIds, Action listener) + public async Task AddListenerToChannelsUpdate(List channelIds, Action listener) { foreach (var channelId in channelIds) { - if (TryGetChannel(channelId, out var channel)) + var getResult = await GetChannel(channelId); + if (!getResult.Error) { - channel.OnChannelUpdate += listener; + getResult.Result.OnChannelUpdate += listener; } } } @@ -139,7 +146,7 @@ public void AddListenerToChannelsUpdate(List channelIds, Action /// /// /// - public async Task CreatePublicConversation(string channelId = "") + public async Task> CreatePublicConversation(string channelId = "") { if (string.IsNullOrEmpty(channelId)) { @@ -170,54 +177,56 @@ public void AddListenerToChannelsUpdate(List channelIds, Action /// /// /// - public async Task CreatePublicConversation(string channelId, ChatChannelData additionalData) + public async Task> CreatePublicConversation(string channelId, ChatChannelData additionalData) { - var existingChannel = await GetChannelAsync(channelId); - if (existingChannel != null) + var result = new ChatOperationResult(); + var existingChannel = await GetChannel(channelId); + if (!result.RegisterOperation(existingChannel)) { Logger.Debug("Trying to create a channel with ID that already exists! Returning existing one."); - return existingChannel; + result.Result = existingChannel.Result; + return result; } additionalData.ChannelType = "public"; var updated = await Channel.UpdateChannelData(this, channelId, additionalData); - if (updated) + if (result.RegisterOperation(updated)) { - var channel = new Channel(this, channelId, additionalData); - channelWrappers.Add(channelId, channel); - return channel; - } - else - { - return null; + return result; } + var channel = new Channel(this, channelId, additionalData); + result.Result = channel; + channelWrappers.Add(channelId, channel); + return result; } - private async Task CreateConversation( + private async Task> CreateConversation( string type, List users, string channelId = "", ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) { + var result = new ChatOperationResult(){Result = new CreatedChannelWrapper()}; + if (string.IsNullOrEmpty(channelId)) { channelId = Guid.NewGuid().ToString(); } - var existingChannel = await GetChannelAsync(channelId); - if (existingChannel != null) + var existingChannel = await GetChannel(channelId); + if (!result.RegisterOperation(existingChannel)) { - Logger.Warn("Trying to create a channel with ID that already exists! Aborting."); - return null; + Logger.Debug("Trying to create a channel with ID that already exists! Returning existing one."); + return result; } channelData ??= new ChatChannelData(); channelData.ChannelType = type; var updated = await Channel.UpdateChannelData(this, channelId, channelData); - if (!updated) + if (result.RegisterOperation(updated)) { - return null; + return result; } membershipData ??= new ChatMembershipData(); @@ -241,24 +250,22 @@ public void AddListenerToChannelsUpdate(List channelIds, Action Type = membershipData.Type }}) .ExecuteAsync(); - - if (setMembershipResult.Status.Error) + + if (result.RegisterOperation(setMembershipResult)) { - Logger.Error($"Error when trying to set memberships for {type} conversation: {setMembershipResult.Status.Error}"); - return null; + return result; } - - var responseWrapper = new CreatedChannelWrapper(); + if (membershipWrappers.TryGetValue(currentUserId + channelId, out var existingHostMembership)) { existingHostMembership.UpdateLocalData(membershipData); - responseWrapper.HostMembership = existingHostMembership; + result.Result.HostMembership = existingHostMembership; } else { var hostMembership = new Membership(this, currentUserId, channelId, membershipData); membershipWrappers.Add(hostMembership.Id, hostMembership); - responseWrapper.HostMembership = hostMembership; + result.Result.HostMembership = hostMembership; } var channel = new Channel(this, channelId, channelData); @@ -267,57 +274,56 @@ public void AddListenerToChannelsUpdate(List channelIds, Action if (type == "direct") { var inviteMembership = await InviteToChannel(channelId, users[0].Id); - if (inviteMembership == null) + if (result.RegisterOperation(inviteMembership)) { - Logger.Error($"Error when trying to invite user \"{users[0].Id}\" to direct conversation \"{channelId}\": {setMembershipResult.Status.Error}"); - return null; + return result; } - responseWrapper.InviteesMemberships = new List() { inviteMembership }; + result.Result.InviteesMemberships = new List() { inviteMembership.Result }; }else if (type == "group") { var inviteMembership = await InviteMultipleToChannel(channelId, users); - if (inviteMembership?.Count == 0) + if (result.RegisterOperation(inviteMembership)) { - Logger.Error($"Error when trying to invite users to group conversation \"{channelId}\": {setMembershipResult.Status.Error}"); - return null; + return result; } - responseWrapper.InviteesMemberships = new List(inviteMembership); + result.Result.InviteesMemberships = new List(inviteMembership.Result); } - return responseWrapper; + return result; } - public async Task CreateDirectConversation(User user, string channelId = "", + public async Task> CreateDirectConversation(User user, string channelId = "", ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) { return await CreateConversation("direct", new List() { user }, channelId, channelData, membershipData); } - public async Task CreateGroupConversation(List users, string channelId = "", + public async Task> CreateGroupConversation(List users, string channelId = "", ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) { return await CreateConversation("group", users, channelId, channelData, membershipData); } - public async Task InviteToChannel(string channelId, string userId) + public async Task> InviteToChannel(string channelId, string userId) { + var result = new ChatOperationResult(); //Check if already a member first var members = await GetChannelMemberships(channelId, filter:$"uuid.id == \"{userId}\""); - if (members != null && members.Memberships.Any()) + if (!result.RegisterOperation(members)) { //Already a member, just return current membership - return members.Memberships[0]; + result.Result = members.Result.Memberships[0]; + return result; } - var channel = await GetChannelAsync(channelId); - if (channel == null) + var channel = await GetChannel(channelId); + if (result.RegisterOperation(channel)) { - PubnubInstance.PNConfig?.Logger.Error($"Error: tried to invite user \"{userId}\" to channel \"{channelId}\" but such channel doesn't exist!"); - return null; + return result; } - var response = await PubnubInstance.SetMemberships().Uuid(userId).Include(new[] + var setMemberships = await PubnubInstance.SetMemberships().Uuid(userId).Include(new[] { PNMembershipField.CUSTOM, PNMembershipField.TYPE, @@ -337,37 +343,36 @@ public void AddListenerToChannelsUpdate(List channelIds, Action } }).ExecuteAsync(); - if (response.Status.Error) + if (result.RegisterOperation(setMemberships)) { - PubnubInstance.PNConfig?.Logger.Error($"Error when trying to invite user \"{userId}\" to channel \"{channelId}\": {response.Status.ErrorData.Information}"); - return null; + return result; } - var newMataData = response.Result.Memberships?.FirstOrDefault(x => x.ChannelMetadata.Channel == channelId)? + var newMataData = setMemberships.Result.Memberships?.FirstOrDefault(x => x.ChannelMetadata.Channel == channelId)? .ChannelMetadata; if (newMataData != null) { - channel.UpdateLocalData(newMataData); + channel.Result.UpdateLocalData(newMataData); } - var inviteEventPayload = $"{{\"channelType\": \"{channel.Type}\", \"channelId\": {channelId}}}"; + var inviteEventPayload = $"{{\"channelType\": \"{channel.Result.Type}\", \"channelId\": {channelId}}}"; await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload); var newMembership = new Membership(this, userId, channelId, new ChatMembershipData()); await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()); membershipWrappers.Add(newMembership.Id, newMembership); - return newMembership; + result.Result = newMembership; + return result; } - public async Task> InviteMultipleToChannel(string channelId, List users) + public async Task>> InviteMultipleToChannel(string channelId, List users) { - var memberships = new List(); - var channel = await GetChannelAsync(channelId); - if (channel == null) + var result = new ChatOperationResult>() { Result = new List() }; + var channel = await GetChannel(channelId); + if (result.RegisterOperation(channel)) { - PubnubInstance.PNConfig?.Logger.Error($"Error: tried to invite multiple users to channel \"{channelId}\" but such channel doesn't exist!"); - return memberships; + return result; } var inviteResponse = await PubnubInstance.SetChannelMembers().Channel(channelId) .Include( @@ -384,10 +389,9 @@ public async Task> InviteMultipleToChannel(string channelId, Li .Uuids(users.Select(x => new PNChannelMember() { Custom = x.CustomData, Uuid = x.Id }).ToList()) .ExecuteAsync(); - if (inviteResponse.Status.Error) + if (result.RegisterOperation(inviteResponse)) { - PubnubInstance.PNConfig?.Logger.Error($"Error when trying to invite multiple users to channel \"{channelId}\": {inviteResponse.Status.ErrorData.Information}"); - return memberships; + return result; } var usersDict = users.ToDictionary(x => x.Id, y => y); @@ -399,48 +403,23 @@ public async Task> InviteMultipleToChannel(string channelId, Li { usersDict[userId].UpdateLocalData(channelMember.UuidMetadata); existingMembership.UpdateLocalData(channelMember); - memberships.Add(existingMembership); + result.Result.Add(existingMembership); } else { var newMembership = new Membership(this, userId, channelId, channelMember); await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()); membershipWrappers.Add(newMembership.Id, newMembership); - memberships.Add(newMembership); + result.Result.Add(newMembership); } - var inviteEventPayload = $"{{\"channelType\": \"{channel.Type}\", \"channelId\": {channelId}}}"; + var inviteEventPayload = $"{{\"channelType\": \"{channel.Result.Type}\", \"channelId\": {channelId}}}"; await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload); } - await channel.Resync(); + await channel.Result.Resync(); - return memberships; - } - - /// - /// 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 - /// } - /// - /// - /// - /// - public bool TryGetChannel(string channelId, out Channel channel) - { - channel = GetChannelAsync(channelId).Result; - return channel != null; + return result; } /// @@ -448,27 +427,26 @@ public bool TryGetChannel(string channelId, out Channel channel) /// /// ID of the channel. /// Channel object if it exists, null otherwise. - public async Task GetChannelAsync(string channelId) + public async Task> GetChannel(string channelId) { + var result = new ChatOperationResult(); if (channelWrappers.TryGetValue(channelId, out var existingChannel)) { await existingChannel.Resync(); - return existingChannel; + result.Result = existingChannel; } else { - var data = await Channel.GetChannelData(this, channelId); - if (data == null) - { - return null; - } - else + var getResult = await Channel.GetChannelData(this, channelId); + if (result.RegisterOperation(getResult)) { - var channel = new Channel(this, channelId, data); - channelWrappers.Add(channelId, channel); - return channel; + return result; } + var channel = new Channel(this, channelId, getResult.Result); + channelWrappers.Add(channelId, channel); + result.Result = channel; } + return result; } public async Task GetChannels(string filter = "", string sort = "", int limit = 0, @@ -573,27 +551,22 @@ public async Task GetCurrentUserMentions(string startTimeTo { throw new NotImplementedException(); } - - /// - /// Tries to retrieve the current User object for this chat. - /// - /// The retrieved current User object. - /// True if chat has a current user, false otherwise. - /// - public bool TryGetCurrentUser(out User user) - { - user = GetCurrentUserAsync().Result; - return user != null; - } /// /// 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() + public async Task> GetCurrentUser() { + var result = new ChatOperationResult(); var userId = PubnubInstance.GetCurrentUserId(); - return await GetUserAsync(userId); + var getUser = await GetUser(userId); + if (result.RegisterOperation(getUser)) + { + return result; + } + result.Result = getUser.Result; + return result; } /// @@ -607,30 +580,90 @@ 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. /// /// - /// var chat = // ... - /// chat.SetRestrictions("user_id", "channel_id", true, true, "Spamming"); + /// 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) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + var restrictionsChannelId = $"{INTERNAL_MODERATION_PREFIX}_{channelId}"; + var getResult = await Channel.GetChannelData(this, restrictionsChannelId); + if (result.RegisterOperation(getResult)) + { + if (result.RegisterOperation(await Channel.UpdateChannelData(this, restrictionsChannelId, + new ChatChannelData()))) + { + 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())) + { + return result; + } + result.RegisterOperation(await EmitEvent(PubnubChatEventType.Moderation, moderationEventsChannelId, + $"{{\"channelId\": \"{channelId}\", \"restriction\": \"lifted\", \"reason\": \"{reason}\"}}")); + 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())) + { + return result; + } + result.RegisterOperation(await EmitEvent(PubnubChatEventType.Moderation, moderationEventsChannelId, + $"{{\"channelId\": \"{channelId}\", \"restriction\": \"{(banUser ? "banned" : "muted")}\", \"reason\": \"{reason}\"}}")); + 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. + /// + /// + /// 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); } - public void AddListenerToUsersUpdate(List userIds, Action listener) + public async void AddListenerToUsersUpdate(List userIds, Action listener) { foreach (var userId in userIds) { - if (TryGetUser(userId, out var user)) + var getUser = await GetUser(userId); + if (!getUser.Error) { - user.OnUserUpdated += listener; + getUser.Result.OnUserUpdated += listener; } } } @@ -654,7 +687,7 @@ public void AddListenerToUsersUpdate(List userIds, Action listener /// /// /// - public async Task CreateUser(string userId) + public async Task> CreateUser(string userId) { return await CreateUser(userId, new ChatUserData()); } @@ -676,26 +709,25 @@ public void AddListenerToUsersUpdate(List userIds, Action listener /// /// /// - public async Task CreateUser(string userId, ChatUserData additionalData) + public async Task> CreateUser(string userId, ChatUserData additionalData) { - var existingUser = await GetUserAsync(userId); - if (existingUser != null) + var result = new ChatOperationResult(); + var existingUser = await GetUser(userId); + if (!result.RegisterOperation(result)) { - Debug.WriteLine("Trying to create a user with ID that already exists! Returning existing one."); - return existingUser; + result.Result = existingUser.Result; + return result; } - var updated = await User.UpdateUserData(this, userId, additionalData); - if (updated) - { - var user = new User(this, userId, additionalData); - userWrappers.Add(userId, user); - return user; - } - else + var update = await User.UpdateUserData(this, userId, additionalData); + if (result.RegisterOperation(update)) { - return null; + return result; } + var user = new User(this, userId, additionalData); + userWrappers.Add(userId, user); + result.Result = user; + return result; } /// @@ -718,16 +750,21 @@ public void AddListenerToUsersUpdate(List userIds, Action listener /// /// /// - 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(); + var getChannel = await GetChannel(channelId); + if (result.RegisterOperation(getChannel)) { - return await channel.IsUserPresent(userId); + return result; } - else + var isPresent = await getChannel.Result.IsUserPresent(userId); + if (result.RegisterOperation(isPresent)) { - return false; + return result; } + result.Result = isPresent.Result; + return result; } /// @@ -750,16 +787,21 @@ public async Task IsPresent(string userId, string channelId) /// /// /// - public async Task> WhoIsPresent(string channelId) + public async Task>> WhoIsPresent(string channelId) { - if (TryGetChannel(channelId, out var channel)) + var result = new ChatOperationResult>() { Result = new List() }; + var getChannel = await GetChannel(channelId); + if (result.RegisterOperation(getChannel)) { - return await channel.WhoIsPresent(); + return result; } - else + var whoIs = await getChannel.Result.WhoIsPresent(); + if (result.RegisterOperation(whoIs)) { - return new List(); + return result; } + result.Result = whoIs.Result; + return result; } /// @@ -782,42 +824,21 @@ public async Task> WhoIsPresent(string channelId) /// /// /// - public async Task> WherePresent(string userId) + public async Task>> WherePresent(string userId) { - if (TryGetUser(userId, out var user)) + var result = new ChatOperationResult>() { Result = new List() }; + var getUser = await GetUser(userId); + if (result.RegisterOperation(getUser)) { - return await user.WherePresent(); + return result; } - else + var wherePresent = await getUser.Result.WherePresent(); + 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) - { - user = GetUserAsync(userId).Result; - return user != null; + result.Result = wherePresent.Result; + return result; } /// @@ -825,27 +846,24 @@ public bool TryGetUser(string userId, out User user) /// /// ID of the User to get. /// User object if one with given ID is found, null otherwise. - public async Task GetUserAsync(string userId) + public async Task> GetUser(string userId) { + var result = new ChatOperationResult(); if (userWrappers.TryGetValue(userId, out var existingUser)) { await existingUser.Resync(); - return existingUser; + result.Result = existingUser; + return result; } - else + var getData = await User.GetUserData(this, userId); + if (result.RegisterOperation(getData)) { - var data = await User.GetUserData(this, userId); - if (data == null) - { - return null; - } - else - { - var user = new User(this, userId, data); - userWrappers.Add(userId, user); - return user; - } + return result; } + var user = new User(this, userId, getData.Result); + userWrappers.Add(userId, user); + result.Result = user; + return result; } /// @@ -1113,11 +1131,12 @@ public void AddListenerToMembershipsUpdate(List membershipIds, Action /// /// - public async Task GetChannelMemberships(string channelId, string filter = "", + public async Task> GetChannelMemberships(string channelId, string filter = "", string sort = "", int limit = 0, PNPageObject page = null) { - var result = await PubnubInstance.GetChannelMembers().Include( + var result = new ChatOperationResult(); + var operation = PubnubInstance.GetChannelMembers().Include( new[] { PNChannelMemberField.UUID_CUSTOM, @@ -1126,16 +1145,32 @@ public void AddListenerToMembershipsUpdate(List membershipIds, Action() { sort }) - .Limit(limit).Page(page).ExecuteAsync(); - if (result.Status.Error) + }).Channel(channelId); + if (!string.IsNullOrEmpty(filter)) { - Logger.Error($"Error when trying to get \"{channelId}\" channel members: {result.Status.ErrorData.Information}"); - return null; + 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(); + if (result.RegisterOperation(getResult)) + { + return result; } var memberships = new List(); - foreach (var channelMemberResult in result.Result.ChannelMembers) + foreach (var channelMemberResult in getResult.Result.ChannelMembers) { var membershipId = channelMemberResult.UuidMetadata.Uuid + channelId; if (membershipWrappers.TryGetValue(membershipId, out var existingMembershipWrapper)) @@ -1153,12 +1188,13 @@ public void AddListenerToMembershipsUpdate(List membershipIds, Action ForwardMessage(Message message, Channel channel) { throw new NotImplementedException(); } @@ -1291,20 +1327,30 @@ public void AddListenerToMessagesUpdate(string channelId, List messageTi } } - public async Task PinMessageToChannel(string channelId, Message message) + public async Task PinMessageToChannel(string channelId, Message message) { - if (TryGetChannel(channelId, out var channel)) + var result = new ChatOperationResult(); + var getChannel = await GetChannel(channelId); + if (result.RegisterOperation(getChannel)) { - await channel.PinMessage(message); + return result; } + var pin = await getChannel.Result.PinMessage(message); + result.RegisterOperation(pin); + return result; } - public async Task UnpinMessageFromChannel(string channelId) + public async Task UnpinMessageFromChannel(string channelId) { - if (TryGetChannel(channelId, out var channel)) + var result = new ChatOperationResult(); + var getChannel = await GetChannel(channelId); + if (result.RegisterOperation(getChannel)) { - await channel.UnpinMessage(); + return result; } + var unpin = await getChannel.Result.UnpinMessage(); + result.RegisterOperation(unpin); + return result; } /// @@ -1348,12 +1394,19 @@ public async Task GetEventsHistory(string channelId, strin throw new NotImplementedException(); } - public async Task EmitEvent(PubnubChatEventType type, string channelId, string jsonPayload) + public async Task EmitEvent(PubnubChatEventType type, string channelId, string jsonPayload) { + var result = new ChatOperationResult(); jsonPayload = jsonPayload.Remove(0, 1); jsonPayload = jsonPayload.Remove(jsonPayload.Length - 1); var fullPayload = $"{{{jsonPayload}, \"type\": {ChatEnumConverters.ChatEventTypeToString(type)}}}"; - await PubnubInstance.Publish().Channel(channelId).Message(fullPayload).ExecuteAsync(); + var publishResult = await PubnubInstance.Publish().Channel(channelId).Message(fullPayload).ExecuteAsync(); + result.InternalStatuses.Add(publishResult.Status); + if (publishResult.Status.Error) + { + result.Error = true; + } + return result; } #endregion diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs index fd26833..b7b9f55 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 { 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/ChatOperationResult.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatOperationResult.cs new file mode 100644 index 0000000..a1fc93c --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatOperationResult.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi.Entities.Data +{ + public class ChatOperationResult + { + public bool Error { get; internal set; } + public List InternalStatuses { get; internal set; } = new(); + public Exception Exception { get; internal set; } + + /// + /// Registers a single PNResult to this overall Chat Operation Result. + /// Returns pubnubResult.Status.Error + /// + internal bool RegisterOperation(PNResult pubnubResult) + { + InternalStatuses.Add(pubnubResult.Status); + Error = pubnubResult.Status.Error; + if (Error) + { + Exception = pubnubResult.Status.ErrorData.Throwable; + } + return Error; + } + + /// + /// Registers another ChatOperationResult to this ChatOperationResult. + /// Returns otherChatResult.Error + /// + internal bool RegisterOperation(ChatOperationResult otherChatResult) + { + InternalStatuses.AddRange(otherChatResult.InternalStatuses); + Exception = otherChatResult.Exception; + Error = otherChatResult.Error; + return Error; + } + } + + public class ChatOperationResult : ChatOperationResult + { + public T Result { get; internal set; } + } +} \ 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..ddf46b1 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 { 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/Message.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs index 9e2352e..a9fadc5 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -290,13 +290,16 @@ public virtual async Task Report(string reason) throw new NotImplementedException(); } - public virtual async Task Forward(string channelId) + public virtual async Task Forward(string channelId) { - var channel = await chat.GetChannelAsync(channelId); - if (channel != null) + var result = new ChatOperationResult(); + var channel = await chat.GetChannel(channelId); + if (result.RegisterOperation(channel)) { - await chat.ForwardMessage(this, channel); + return result; } + result.RegisterOperation(await chat.ForwardMessage(this, channel.Result)); + return result; } public virtual bool HasUserReaction(string reactionValue) diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs index edaa5cf..295957a 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -37,12 +37,12 @@ internal static string MessageToThreadChannelId(Message message) return $"PUBNUB_INTERNAL_THREAD_{message.ChannelId}_{message.Id}"; } - public override async Task PinMessage(Message message) + public override async Task PinMessage(Message message) { throw new NotImplementedException(); } - public override async Task UnpinMessage() + public override async Task UnpinMessage() { throw new NotImplementedException(); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs index 3b36d23..e1983c0 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -57,7 +57,7 @@ public override async Task Report(string reason) throw new NotImplementedException(); } - public override async Task Forward(string channelId) + public override async Task Forward(string channelId) { throw new NotImplementedException(); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index e834ab1..64657ed 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -189,7 +189,7 @@ public async Task Update(ChatUserData updatedData) await UpdateUserData(chat, Id, updatedData); } - internal static async Task UpdateUserData(Chat chat, string userId, ChatUserData chatUserData) + internal static async Task> UpdateUserData(Chat chat, string userId, ChatUserData chatUserData) { //TODO: Create a better way to do this var operation = chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true).Uuid(userId); @@ -221,32 +221,12 @@ internal static async Task UpdateUserData(Chat chat, string userId, ChatUs { operation = operation.Custom(chatUserData.CustomData); } - var result = await operation.ExecuteAsync(); - if (result.Status.Error) - { - chat.PubnubInstance.PNConfig.Logger.Error($"Error when trying to update user data for user \"{userId}\": {result.Status.ErrorData.Information}"); - return false; - } - return true; + return await operation.ExecuteAsync(); } - internal static async Task GetUserData(Chat chat, string userId) + internal static async Task> GetUserData(Chat chat, string userId) { - var result = await chat.PubnubInstance.GetUuidMetadata().Uuid(userId).IncludeCustom(true).ExecuteAsync(); - if (result.Status.Error) - { - chat.PubnubInstance.PNConfig.Logger.Error($"Error when trying to Resync() User \"{userId}\": {result.Status.ErrorData.Information}"); - return null; - } - try - { - return (ChatUserData)result.Result; - } - catch (Exception e) - { - chat.PubnubInstance.PNConfig.Logger.Error($"Error when trying to parse data for User \"{userId}\": {e.Message}"); - return null; - } + return await chat.PubnubInstance.GetUuidMetadata().Uuid(userId).IncludeCustom(true).ExecuteAsync(); } internal void UpdateLocalData(ChatUserData? newData) @@ -261,7 +241,10 @@ internal void UpdateLocalData(ChatUserData? newData) public override async Task Resync() { var newData = await GetUserData(chat, Id); - UpdateLocalData(newData); + if (!newData.Status.Error) + { + UpdateLocalData(newData.Result); + } } /// @@ -305,14 +288,14 @@ public async Task DeleteUser() /// 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); } - public async Task SetRestriction(string channelId, Restriction restriction) + public async Task SetRestriction(string channelId, Restriction restriction) { - await chat.SetRestriction(Id, channelId, restriction); + return await chat.SetRestriction(Id, channelId, restriction); } /// @@ -329,18 +312,90 @@ public async Task SetRestriction(string channelId, Restriction restriction) /// /// 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) + public async Task> GetChannelRestrictions(Channel channel) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + var membersResult = await chat.PubnubInstance.GetChannelMembers().Channel($"{Chat.INTERNAL_MODERATION_PREFIX}_{channel.Id}").Include(new[] + { + PNChannelMemberField.CUSTOM + }).Filter($"uuid.id == \"{Id}\"").IncludeCount(true).ExecuteAsync(); + 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; } - public async Task GetChannelsRestrictions(string sort = "", int limit = 0, - Page page = null) + public async Task> GetChannelsRestrictions(string sort = "", int limit = 0, + PNPageObject page = null) { - throw new NotImplementedException(); + var result = new ChatOperationResult(){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(); + 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; } /// @@ -399,7 +454,7 @@ public async Task IsPresentOn(string channelId) /// } /// /// - public async Task> WherePresent() + public async Task>> WherePresent() { throw new NotImplementedException(); } From 0ccfe867731efd82ebcdfd7d3b78d004236467e9 Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Fri, 22 Aug 2025 15:49:08 +0200 Subject: [PATCH 11/28] implement events (both entity update callbacks and chat events) --- .../PubNubChatApi.Tests/MessageDraftTests.cs | 28 +-- .../PubNubChatApi.Tests/RestrictionsTests.cs | 67 +---- .../PubNubChatApi.Tests/TestUtils.cs | 22 +- .../PubNubChatApi.Tests/UserTests.cs | 32 +-- .../PubnubChatApi/Entities/Base/ChatEntity.cs | 33 ++- .../Entities/Base/UniqueChatEntity.cs | 2 +- .../PubnubChatApi/Entities/Channel.cs | 236 ++++++++++-------- .../PubnubChatApi/Entities/Chat.cs | 6 +- .../PubnubChatApi/Entities/Membership.cs | 31 +-- .../PubnubChatApi/Entities/Message.cs | 18 +- .../PubnubChatApi/Entities/ThreadChannel.cs | 5 - .../PubnubChatApi/Entities/ThreadMessage.cs | 11 - .../PubnubChatApi/Entities/User.cs | 78 +++--- .../Utilities/ChatEnumConverters.cs | 45 ++++ .../PubnubChatApi/Utilities/ChatParsers.cs | 130 +++++++++- 15 files changed, 467 insertions(+), 277 deletions(-) diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs index ff2197c..78699c7 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs @@ -11,40 +11,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(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("message_draft_tests_user")) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("message_draft_tests_user")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey - }); - channel = await chat.CreatePublicConversation("message_draft_tests_channel", new ChatChannelData() + })); + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("message_draft_tests_channel", new ChatChannelData() { ChannelName = "MessageDraftTestingChannel" - }); - if (!chat.TryGetCurrentUser(out var user)) - { - Assert.Fail(); - } - + })); 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] diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs index ce41f54..a9c3a95 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/RestrictionsTests.cs @@ -12,16 +12,11 @@ public class RestrictionsTests [SetUp] public async Task Setup() { - var createChat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("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 - }); - if (createChat.Error) - { - Assert.Fail($"Failed to create chat! Error: {createChat.Exception.Message}"); - } - chat = createChat.Result; + })); } [TearDown] @@ -35,12 +30,7 @@ public async Task CleanUp() public async Task TestSetRestrictions() { var user = await chat.GetOrCreateUser("user123"); - var createChannel = await chat.CreatePublicConversation("new_channel"); - if (createChannel.Error) - { - Assert.Fail($"Failed to create channel, error: {createChannel.Exception.Message}"); - } - var channel = createChannel.Result; + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("new_channel")); await Task.Delay(2000); @@ -50,30 +40,16 @@ public async Task TestSetRestrictions() Mute = true, Reason = "Some Reason" }; - var setRestrictions = await channel.SetRestrictions(user.Id, restriction); - if (setRestrictions.Error) - { - Assert.Fail($"Failed to set restrictions, error: {setRestrictions.Exception.Message}"); - } + TestUtils.AssertOperation(await channel.SetRestrictions(user.Id, restriction)); await Task.Delay(3000); - - var getUser = await channel.GetUserRestrictions(user); - if (getUser.Error) - { - Assert.Fail($"Failed to fetch User restrictions. Exception: {getUser.Exception}"); - } - var fetchedRestriction = getUser.Result; + + var fetchedRestriction = TestUtils.AssertOperation(await channel.GetUserRestrictions(user)); Assert.True(restriction.Ban == fetchedRestriction.Ban && restriction.Mute == fetchedRestriction.Mute && restriction.Reason == fetchedRestriction.Reason); - var getChannel = await user.GetChannelRestrictions(channel); - if (getChannel.Error) - { - Assert.Fail($"Failed to fetch Channel restrictions. Exception: {getChannel.Exception}"); - } - var restrictionFromUser = getChannel.Result; + var restrictionFromUser = TestUtils.AssertOperation(await user.GetChannelRestrictions(channel)); Assert.True(restriction.Ban == restrictionFromUser.Ban && restriction.Mute == restrictionFromUser.Mute && restriction.Reason == restrictionFromUser.Reason); @@ -83,12 +59,7 @@ public async Task TestSetRestrictions() public async Task TestGetRestrictionsSets() { var user = await chat.GetOrCreateUser("user1234"); - var createChannel = await chat.CreatePublicConversation("new_channel"); - if (createChannel.Error) - { - Assert.Fail($"Failed to create channel, error: {createChannel.Exception.Message}"); - } - var channel = createChannel.Result; + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("new_channel")); await Task.Delay(4000); @@ -98,26 +69,14 @@ public async Task TestGetRestrictionsSets() Mute = true, Reason = "Some Reason" }; - var setRestrictions = await channel.SetRestrictions(user.Id, restriction); - if (setRestrictions.Error) - { - Assert.Fail($"Failed to set restrictions, error: {setRestrictions.Exception.Message}"); - } + TestUtils.AssertOperation(await channel.SetRestrictions(user.Id, restriction)); await Task.Delay(4000); - var a = await channel.GetUsersRestrictions(); - if (a.Error) - { - Assert.Fail($"Failed to fetch Users restrictions. Exception: {a.Exception}"); - } - var b = await user.GetChannelsRestrictions(); - if (b.Error) - { - Assert.Fail($"Failed to fetch Channels restrictions. Exception: {b.Exception}"); - } + var a = TestUtils.AssertOperation(await channel.GetUsersRestrictions()); + var b = TestUtils.AssertOperation(await user.GetChannelsRestrictions()); - Assert.True(a.Result.Restrictions.Any(x => x.UserId == user.Id)); - Assert.True(b.Result.Restrictions.Any(x => x.ChannelId == channel.Id)); + Assert.True(a.Restrictions.Any(x => x.UserId == user.Id)); + Assert.True(b.Restrictions.Any(x => x.ChannelId == channel.Id)); } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs index a97710a..27b1c0d 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/TestUtils.cs @@ -1,15 +1,17 @@ using PubNubChatAPI.Entities; +using PubnubChatApi.Entities.Data; 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) { var getUser = await chat.GetUser(userId); if (getUser.Error) { - var createUser = await chat.CreateUser(userId); + userData ??= new ChatUserData(); + var createUser = await chat.CreateUser(userId, userData); if (createUser.Error) { Assert.Fail($"Failed to create User! Error: {createUser.Exception.Message}"); @@ -20,4 +22,20 @@ public static async Task GetOrCreateUser(this Chat chat, string userId) } 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) + { + 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/UserTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs index 785b583..834a52c 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; @@ -16,16 +15,13 @@ public class UserTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("user_tests_user")) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("user_tests_user")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey - }); - channel = await chat.CreatePublicConversation("user_tests_channel"); - if (!chat.TryGetCurrentUser(out user)) - { - Assert.Fail(); - } + })); + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("user_tests_channel")); + user = TestUtils.AssertOperation(await chat.GetCurrentUser()); channel.Join(); await Task.Delay(3500); } @@ -98,26 +94,30 @@ await testUser.Update(new ChatUserData() [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!"); } @@ -125,7 +125,7 @@ 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); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs index 706986f..076d56e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs @@ -1,21 +1,44 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; using System.Threading.Tasks; using PubnubApi; -using PubnubChatApi.Utilities; namespace PubNubChatAPI.Entities { 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(Subscription subscription, bool listen, string channelId, SubscribeCallback listener) + { + if (listen) + { + if (subscription != null) + { + return; + } + subscription = chat.PubnubInstance.Channel(channelId).Subscription(SubscriptionOptions.ReceivePresenceEvents); + subscription.AddListener(listener); + subscription.Subscribe(); + } + else + { + subscription?.Unsubscribe(); + } + } public virtual void SetListeningForUpdates(bool listen) { - throw new NotImplementedException(); + SetListening(updateSubscription, listen, UpdateChannelId, CreateUpdateListener()); } + protected abstract SubscribeCallback CreateUpdateListener(); + public abstract Task Resync(); } } \ 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 82be98e..3b06a01 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/UniqueChatEntity.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/UniqueChatEntity.cs @@ -6,7 +6,7 @@ public abstract class UniqueChatEntity : ChatEntity { public string Id { get; protected set; } - internal UniqueChatEntity(string uniqueId) + 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 8d6573b..a27d270 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -73,8 +73,6 @@ public class Channel : UniqueChatEntity private ChatChannelData channelData; - protected Chat chat; - protected Subscription? subscription; private Dictionary typingIndicators = new(); @@ -119,6 +117,7 @@ public class Channel : UniqueChatEntity /// public event Action OnChannelUpdate; + private Subscription presenceEventsSubscription; /// /// Event that is triggered when any presence update occurs. /// @@ -139,17 +138,33 @@ public class Channel : UniqueChatEntity /// 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, ChatChannelData data) : base(channelId) + protected override string UpdateChannelId => Id; + + internal Channel(Chat chat, string channelId, ChatChannelData data) : base(chat, channelId) { - this.chat = chat; 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) { @@ -187,106 +202,129 @@ public override async Task Resync() } } - public async void SetListeningForCustomEvents(bool listen) - { - throw new NotImplementedException(); - } - - internal void BroadcastCustomEvent(ChatEvent chatEvent) - { - OnCustomEvent?.Invoke(chatEvent); - } - - public async void SetListeningForReportEvents(bool listen) - { - throw new NotImplementedException(); - } - - internal void BroadcastReportEvent(ChatEvent chatEvent) - { - OnReportEvent?.Invoke(chatEvent); - } - - public async void SetListeningForReadReceiptsEvents(bool listen) - { - throw new NotImplementedException(); - } - - public async void SetListeningForTyping(bool listen) + public void SetListeningForCustomEvents(bool listen) { - throw new NotImplementedException(); + SetListening(customEventsSubscription, 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); + } + })); } - public async void SetListeningForPresence(bool listen) + public void SetListeningForReportEvents(bool listen) { - throw new NotImplementedException(); + SetListening(reportEventsSubscription, listen, 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 void BroadcastMessageReceived(Message message) + public void SetListeningForReadReceiptsEvents(bool listen) { - OnMessageReceived?.Invoke(message); - } - - internal void BroadcastReadReceipt(Dictionary?> readReceiptEventData) - { - OnReadReceiptEvent?.Invoke(readReceiptEventData); - } - - internal void BroadcastChannelUpdate() - { - OnChannelUpdate?.Invoke(this); + SetListening(readReceiptsSubscription, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + async delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Receipt, out var readEvent)) + { + var getMembers = await chat.GetChannelMemberships(Id); + 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 async void BroadcastPresenceUpdate() + public void SetListeningForTyping(bool listen) { - var whoIs = await WhoIsPresent(); - if (whoIs.Error) - { - chat.Logger.Error($"Error when trying to broadcast presence update after WhoIs(): {whoIs.Exception.Message}"); - } - else - { - OnPresenceUpdate?.Invoke(whoIs.Result); - } + SetListening(typingEventsSubscription, 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(); + return; + } + } + //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}"); + } + } + })); } - internal void TryParseAndBroadcastTypingEvent(List userIds) + public void SetListeningForPresence(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)) + SetListening(presenceEventsSubscription, listen, Id, chat.ListenerFactory.ProduceListener(presenceCallback: + async delegate(Pubnub pn, PNPresenceEventResult p) { - typingTimer.Stop(); - } - - //Create and start new timer - var newTimer = new Timer(chat.Config.TypingTimeout); - newTimer.Elapsed += (_, _) => - { - typingIndicators.Remove(typingUserId); - OnUsersTyping?.Invoke(typingIndicators.Keys.ToList()); - }; - typingIndicators[typingUserId] = newTimer; - newTimer.Start(); - } - - OnUsersTyping?.Invoke(userIds); + var whoIs = await WhoIsPresent(); + 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 ForwardMessage(Message message) @@ -299,14 +337,14 @@ public async Task EmitUserMention(string userId, string timeToken, string text) throw new NotImplementedException(); } - public async Task StartTyping() + public async Task StartTyping() { - await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":true}}"); + return await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":true}}"); } - public async Task StopTyping() + public async Task StopTyping() { - await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":false}}"); + return await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":false}}"); } public virtual async Task PinMessage(Message message) @@ -379,11 +417,7 @@ public MessageDraft CreateMessageDraft(UserSuggestionSource userSuggestionSource /// public void Disconnect() { - if (subscription == null) - { - return; - } - subscription.Unsubscribe(); + subscription?.Unsubscribe(); } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 77a492e..8fdb8f5 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using PubnubApi; @@ -1387,6 +1386,11 @@ public async Task> GetChannelMessageHistory(string channelId, stri #region Events + internal void BroadcastAnyEvent(ChatEvent chatEvent) + { + OnAnyEvent?.Invoke(chatEvent); + } + public async Task GetEventsHistory(string channelId, string startTimeToken, string endTimeToken, int count) diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs index 2037083..650cfe4 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs @@ -61,14 +61,13 @@ public class Membership : UniqueChatEntity /// public event Action OnMembershipUpdated; - private Chat chat; + protected override string UpdateChannelId => ChannelId; - internal Membership(Chat chat, string userId, string channelId, ChatMembershipData membershipData) : base(userId+channelId) + internal Membership(Chat chat, string userId, string channelId, ChatMembershipData membershipData) : base(chat, userId+channelId) { UserId = userId; ChannelId = channelId; UpdateLocalData(membershipData); - this.chat = chat; } internal void UpdateLocalData(ChatMembershipData newData) @@ -76,30 +75,16 @@ internal void UpdateLocalData(ChatMembershipData newData) MembershipData = newData; } - public override void SetListeningForUpdates(bool listen) + protected override SubscribeCallback CreateUpdateListener() { - if (listen) + return chat.ListenerFactory.ProduceListener(objectEventCallback: delegate(Pubnub pn, PNObjectEventResult e) { - if (updateSubscription != null) + if (ChatParsers.TryParseMembershipUpdate(chat, this, e, out var updatedData)) { - return; + UpdateLocalData(updatedData); + OnMembershipUpdated?.Invoke(this); } - updateSubscription = chat.PubnubInstance.Channel(ChannelId).Subscription(SubscriptionOptions.ReceivePresenceEvents); - updateSubscription.AddListener(chat.ListenerFactory.ProduceListener(objectEventCallback: - delegate(Pubnub pn, PNObjectEventResult e) - { - if (ChatParsers.TryParseMembershipUpdate(chat, this, e, out var updatedData)) - { - UpdateLocalData(updatedData); - OnMembershipUpdated?.Invoke(this); - } - })); - updateSubscription.Subscribe(); - } - else - { - updateSubscription?.Unsubscribe(); - } + }); } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs index a9fadc5..a2ac7eb 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; +using PubnubApi; using PubnubChatApi.Entities.Data; using PubnubChatApi.Enums; using PubnubChatApi.Utilities; @@ -23,8 +24,6 @@ namespace PubNubChatAPI.Entities /// public class Message : UniqueChatEntity { - protected Chat chat; - /// /// The text content of the message. /// @@ -204,9 +203,10 @@ public virtual List TextLinks { /// public event Action OnMessageUpdated; - internal Message(Chat chat, string timeToken,string originalMessageText, string channelId, string userId, PubnubChatMessageType type, Dictionary meta) : base(timeToken) + protected override string UpdateChannelId => ChannelId; + + internal Message(Chat chat, string timeToken,string originalMessageText, string channelId, string userId, PubnubChatMessageType type, Dictionary meta) : base(chat, timeToken) { - this.chat = chat; TimeToken = timeToken; OriginalMessageText = originalMessageText; ChannelId = channelId; @@ -215,9 +215,15 @@ internal Message(Chat chat, string timeToken,string originalMessageText, string Meta = meta; } - 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); + } + }); } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs index 295957a..d2fcfed 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -1,12 +1,7 @@ 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.Utilities; namespace PubNubChatAPI.Entities { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs index e1983c0..67bbfad 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -1,13 +1,8 @@ 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; namespace PubNubChatAPI.Entities { @@ -82,12 +77,6 @@ public override async Task Delete(bool soft) throw new NotImplementedException(); } - internal override void BroadcastMessageUpdate() - { - base.BroadcastMessageUpdate(); - OnThreadMessageUpdated?.Invoke(this); - } - public async Task PinMessageToParentChannel() { throw new NotImplementedException(); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index 64657ed..02a4590 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -95,9 +95,7 @@ public string LastActiveTimeStamp throw new NotImplementedException(); } } - - private Chat chat; - + /// /// Event that is triggered when the user is updated. /// @@ -117,50 +115,70 @@ 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, ChatUserData chatUserData) : base(userId) + + protected override string UpdateChannelId => Id; + + internal User(Chat chat, string userId, ChatUserData chatUserData) : base(chat, userId) { UpdateLocalData(chatUserData); - this.chat = chat; } - public async void SetListeningForMentionEvents(bool listen) - { - throw new NotImplementedException(); - } - - internal void BroadcastMentionEvent(ChatEvent chatEvent) - { - OnMentionEvent?.Invoke(chatEvent); - } - - public async void SetListeningForInviteEvents(bool listen) + protected override SubscribeCallback CreateUpdateListener() { - throw new NotImplementedException(); + 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 BroadcastInviteEvent(ChatEvent chatEvent) + public void SetListeningForMentionEvents(bool listen) { - OnInviteEvent?.Invoke(chatEvent); + SetListening(mentionsSubscription, 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 SetListeningForModerationEvents(bool listen) - { - throw new NotImplementedException(); - } - - internal void BroadcastModerationEvent(ChatEvent chatEvent) + public void SetListeningForInviteEvents(bool listen) { - OnModerationEvent?.Invoke(chatEvent); + SetListening(invitesSubscription, 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); + } + })); } - internal void BroadcastUserUpdate() + public void SetListeningForModerationEvents(bool listen) { - OnUserUpdated?.Invoke(this); + SetListening(moderationSubscription, 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); + } + })); } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatEnumConverters.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatEnumConverters.cs index 574bbb7..7df0fe1 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatEnumConverters.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatEnumConverters.cs @@ -1,3 +1,4 @@ +using System; using PubnubChatApi.Enums; namespace PubnubChatApi.Utilities @@ -27,5 +28,49 @@ internal static string ChatEventTypeToString(PubnubChatEventType eventType) 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/ChatParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs index e57922a..865c76c 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json.Linq; +using System.Linq; using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; +using PubnubChatApi.Entities.Events; using PubnubChatApi.Enums; +using Channel = PubNubChatAPI.Entities.Channel; namespace PubnubChatApi.Utilities { @@ -65,5 +67,131 @@ internal static bool TryParseMembershipUpdate(Chat chat, Membership membership, 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") + { + var valueJson = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(actionEvent.Action + .Value); + var actionUserId = valueJson.TryGetValue("uuid", out var fromAction) ? (string)fromAction : actionEvent.Uuid; + message.MessageActions.Add(new MessageAction() + { + TimeToken = actionEvent.ActionTimetoken.ToString(), + Type = ChatEnumConverters.StringToActionType(actionEvent.Action.Type), + Value = actionEvent.Action.Value, + UserId = actionUserId + }); + } + 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 Event of type \"{eventType}\". Exception was: {e.Message}"); + chatEvent = default; + return false; + } + } } } \ No newline at end of file From 837ec94709601daf740129c76dcecc0df255b515 Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Fri, 22 Aug 2025 17:29:00 +0200 Subject: [PATCH 12/28] update event related tests, implement minor fixes --- .../PubNubChatApi.Tests/ChannelTests.cs | 138 ++++++++++++------ .../PubNubChatApi.Tests/ChatEventTests.cs | 11 +- .../PubNubChatApi.Tests/ChatTests.cs | 38 ++--- .../PubNubChatApi.Tests/MessageDraftTests.cs | 2 +- .../PubNubChatApi.Tests/MessageTests.cs | 6 +- .../PubNubChatApi.Tests/ThreadsTests.cs | 4 +- .../PubnubChatApi/Entities/Channel.cs | 108 +++++++------- .../PubnubChatApi/Entities/Chat.cs | 58 ++------ .../Entities/Data/ChatChannelData.cs | 24 +-- .../PubnubChatApi/Entities/Message.cs | 13 +- .../PubnubChatApi/Entities/ThreadMessage.cs | 2 +- .../PubnubChatApi/Entities/User.cs | 6 +- 12 files changed, 219 insertions(+), 191 deletions(-) diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs index 324e520..1146d88 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs @@ -15,19 +15,16 @@ public class ChannelTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("ctuuuuuuuuuuuuuuuuuuuuuuuuuuuuu")) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("ctuuuuuuuuuuuuuuuuuuuuuuuuuuuuu")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey - }); - if (!chat.TryGetCurrentUser(out user)) - { - Assert.Fail(); - } - await user.Update(new ChatUserData() + })); + user = TestUtils.AssertOperation(await chat.GetCurrentUser()); + TestUtils.AssertOperation(await user.Update(new ChatUserData() { Username = "Testificate" - }); + })); talkUser = await chat.GetOrCreateUser("talk_user"); } @@ -41,7 +38,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); @@ -49,22 +46,22 @@ public async Task TestUpdateChannel() var updateReset = new ManualResetEvent(false); var updatedData = new ChatChannelData() { - ChannelDescription = "some description", - ChannelCustomData = new Dictionary(){{"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.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.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.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); } @@ -72,32 +69,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.GetCurrentUser(); + 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"); @@ -105,7 +104,7 @@ 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"); } @@ -113,17 +112,17 @@ public async Task TestLeaveChannel() [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); @@ -136,7 +135,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); @@ -145,13 +144,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); @@ -161,7 +160,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); @@ -170,14 +169,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); @@ -195,7 +194,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); @@ -206,7 +205,7 @@ public async Task TestPinMessage() await Task.Delay(4000); - await channel.PinMessage(message); + TestUtils.AssertOperation(await channel.PinMessage(message)); await Task.Delay(2000); @@ -222,18 +221,18 @@ 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); Assert.True(channel.TryGetPinnedMessage(out var pinnedMessage) && pinnedMessage.MessageText == "message to pin"); - await channel.UnpinMessage(); + TestUtils.AssertOperation(await channel.UnpinMessage()); await Task.Delay(2000); @@ -249,7 +248,7 @@ public async Task TestUnPinMessage() [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(); @@ -264,7 +263,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); @@ -283,12 +282,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!"); } @@ -296,13 +295,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 7040d8c..e544875 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs @@ -16,16 +16,13 @@ public class ChatEventTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("event_tests_user")) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("event_tests_user")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey - }); - channel = await chat.CreatePublicConversation("event_tests_channel"); - if (!chat.TryGetCurrentUser(out user)) - { - Assert.Fail(); - } + })); + 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 da86129..9ac54f4 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -16,16 +16,13 @@ public class ChatTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("chats_tests_user_10_no_calkiem_nowy_2")) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("chats_tests_user_10_no_calkiem_nowy_2")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey - }); - channel = await chat.CreatePublicConversation("chat_tests_channel_2"); - if (!chat.TryGetCurrentUser(out currentUser)) - { - Assert.Fail(); - } + })); + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("chat_tests_channel_2")); + currentUser = TestUtils.AssertOperation(await chat.GetCurrentUser()); channel.Join(); await Task.Delay(3500); } @@ -66,7 +63,8 @@ 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] @@ -99,8 +97,8 @@ 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"); + var directConversation = TestUtils.AssertOperation( + await chat.CreateDirectConversation(convoUser, "direct_conversation_test")); Assert.True(directConversation.CreatedChannel is { Id: "direct_conversation_test" }); Assert.True(directConversation.HostMembership != null && directConversation.HostMembership.UserId == currentUser.Id); Assert.True(directConversation.InviteesMemberships != null && @@ -113,8 +111,8 @@ 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"); + var groupConversation = TestUtils.AssertOperation(await + chat.CreateGroupConversation([convoUser1, convoUser2, convoUser3], "group_conversation_test")); Assert.True(groupConversation.CreatedChannel is { Id: "group_conversation_test" }); Assert.True(groupConversation.HostMembership != null && groupConversation.HostMembership.UserId == currentUser.Id); Assert.True(groupConversation.InviteesMemberships is { Count: 3 }); @@ -127,7 +125,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"); @@ -192,16 +190,12 @@ public async Task TestMarkAllMessagesAsRead() [Test] public async Task TestReadReceipts() { - var otherChat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("other_chat_user")) + var otherChat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("other_chat_user")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey - }); - if (!otherChat.TryGetChannel(channel.Id, out var otherChatChannel)) - { - Assert.Fail(); - return; - } + })); + var otherChatChannel = TestUtils.AssertOperation(await otherChat.GetChannel(channel.Id)); otherChatChannel.Join(); await Task.Delay(2500); @@ -232,12 +226,12 @@ public async Task TestCanI() { await Task.Delay(4000); - var accessChat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("can_i_test_user")) + 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/MessageDraftTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs index 78699c7..6ad8624 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageDraftTests.cs @@ -22,7 +22,7 @@ public async Task Setup() })); channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("message_draft_tests_channel", new ChatChannelData() { - ChannelName = "MessageDraftTestingChannel" + Name = "MessageDraftTestingChannel" })); channel.Join(); await Task.Delay(3000); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs index 2ed9bcc..8c68cd5 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +/*using System.Diagnostics; using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; @@ -61,7 +61,7 @@ public async Task TestSendAndReceive() { Id = user.Id, Name = user.UserName - } } },*/ + } } },#1# }); var received = manualReceiveEvent.WaitOne(6000); Assert.IsTrue(received); @@ -318,4 +318,4 @@ public async Task TestCreateThread() var received = manualReceiveEvent.WaitOne(25000); Assert.IsTrue(received); } -} \ No newline at end of file +}*/ \ 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 565f16b..46d1bc3 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +/*using System.Diagnostics; using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; @@ -196,4 +196,4 @@ public async Task TestThreadMessageUpdate() var updated = messageUpdatedReset.WaitOne(25000); Assert.True(updated); } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index a27d270..2765949 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -28,7 +28,7 @@ public class Channel : UniqueChatEntity /// /// /// The name of the channel. - public string Name => channelData.ChannelName; + public string Name => channelData.Name; /// /// The description of the channel. @@ -36,7 +36,7 @@ public class Channel : UniqueChatEntity /// /// The description that allows users to understand the purpose of the channel. /// - public string Description => channelData.ChannelDescription; + public string Description => channelData.Description; /// /// The custom data of the channel. @@ -45,7 +45,7 @@ public class Channel : UniqueChatEntity /// The custom data that can be used to store additional information about the channel. /// /// - public Dictionary CustomData => channelData.ChannelCustomData; + public Dictionary CustomData => channelData.CustomData; /// /// The information about the last update of the channel. @@ -53,7 +53,7 @@ public class Channel : UniqueChatEntity /// The time when the channel was last updated. /// /// - public string Updated => channelData.ChannelUpdated; + public string Updated => channelData.Updated; /// /// The status of the channel. @@ -61,7 +61,7 @@ public class Channel : UniqueChatEntity /// The last status response received from the server. /// /// - public string Status => channelData.ChannelStatus; + public string Status => channelData.Status; /// /// The type of the channel. @@ -69,7 +69,7 @@ public class Channel : UniqueChatEntity /// The type of the response received from the server when the channel was created. /// /// - public string Type => channelData.ChannelType; + public string Type => channelData.Type; private ChatChannelData channelData; @@ -177,13 +177,25 @@ internal void UpdateLocalData(ChatChannelData? newData) internal static async Task> UpdateChannelData(Chat chat, string channelId, ChatChannelData data) { - return await chat.PubnubInstance.SetChannelMetadata().IncludeCustom(true) - .Channel(channelId) - .Name(data.ChannelName) - .Description(data.ChannelDescription) - .Status(data.ChannelStatus) - .Custom(data.ChannelCustomData) - .ExecuteAsync(); + 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 && data.CustomData.Any()) + { + operation = operation.Custom(data.CustomData); + } + return await operation.ExecuteAsync(); } internal static async Task> GetChannelData(Chat chat, string channelId) @@ -217,7 +229,7 @@ public void SetListeningForCustomEvents(bool listen) public void SetListeningForReportEvents(bool listen) { - SetListening(reportEventsSubscription, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + SetListening(reportEventsSubscription, 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)) @@ -278,7 +290,6 @@ public void SetListeningForTyping(bool listen) timer.Stop(); typingIndicators.Remove(userId); timer.Dispose(); - return; } } //start or restart typing @@ -332,9 +343,16 @@ public async Task ForwardMessage(Message message) await chat.ForwardMessage(message, this); } - public async Task EmitUserMention(string userId, string timeToken, string text) + public virtual async Task EmitUserMention(string userId, string timeToken, string text) { - throw new NotImplementedException(); + var jsonDict = new Dictionary() + { + {"text",text}, + {"messageTimetoken",timeToken}, + {"channel",Id} + }; + return await chat.EmitEvent(PubnubChatEventType.Mention, userId, + chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)); } public async Task StartTyping() @@ -609,8 +627,10 @@ public virtual async Task SendText(string message) await SendText(message, new SendTextParams()); } - public virtual async Task SendText(string message, SendTextParams sendTextParams) + public virtual async Task SendText(string message, SendTextParams sendTextParams) { + var result = new ChatOperationResult(); + //TODO: maybe move this to a method in config? var baseInterval = Type switch { @@ -645,18 +665,29 @@ public virtual async Task SendText(string message, SendTextParams sendTextParams { meta.Add("mentionedUsers", sendTextParams.MentionedUsers); } - return await chat.PubnubInstance.Publish() + + var publishResult = await chat.PubnubInstance.Publish() .Channel(Id) .ShouldStore(sendTextParams.StoreInHistory) .UsePOST(sendTextParams.SendByPost) .Message(chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(messageDict)) .Meta(meta) .ExecuteAsync(); + if (result.RegisterOperation(publishResult)) + { + return result; + } + foreach (var mention in sendTextParams.MentionedUsers) + { + result.RegisterOperation(await EmitUserMention(mention.Value.Id, + publishResult.Result.Timetoken.ToString(), message)); + } + return result; }, response => { - if (response is PNResult result && result.Status.Error) + if (result.Error) { - chat.Logger.Error($"Error occured when trying to SendText(): {result.Status.ErrorData.Information}"); + chat.Logger.Error($"Error occured when trying to SendText(): {result.Exception.Message}"); } completionSource.SetResult(true); }, exception => @@ -666,6 +697,8 @@ public virtual async Task SendText(string message, SendTextParams sendTextParams }); await completionSource.Task; + + return result; } /// @@ -690,9 +723,9 @@ 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); } /// @@ -902,39 +935,14 @@ public async Task> GetMemberships(st 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) - { - return chat.TryGetMessage(Id, timeToken, out message); - } - /// /// 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) + public async Task> GetMessage(string timeToken) { - return await chat.GetMessageAsync(Id, timeToken); + return await chat.GetMessage(Id, timeToken); } public async Task> GetMessageHistory(string startTimeToken, string endTimeToken, diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 8fdb8f5..bef0639 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -187,7 +187,7 @@ public async Task> CreatePublicConversation(string return result; } - additionalData.ChannelType = "public"; + additionalData.Type = "public"; var updated = await Channel.UpdateChannelData(this, channelId, additionalData); if (result.RegisterOperation(updated)) { @@ -217,11 +217,12 @@ private async Task> CreateConversatio 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.ChannelType = type; + channelData.Type = type; var updated = await Channel.UpdateChannelData(this, channelId, channelData); if (result.RegisterOperation(updated)) { @@ -269,6 +270,7 @@ private async Task> CreateConversatio var channel = new Channel(this, channelId, channelData); channelWrappers.Add(channelId, channel); + result.Result.CreatedChannel = channel; if (type == "direct") { @@ -517,7 +519,7 @@ public async Task GetChannels(string filter = "", strin /// /// /// - public async Task UpdateChannel(string channelId, ChatChannelData updatedData) + public async Task UpdateChannel(string channelId, ChatChannelData updatedData) { throw new NotImplementedException(); } @@ -1213,45 +1215,20 @@ public async Task GetMessageReportsHistory(string channelI 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. - /// - /// - /// 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) - { - throw new NotImplementedException(); - } - /// /// 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. - public async Task GetMessageAsync(string channelId, string messageTimeToken) + public async Task> GetMessage(string channelId, string messageTimeToken) { - return await Task.Run(() => + throw new NotImplementedException(); + /*return await Task.Run(() => { var result = TryGetMessage(channelId, messageTimeToken, out var message); return result ? message : null; - }); + });*/ } public async Task MarkAllMessagesAsRead(string filter = "", string sort = "", @@ -1314,14 +1291,15 @@ public async Task ForwardMessage(Message message, Channel c throw new NotImplementedException(); } - public void AddListenerToMessagesUpdate(string channelId, List messageTimeTokens, + 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); + if (!getMessage.Error) { - message.OnMessageUpdated += listener; + getMessage.Result.OnMessageUpdated += listener; } } } @@ -1403,13 +1381,9 @@ public async Task EmitEvent(PubnubChatEventType type, strin var result = new ChatOperationResult(); jsonPayload = jsonPayload.Remove(0, 1); jsonPayload = jsonPayload.Remove(jsonPayload.Length - 1); - var fullPayload = $"{{{jsonPayload}, \"type\": {ChatEnumConverters.ChatEventTypeToString(type)}}}"; - var publishResult = await PubnubInstance.Publish().Channel(channelId).Message(fullPayload).ExecuteAsync(); - result.InternalStatuses.Add(publishResult.Status); - if (publishResult.Status.Error) - { - result.Error = true; - } + var fullPayload = $"{{{jsonPayload}, \"type\": \"{ChatEnumConverters.ChatEventTypeToString(type)}\"}}"; + result.RegisterOperation(await PubnubInstance.Publish().Channel(channelId).Message(fullPayload) + .ExecuteAsync()); return result; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs index 52f7db1..84a2209 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatChannelData.cs @@ -14,23 +14,23 @@ namespace PubnubChatApi.Entities.Data /// public class ChatChannelData { - public string ChannelName { get; set; } = string.Empty; - public string ChannelDescription { get; set; } = string.Empty; - public Dictionary ChannelCustomData { get; set; } = new (); - 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() { - ChannelName = metadataResult.Name, - ChannelDescription = metadataResult.Description, - ChannelCustomData = metadataResult.Custom, - ChannelStatus = metadataResult.Status, - ChannelUpdated = metadataResult.Updated, - ChannelType = metadataResult.Type + Name = metadataResult.Name, + Description = metadataResult.Description, + CustomData = metadataResult.Custom, + Status = metadataResult.Status, + Updated = metadataResult.Updated, + Type = metadataResult.Type }; } } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs index a2ac7eb..5b0ac24 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -291,9 +291,18 @@ public async Task Pin() throw new NotImplementedException(); } - public virtual async Task Report(string reason) + public virtual async Task Report(string reason) { - throw new NotImplementedException(); + 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)); } public virtual async Task Forward(string channelId) diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs index 67bbfad..b78f2f4 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -47,7 +47,7 @@ public override bool TryGetQuotedMessage(out Message quotedMessage) throw new NotImplementedException(); } - public override async Task Report(string reason) + public override async Task Report(string reason) { throw new NotImplementedException(); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index 02a4590..11f54e8 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -201,10 +201,12 @@ public void SetListeningForModerationEvents(bool listen) /// /// /// - public async Task Update(ChatUserData updatedData) + public async Task Update(ChatUserData updatedData) { UpdateLocalData(updatedData); - await UpdateUserData(chat, Id, updatedData); + var result = new ChatOperationResult(); + result.RegisterOperation(await UpdateUserData(chat, Id, updatedData)); + return result; } internal static async Task> UpdateUserData(Chat chat, string userId, ChatUserData chatUserData) From b75341d156117e5f37159051f34e25759cdade27 Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Tue, 26 Aug 2025 13:16:40 +0200 Subject: [PATCH 13/28] implement thread channel and message, adapt more tests --- .../PubNubChatApi.Tests/MembershipTests.cs | 32 +++----- .../PubNubChatApi.Tests/MessageTests.cs | 41 +++++----- .../PubNubChatApi.Tests/ThreadsTests.cs | 24 +++--- .../PubnubChatApi/Entities/Channel.cs | 10 +-- .../PubnubChatApi/Entities/Chat.cs | 45 ++++++----- .../PubnubChatApi/Entities/Message.cs | 71 ++++++++++++++---- .../PubnubChatApi/Entities/MessageDraft.cs | 8 +- .../PubnubChatApi/Entities/ThreadChannel.cs | 56 +++++++------- .../PubnubChatApi/Entities/ThreadMessage.cs | 74 ++++--------------- .../PubnubChatApi/Utilities/ChatParsers.cs | 11 +-- 10 files changed, 176 insertions(+), 196 deletions(-) diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs index e3c0a96..3323e3e 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs @@ -16,21 +16,13 @@ public class MembershipTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("membership_tests_user_54")) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("membership_tests_user_54")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey - }); - channel = await chat.CreatePublicConversation("membership_tests_channel"); - if (channel == null) - { - Assert.Fail(); - } - if (!chat.TryGetCurrentUser(out user)) - { - Assert.Fail(); - } - + })); + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("membership_tests_channel")); + user = TestUtils.AssertOperation(await chat.GetCurrentUser()); channel.Join(); await Task.Delay(3500); } @@ -94,23 +86,23 @@ public async Task TestUpdateMemberships() [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) && @@ -120,7 +112,7 @@ 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); @@ -159,7 +151,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); @@ -170,7 +162,7 @@ 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(); Assert.True(unreadCount >= 3, $"Expected >=3 unread but got: {unreadCount}"); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs index 8c68cd5..d200d80 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs @@ -1,4 +1,4 @@ -/*using System.Diagnostics; +using System.Diagnostics; using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; @@ -16,20 +16,13 @@ public class MessageTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("message_tests_user_2")) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("message_tests_user_2")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey - }); - channel = await chat.CreatePublicConversation("message_tests_channel_2"); - if (channel == null) - { - Assert.Fail(); - } - if (!chat.TryGetCurrentUser(out user)) - { - Assert.Fail(); - } + })); + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("message_tests_channel_2")); + user = TestUtils.AssertOperation(await chat.GetCurrentUser()); channel.Join(); await Task.Delay(3500); } @@ -61,7 +54,7 @@ public async Task TestSendAndReceive() { Id = user.Id, Name = user.UserName - } } },#1# + } } },*/ }); var received = manualReceiveEvent.WaitOne(6000); Assert.IsTrue(received); @@ -71,7 +64,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 => @@ -103,14 +96,15 @@ public async Task TestReceivingMessageData() } [Test] - public async Task TestTryGetMessage() + public async Task TestGetMessage() { var manualReceiveEvent = new ManualResetEvent(false); - channel.OnMessageReceived += message => + channel.OnMessageReceived += async message => { if (message.ChannelId == channel.Id) { - Assert.True(chat.TryGetMessage(channel.Id, message.TimeToken, out _)); + var getMessage = await chat.GetMessage(channel.Id, message.TimeToken); + Assert.True(!getMessage.Error, $"Error when trying to GetMessage(): {getMessage.Error}"); manualReceiveEvent.Set(); } }; @@ -215,7 +209,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); @@ -287,7 +281,7 @@ public async Task TestCreateThread() try { message.SetListeningForUpdates(true); - var thread = await message.CreateThread(); + var thread = TestUtils.AssertOperation(await message.CreateThread()); thread.Join(); await Task.Delay(3500); await thread.SendText("thread_init_text"); @@ -302,13 +296,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(); @@ -318,4 +311,4 @@ public async Task TestCreateThread() var received = manualReceiveEvent.WaitOne(25000); Assert.IsTrue(received); } -}*/ \ No newline at end of file +} \ 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 46d1bc3..f17a2e0 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs @@ -1,4 +1,3 @@ -/*using System.Diagnostics; using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; @@ -16,17 +15,14 @@ public class ThreadsTests [SetUp] public async Task Setup() { - chat = await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("threads_tests_user_2")) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("threads_tests_user_2")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey - }); + })); var randomId = Guid.NewGuid().ToString()[..10]; - channel = await chat.CreatePublicConversation(randomId); - if (!chat.TryGetCurrentUser(out user)) - { - Assert.Fail(); - } + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation(randomId)); + user = TestUtils.AssertOperation(await chat.GetCurrentUser()); channel.Join(); await Task.Delay(3500); } @@ -48,7 +44,7 @@ public async Task TestGetThreadHistory() channel.OnMessageReceived += async message => { message.SetListeningForUpdates(true); - var thread = await message.CreateThread(); + var thread = TestUtils.AssertOperation(await message.CreateThread()); thread.Join(); await Task.Delay(5000); @@ -75,7 +71,7 @@ public async Task TestThreadChannelParentChannelPinning() channel.OnMessageReceived += async message => { message.SetListeningForUpdates(true); - var thread = await message.CreateThread(); + var thread = TestUtils.AssertOperation(await message.CreateThread()); thread.Join(); await thread.SendText("thread init message"); @@ -108,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(await message.CreateThread()); thread.Join(); await Task.Delay(2500); user.SetListeningForMentionEvents(true); @@ -132,7 +128,7 @@ public async Task TestThreadMessageParentChannelPinning() channel.OnMessageReceived += async message => { message.SetListeningForUpdates(true); - var thread = await message.CreateThread(); + var thread = TestUtils.AssertOperation(await message.CreateThread()); thread.Join(); await Task.Delay(3500); @@ -170,7 +166,7 @@ public async Task TestThreadMessageUpdate() channel.OnMessageReceived += async message => { message.SetListeningForUpdates(true); - var thread = await message.CreateThread(); + var thread = TestUtils.AssertOperation(await message.CreateThread()); thread.Join(); await Task.Delay(3000); @@ -196,4 +192,4 @@ public async Task TestThreadMessageUpdate() var updated = messageUpdatedReset.WaitOne(25000); Assert.True(updated); } -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index 2765949..9183d1a 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -71,7 +71,7 @@ public class Channel : UniqueChatEntity /// public string Type => channelData.Type; - private ChatChannelData channelData; + protected ChatChannelData channelData; protected Subscription? subscription; @@ -622,9 +622,9 @@ public async Task SetRestrictions(string userId, Restrictio /// /// Thrown when an error occurs while sending the message. /// - public virtual async Task SendText(string message) + public virtual async Task SendText(string message) { - await SendText(message, new SendTextParams()); + return await SendText(message, new SendTextParams()); } public virtual async Task SendText(string message, SendTextParams sendTextParams) @@ -741,9 +741,9 @@ public async Task Update(ChatChannelData updatedData) /// /// /// 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); } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index bef0639..b35cf51 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -29,9 +29,9 @@ public class Chat internal const string MESSAGE_THREAD_ID_PREFIX = "PUBNUB_INTERNAL_THREAD"; internal const string ERROR_LOGGER_KEY_PREFIX = "PUBNUB_INTERNAL_ERROR_LOGGER"; - private Dictionary channelWrappers = new(); - private Dictionary userWrappers = new(); //TODO: wrappers rethink + internal Dictionary channelWrappers = new(); + private Dictionary userWrappers = new(); internal Dictionary membershipWrappers = new(); private Dictionary messageWrappers = new(); private bool fetchUpdates = true; @@ -538,9 +538,11 @@ public async Task UpdateChannel(string channelId, ChatChann /// chat.DeleteChannel("channel_id"); /// /// - public async Task DeleteChannel(string channelId) + public async Task DeleteChannel(string channelId) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + result.RegisterOperation(await PubnubInstance.RemoveChannelMetadata().Channel(channelId).ExecuteAsync()); + return result; } #endregion @@ -1255,21 +1257,9 @@ public async Task CreateThreadChannel(Message message) throw new NotImplementedException(); } - public async Task RemoveThreadChannel(Message message) - { - throw new NotImplementedException(); - } - - /// - /// 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. - /// - public bool TryGetThreadChannel(Message message, out ThreadChannel threadChannel) + public async Task RemoveThreadChannel(Message message) { - throw new NotImplementedException(); + return await message.RemoveThread(); } /// @@ -1277,13 +1267,22 @@ public bool TryGetThreadChannel(Message message, out ThreadChannel threadChannel /// /// Message on which the ThreadChannel is supposed to be. /// The ThreadChannel object if one was found, null otherwise. - public async Task GetThreadChannelAsync(Message message) + public async Task> GetThreadChannel(Message message) { - return await Task.Run(() => + var result = new ChatOperationResult(); + var getChannel = await GetChannel(message.GetThreadId()); + if (result.RegisterOperation(getChannel)) + { + return result; + } + if (getChannel.Result is not ThreadChannel threadChannel) { - var result = TryGetThreadChannel(message, out var threadChannel); - return result ? threadChannel : null; - }); + 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) diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs index 5b0ac24..c07ccf2 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -253,37 +253,78 @@ public virtual bool TryGetQuotedMessage(out Message quotedMessage) public bool HasThread() { - throw new NotImplementedException(); + 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. - /// - /// 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) + public async Task> CreateThread() { - return chat.TryGetThreadChannel(this, out threadChannel); + var result = new ChatOperationResult(); + 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() + public async Task> GetThread() { - return await chat.GetThreadChannelAsync(this); + return await chat.GetThreadChannel(this); } - public async Task RemoveThread() + public async Task RemoveThread() { - await chat.RemoveThreadChannel(this); + var result = new ChatOperationResult(); + 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(); + 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())) + { + return result; + } + MessageActions = MessageActions.Where(x => x.Type != PubnubMessageActionType.ThreadRootId).ToList(); + result.RegisterOperation(await getThread.Result.Delete()); + return result; } public async Task Pin() diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs index 94fba24..7336b2e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs @@ -500,16 +500,16 @@ private void ApplyRemoveTextInternal(int offset, int length) /// /// Send the MessageDraft, along with its quotedMessage if any, on the channel. /// - public async Task Send() + public async Task Send() { - await Send(new SendTextParams()); + return await Send(new SendTextParams()); } /// /// 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) { var mentions = new Dictionary(); //TODO: revisit if this is the final data format and how to solve that we don't include name anywhere @@ -556,7 +556,7 @@ public async Task Send(SendTextParams sendTextParams) } } sendTextParams.MentionedUsers = mentions; - await channel.SendText(Render(), sendTextParams); + return await channel.SendText(Render(), sendTextParams); } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs index d2fcfed..4fd1c87 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -1,45 +1,51 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using PubnubApi; using PubnubChatApi.Entities.Data; namespace PubNubChatAPI.Entities { public class ThreadChannel : Channel { - public string ParentChannelId + 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) { - get - { - throw new NotImplementedException(); - } + ParentChannelId = parentChannelId; + ParentMessageTimeToken = parentMessageTimeToken; } - - public Message ParentMessage + + private async Task InitThreadChannel() { - get + var result = new ChatOperationResult(); + var channelUpdate = await UpdateChannelData(chat, Id, channelData); + if (result.RegisterOperation(channelUpdate)) { - throw new NotImplementedException(); + return result; } - } - - internal ThreadChannel(Chat chat, string channelId, ChatChannelData data) : base(chat, channelId, data) - { - } - - internal static string MessageToThreadChannelId(Message message) - { - return $"PUBNUB_INTERNAL_THREAD_{message.ChannelId}_{message.Id}"; - } - - public override async Task PinMessage(Message message) - { - throw new NotImplementedException(); + chat.channelWrappers.Add(Id, this); + result.RegisterOperation(await chat.PubnubInstance.AddMessageAction() + .Action(new PNMessageAction() { Type = "threadRootId", Value = Id }).Channel(ParentChannelId) + .MessageTimetoken(long.Parse(ParentMessageTimeToken)).ExecuteAsync()); + return result; } - public override async Task UnpinMessage() + public override async Task SendText(string message, SendTextParams sendTextParams) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + if (!initialised) + { + if (result.RegisterOperation(await InitThreadChannel())) + { + return result; + } + initialised = true; + } + return await base.SendText(message, sendTextParams); } public async Task> GetThreadHistory(string startTimeToken, string endTimeToken, int count) diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs index b78f2f4..89b6e24 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using PubnubChatApi.Entities.Data; +using PubnubApi; using PubnubChatApi.Enums; +using PubnubChatApi.Utilities; namespace PubNubChatAPI.Entities { @@ -10,71 +11,22 @@ public class ThreadMessage : Message { public event Action OnThreadMessageUpdated; - public string ParentChannelId - { - get - { - throw new NotImplementedException(); - } - } + public string ParentChannelId { get; } - internal ThreadMessage(Chat chat, string timeToken, string originalMessageText, string channelId, string userId, PubnubChatMessageType type, Dictionary meta) : base(chat, timeToken, originalMessageText, channelId, userId, type, meta) + internal ThreadMessage(Chat chat, string timeToken, string originalMessageText, string channelId, string parentChannelId, string userId, PubnubChatMessageType type, Dictionary meta) : base(chat, timeToken, originalMessageText, channelId, userId, type, meta) { + ParentChannelId = parentChannelId; } - /// - /// 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"); - /// - /// - /// - public override async Task EditMessageText(string newText) + protected override SubscribeCallback CreateUpdateListener() { - throw new NotImplementedException(); - } - - public override bool TryGetQuotedMessage(out Message quotedMessage) - { - throw new NotImplementedException(); - } - - public override async Task Report(string reason) - { - throw new NotImplementedException(); - } - - public override async Task Forward(string channelId) - { - throw new NotImplementedException(); - } - - public override bool HasUserReaction(string reactionValue) - { - throw new NotImplementedException(); - } - - public override async Task ToggleReaction(string reactionValue) - { - throw new NotImplementedException(); - } - - public override async Task Restore() - { - throw new NotImplementedException(); - } - - public override async Task Delete(bool soft) - { - throw new NotImplementedException(); + return chat.ListenerFactory.ProduceListener(messageActionCallback: delegate(Pubnub pn, PNMessageActionEventResult e) + { + if (ChatParsers.TryParseMessageUpdate(chat, this, e)) + { + OnThreadMessageUpdated?.Invoke(this); + } + }); } public async Task PinMessageToParentChannel() diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs index 865c76c..4dda823 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs @@ -126,16 +126,17 @@ internal static bool TryParseMessageUpdate(Chat chat, Message message, PNMessage { if (actionEvent.Event != "removed") { - var valueJson = - chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(actionEvent.Action - .Value); - var actionUserId = valueJson.TryGetValue("uuid", out var fromAction) ? (string)fromAction : actionEvent.Uuid; + //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 = actionUserId + UserId = actionEvent.Uuid }); } else From 9c0d0e943f899fee33fd21e94dccd55aba8535d7 Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Wed, 27 Aug 2025 13:58:19 +0200 Subject: [PATCH 14/28] implement WIP threads + history functionality --- .../PubNubChatApi.Tests/ChannelTests.cs | 23 ++++ .../PubNubChatApi.Tests/ChatTests.cs | 9 +- .../PubNubChatApi.Tests/MembershipTests.cs | 2 +- .../PubNubChatApi.Tests/MessageTests.cs | 2 +- .../PubNubChatApi.Tests/ThreadsTests.cs | 18 +-- .../PubnubChatApi/Entities/Base/ChatEntity.cs | 8 +- .../PubnubChatApi/Entities/Channel.cs | 66 +++++----- .../PubnubChatApi/Entities/Chat.cs | 122 ++++++++++++------ .../Entities/ChatAccessManager.cs | 1 + .../Entities/Data/ChatUserData.cs | 3 +- .../Data/MarkMessagesAsReadWrapper.cs | 3 +- .../PubnubChatApi/Entities/Data/Page.cs | 9 -- .../PubnubChatApi/Entities/Membership.cs | 3 +- .../PubnubChatApi/Entities/Message.cs | 59 +++++---- .../PubnubChatApi/Entities/ThreadChannel.cs | 31 ++++- .../PubnubChatApi/Entities/ThreadMessage.cs | 3 +- .../PubnubChatApi/Entities/User.cs | 27 +--- .../PubnubChatApi/Utilities/ChatParsers.cs | 101 ++++++++++++++- .../Utilities/ExponentialRateLimiter.cs | 21 ++- 19 files changed, 336 insertions(+), 175 deletions(-) delete mode 100644 c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/Page.cs diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs index 1146d88..6cbfc6d 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs @@ -109,6 +109,29 @@ public async Task TestLeaveChannel() Assert.False(memberships.Memberships.Any(x => x.UserId == currentChatUser.Id), "Leave failed, current user found in channel memberships"); } + [Test] + public async Task TestGetMessagesHistory() + { + 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); + + var history = + TestUtils.AssertOperation(await channel.GetMessageHistory("99999999999999999", "00000000000000000", 1)); + + 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() { diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs index 9ac54f4..ee9d700 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -70,12 +70,15 @@ public async Task TestGetCurrentUser() [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] diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs index 3323e3e..12699f2 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs @@ -170,7 +170,7 @@ public async Task TestUnreadMessagesCount() [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"); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs index d200d80..9ff8bea 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs @@ -281,7 +281,7 @@ public async Task TestCreateThread() try { message.SetListeningForUpdates(true); - var thread = TestUtils.AssertOperation(await message.CreateThread()); + var thread = TestUtils.AssertOperation(message.CreateThread()); thread.Join(); await Task.Delay(3500); await thread.SendText("thread_init_text"); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs index f17a2e0..7391926 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs @@ -44,7 +44,7 @@ public async Task TestGetThreadHistory() channel.OnMessageReceived += async message => { message.SetListeningForUpdates(true); - var thread = TestUtils.AssertOperation(await message.CreateThread()); + var thread = TestUtils.AssertOperation(message.CreateThread()); thread.Join(); await Task.Delay(5000); @@ -55,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(); }; @@ -71,14 +71,14 @@ public async Task TestThreadChannelParentChannelPinning() channel.OnMessageReceived += async message => { message.SetListeningForUpdates(true); - var thread = TestUtils.AssertOperation(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); @@ -104,7 +104,7 @@ public async Task TestThreadChannelEmitUserMention() var mentionedReset = new ManualResetEvent(false); channel.OnMessageReceived += async message => { - var thread = TestUtils.AssertOperation(await message.CreateThread()); + var thread = TestUtils.AssertOperation(message.CreateThread()); thread.Join(); await Task.Delay(2500); user.SetListeningForMentionEvents(true); @@ -128,7 +128,7 @@ public async Task TestThreadMessageParentChannelPinning() channel.OnMessageReceived += async message => { message.SetListeningForUpdates(true); - var thread = TestUtils.AssertOperation(await message.CreateThread()); + var thread = TestUtils.AssertOperation(message.CreateThread()); thread.Join(); await Task.Delay(3500); @@ -139,7 +139,7 @@ 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(); @@ -166,7 +166,7 @@ public async Task TestThreadMessageUpdate() channel.OnMessageReceived += async message => { message.SetListeningForUpdates(true); - var thread = TestUtils.AssertOperation(await message.CreateThread()); + var thread = TestUtils.AssertOperation(message.CreateThread()); thread.Join(); await Task.Delay(3000); @@ -177,7 +177,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/Entities/Base/ChatEntity.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs index 076d56e..b8a214b 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs @@ -14,7 +14,7 @@ internal ChatEntity(Chat chat) this.chat = chat; } - protected void SetListening(Subscription subscription, bool listen, string channelId, SubscribeCallback listener) + protected void SetListening(Subscription subscription, SubscriptionOptions subscriptionOptions, bool listen, string channelId, SubscribeCallback listener) { if (listen) { @@ -22,7 +22,7 @@ protected void SetListening(Subscription subscription, bool listen, string chann { return; } - subscription = chat.PubnubInstance.Channel(channelId).Subscription(SubscriptionOptions.ReceivePresenceEvents); + subscription = chat.PubnubInstance.Channel(channelId).Subscription(subscriptionOptions); subscription.AddListener(listener); subscription.Subscribe(); } @@ -34,11 +34,11 @@ protected void SetListening(Subscription subscription, bool listen, string chann public virtual void SetListeningForUpdates(bool listen) { - SetListening(updateSubscription, listen, UpdateChannelId, CreateUpdateListener()); + SetListening(updateSubscription, SubscriptionOptions.None, listen, UpdateChannelId, CreateUpdateListener()); } protected abstract SubscribeCallback CreateUpdateListener(); - public abstract Task Resync(); + public abstract Task Refresh(); } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index 9183d1a..6ecafa4 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -205,7 +205,7 @@ internal static async Task> GetChannelData( .ExecuteAsync(); } - public override async Task Resync() + public override async Task Refresh() { var getResult = await GetChannelData(chat, Id); if (!getResult.Status.Error) @@ -216,7 +216,7 @@ public override async Task Resync() public void SetListeningForCustomEvents(bool listen) { - SetListening(customEventsSubscription, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + SetListening(customEventsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: delegate(Pubnub pn, PNMessageResult m) { if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Custom, out var customEvent)) @@ -229,7 +229,7 @@ public void SetListeningForCustomEvents(bool listen) public void SetListeningForReportEvents(bool listen) { - SetListening(reportEventsSubscription, listen, $"{Chat.INTERNAL_MODERATION_PREFIX}_{Id}", chat.ListenerFactory.ProduceListener(messageCallback: + SetListening(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)) @@ -242,7 +242,7 @@ public void SetListeningForReportEvents(bool listen) public void SetListeningForReadReceiptsEvents(bool listen) { - SetListening(readReceiptsSubscription, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + SetListening(readReceiptsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: async delegate(Pubnub pn, PNMessageResult m) { if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Receipt, out var readEvent)) @@ -267,7 +267,7 @@ async delegate(Pubnub pn, PNMessageResult m) public void SetListeningForTyping(bool listen) { - SetListening(typingEventsSubscription, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + SetListening(typingEventsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: delegate(Pubnub pn, PNMessageResult m) { if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Typing, out var rawTypingEvent)) @@ -323,7 +323,7 @@ public void SetListeningForTyping(bool listen) public void SetListeningForPresence(bool listen) { - SetListening(presenceEventsSubscription, listen, Id, chat.ListenerFactory.ProduceListener(presenceCallback: + SetListening(presenceEventsSubscription, SubscriptionOptions.ReceivePresenceEvents, listen, Id, chat.ListenerFactory.ProduceListener(presenceCallback: async delegate(Pubnub pn, PNPresenceEventResult p) { var whoIs = await WhoIsPresent(); @@ -343,7 +343,7 @@ public async Task ForwardMessage(Message message) await chat.ForwardMessage(message, this); } - public virtual async Task EmitUserMention(string userId, string timeToken, string text) + public async Task EmitUserMention(string userId, string timeToken, string text) { var jsonDict = new Dictionary() { @@ -365,17 +365,16 @@ public async Task StopTyping() return await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":false}}"); } - public virtual async Task PinMessage(Message message) + public async Task PinMessage(Message message) { throw new NotImplementedException(); } - public virtual async Task UnpinMessage() + public async Task UnpinMessage() { throw new NotImplementedException(); } - - //TODO: currently same result whether error or no pinned message present + /// /// Tries to get the Message pinned to this Channel. /// @@ -391,13 +390,9 @@ public bool TryGetPinnedMessage(out Message pinnedMessage) /// Asynchronously tries to get the Message pinned to this Channel. /// /// The pinned Message object if there was one, null otherwise. - public async Task GetPinnedMessageAsync() + public async Task> GetPinnedMessage() { - return await Task.Run(() => - { - var result = TryGetPinnedMessage(out var pinnedMessage); - return result ? pinnedMessage : null; - }); + throw new NotImplementedException(); } /// @@ -430,12 +425,11 @@ public MessageDraft CreateMessageDraft(UserSuggestionSource userSuggestionSource /// channel.Disconnect(); /// /// - /// Thrown when an error occurs while disconnecting from the channel. /// /// public void Disconnect() { - subscription?.Unsubscribe(); + SetListening(subscription, SubscriptionOptions.None, false, Id, null); } /// @@ -454,7 +448,6 @@ public void Disconnect() /// channel.Leave(); /// /// - /// Thrown when an error occurs while leaving the channel. /// /// /// @@ -490,27 +483,37 @@ public async void Leave() /// channel.Connect(); /// /// - /// Thrown when an error occurs while connecting to the channel. /// /// /// public void Connect() { - if (subscription != null) + SetListening(subscription, SubscriptionOptions.None, true, Id, chat.ListenerFactory.ProduceListener(messageCallback: + delegate(Pubnub pn, PNMessageResult m) + { + if (ChatParsers.TryParseMessageResult(chat, m, out var message)) + { + //TODO: wrappers rethink + //chat.RegisterMessage(message); + OnMessageReceived?.Invoke(message); + } + })); + /*if (subscription != null) { return; } - subscription = chat.PubnubInstance.Channel(Id).Subscription(SubscriptionOptions.ReceivePresenceEvents); + subscription = chat.PubnubInstance.Channel(Id).Subscription(SubscriptionOptions.None); subscription.AddListener(chat.ListenerFactory.ProduceListener(messageCallback: delegate(Pubnub pn, PNMessageResult m) { if (ChatParsers.TryParseMessageResult(chat, m, out var message)) { - chat.RegisterMessage(message); + //TODO: wrappers rethink + //chat.RegisterMessage(message); OnMessageReceived?.Invoke(message); } })); - subscription.Subscribe(); + subscription.Subscribe();*/ } /// @@ -531,7 +534,6 @@ public void Connect() /// channel.Join(); /// /// - /// Thrown when an error occurs while joining the channel. /// /// /// @@ -620,9 +622,8 @@ public async Task SetRestrictions(string userId, Restrictio /// 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) { return await SendText(message, new SendTextParams()); } @@ -631,7 +632,6 @@ public virtual async Task SendText(string message, SendText { var result = new ChatOperationResult(); - //TODO: maybe move this to a method in config? var baseInterval = Type switch { "public" => chat.Config.RateLimitsPerChannel.PublicConversation, @@ -720,7 +720,6 @@ public virtual async Task SendText(string message, SendText /// }); /// /// - /// Thrown when an error occurs while updating the channel. /// /// public async Task Update(ChatChannelData updatedData) @@ -740,7 +739,6 @@ public async Task Update(ChatChannelData updatedData) /// channel.DeleteChannel(); /// /// - /// Thrown when an error occurs while deleting the channel. public async Task Delete() { return await chat.DeleteChannel(Id); @@ -768,7 +766,6 @@ public async Task Delete() /// ); /// /// - /// Thrown when an error occurs while getting the user restrictions. /// public async Task> GetUserRestrictions(User user) { @@ -865,7 +862,6 @@ public async Task> GetUsersRestric /// Console.WriteLine($"User present: {isUserPresent}"); /// /// - /// Thrown when an error occurs while checking the presence of the user. /// public async Task> IsUserPresent(string userId) { @@ -888,7 +884,6 @@ public async Task> IsUserPresent(string userId) /// } /// /// - /// Thrown when an error occurs while getting the list of users present in the channel. /// public async Task>> WhoIsPresent() { @@ -927,7 +922,6 @@ public async Task>> WhoIsPresent() /// } /// /// - /// Thrown when an error occurs while getting the list of memberships. /// public async Task> GetMemberships(string filter = "", string sort = "", int limit = 0, PNPageObject page = null) @@ -945,7 +939,7 @@ public async Task> GetMessage(string timeToken) return await chat.GetMessage(Id, timeToken); } - public async Task> GetMessageHistory(string startTimeToken, string endTimeToken, + public async Task>> GetMessageHistory(string startTimeToken, string endTimeToken, int count) { return await chat.GetChannelMessageHistory(Id, startTimeToken, endTimeToken, count); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index b35cf51..5f27ac2 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -11,7 +11,6 @@ namespace PubNubChatAPI.Entities { //TODO: make IDisposable? - //TODO: global remove CCoreException from inline docs /// /// Main class for the chat. /// @@ -27,7 +26,6 @@ public class Chat internal const string INTERNAL_MODERATION_PREFIX = "PUBNUB_INTERNAL_MODERATION"; internal const string INTERNAL_ADMIN_CHANNEL = "PUBNUB_INTERNAL_ADMIN_CHANNEL"; internal const string MESSAGE_THREAD_ID_PREFIX = "PUBNUB_INTERNAL_THREAD"; - internal const string ERROR_LOGGER_KEY_PREFIX = "PUBNUB_INTERNAL_ERROR_LOGGER"; //TODO: wrappers rethink internal Dictionary channelWrappers = new(); @@ -418,7 +416,7 @@ public async Task>> InviteMultipleToChannel await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload); } - await channel.Result.Resync(); + await channel.Result.Refresh(); return result; } @@ -433,7 +431,7 @@ public async Task> GetChannel(string channelId) var result = new ChatOperationResult(); if (channelWrappers.TryGetValue(channelId, out var existingChannel)) { - await existingChannel.Resync(); + await existingChannel.Refresh(); result.Result = existingChannel; } else @@ -508,7 +506,6 @@ 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. /// /// /// var chat = // ... @@ -531,7 +528,6 @@ public async Task UpdateChannel(string channelId, ChatChann /// /// /// The channel ID. - /// Throws an exception if the channel with the provided ID does not exist or any connection problem persists. /// /// /// var chat = // ... @@ -682,7 +678,6 @@ public async void AddListenerToUsersUpdate(List userIds, Action li /// /// The data for user is empty. /// - /// Throws an exception if any connection problem persists. /// /// /// var chat = // ... @@ -704,7 +699,6 @@ 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. /// /// /// var chat = // ... @@ -742,7 +736,6 @@ public async Task> CreateUser(string userId, ChatUserD /// The user ID. /// The channel ID. /// True if the user is present, false otherwise. - /// Throws an exception if any connection problem persists. /// /// /// var chat = // ... @@ -778,7 +771,6 @@ public async Task> IsPresent(string userId, string cha /// /// The channel ID. /// The list of the users present in the channel. - /// Throws an exception if any connection problem persists. /// /// /// var chat = // ... @@ -815,7 +807,6 @@ public async Task>> WhoIsPresent(string channel /// /// The user ID. /// The list of the channels where the user is present. - /// Throws an exception if any connection problem persists. /// /// /// var chat = // ... @@ -854,7 +845,7 @@ public async Task> GetUser(string userId) var result = new ChatOperationResult(); if (userWrappers.TryGetValue(userId, out var existingUser)) { - await existingUser.Resync(); + await existingUser.Refresh(); result.Result = existingUser; return result; } @@ -880,7 +871,6 @@ public async Task> GetUser(string userId) /// 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 = // ... @@ -955,7 +945,6 @@ 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. /// /// /// var chat = // ... @@ -985,7 +974,6 @@ 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. /// /// /// var chat = // ... @@ -1013,7 +1001,6 @@ public async Task DeleteUser(string userId) /// 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 = // ... @@ -1118,7 +1105,6 @@ public void AddListenerToMembershipsUpdate(List membershipIds, ActionThe 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 = // ... @@ -1203,14 +1189,8 @@ public async Task> GetChannelMembers #endregion #region Messages - - //TODO: wrappers rethink - internal void RegisterMessage(Message message) - { - messageWrappers.TryAdd(message.Id, message); - } - public async Task GetMessageReportsHistory(string channelId, string startTimeToken, + public async Task> GetMessageReportsHistory(string channelId, string startTimeToken, string endTimeToken, int count) { return await GetEventsHistory($"PUBNUB_INTERNAL_MODERATION_{channelId}", startTimeToken, endTimeToken, @@ -1235,31 +1215,45 @@ public async Task> GetMessage(string channelId, str public async Task MarkAllMessagesAsRead(string filter = "", string sort = "", int limit = 0, - Page page = null) + PNPageObject page = null) { throw new NotImplementedException(); } - internal bool TryGetAnyMessage(string timeToken, out Message message) - { - return messageWrappers.TryGetValue(timeToken, out message); - } - public async Task> GetUnreadMessagesCounts(string filter = "", string sort = "", int limit = 0, - Page page = null) + PNPageObject page = null) { throw new NotImplementedException(); } - public async Task CreateThreadChannel(Message message) + public async Task> CreateThreadChannel(string messageTimeToken, string messageChannelId) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + var getMessage = await GetMessage(messageChannelId, messageTimeToken); + if (result.RegisterOperation(getMessage)) + { + return result; + } + var createThread = getMessage.Result.CreateThread(); + if (result.RegisterOperation(createThread)) + { + return result; + } + result.Result = createThread.Result; + return result; } - public async Task RemoveThreadChannel(Message message) + public async Task RemoveThreadChannel(string messageTimeToken, string messageChannelId) { - return await message.RemoveThread(); + var result = new ChatOperationResult(); + var getMessage = await GetMessage(messageChannelId, messageTimeToken); + if (result.RegisterOperation(getMessage)) + { + return result; + } + result.RegisterOperation(await getMessage.Result.RemoveThread()); + return result; } /// @@ -1341,7 +1335,6 @@ public async Task UnpinMessageFromChannel(string channelId) /// 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 = // ... @@ -1352,11 +1345,31 @@ public async Task UnpinMessageFromChannel(string channelId) /// /// /// - public async Task> GetChannelMessageHistory(string channelId, string startTimeToken, + public async Task>> GetChannelMessageHistory(string channelId, string startTimeToken, string endTimeToken, int count) { - throw new NotImplementedException(); + var result = new ChatOperationResult>() + { + 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(); + if (result.RegisterOperation(getHistory) || !getHistory.Result.Messages.ContainsKey(channelId)) + { + return result; + } + + 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 @@ -1368,11 +1381,40 @@ internal void BroadcastAnyEvent(ChatEvent chatEvent) OnAnyEvent?.Invoke(chatEvent); } - public async Task GetEventsHistory(string channelId, string startTimeToken, + public async Task> GetEventsHistory(string channelId, string startTimeToken, string endTimeToken, int count) { - throw new NotImplementedException(); + var result = new ChatOperationResult() + { + 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(); + 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; } public async Task EmitEvent(PubnubChatEventType type, string channelId, string jsonPayload) diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs index e44466c..aae87da 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs @@ -4,6 +4,7 @@ namespace PubNubChatAPI.Entities { + //TODO: now that core SDK is exposed do we still need SetAuthToken, ParseToken, and SetPubnubOrigin? public class ChatAccessManager { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs index 3a0ed40..098901f 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatUserData.cs @@ -32,8 +32,7 @@ public static implicit operator ChatUserData(PNUuidMetadataResult metadataResult Username = metadataResult.Name, Status = metadataResult.Status, Type = metadataResult.Type, - //TODO: I think this is correct? - CustomData = metadataResult.Custom//.TryGetValue("custom", out var custom) ? (Dictionary)custom : new () + CustomData = metadataResult.Custom }; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs index 5c322dd..fe1b959 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using PubnubApi; using PubNubChatAPI.Entities; using PubnubChatApi.Utilities; @@ -7,7 +8,7 @@ namespace PubnubChatApi.Entities.Data { public struct MarkMessagesAsReadWrapper { - public Page Page; + public PNPageObject Page; public int Total; public int Status; public List Memberships; 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 30c55d1..0000000 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/Page.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace PubnubChatApi.Entities.Data -{ - //TODO: REMOVE and replace with PNPage - 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/Membership.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs index 650cfe4..4540ab0 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs @@ -168,8 +168,9 @@ public async Task GetUnreadMessagesCount() return countsResponse.Result.Channels[ChannelId]; } - public override async Task Resync() + public override async Task Refresh() { + //TODO: wrappers rethink await chat.GetChannelMemberships(ChannelId, filter:$"uuid.id == \"{UserId}\""); } } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs index c07ccf2..df97321 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -30,7 +30,7 @@ public class Message : UniqueChatEntity /// 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 edits = MessageActions.Where(x => x.Type == PubnubMessageActionType.Edited).ToList(); @@ -41,7 +41,7 @@ public virtual string MessageText { /// /// The original, un-edited text of the message. /// - public virtual string OriginalMessageText { get; internal set; } + public string OriginalMessageText { get; internal set; } /// /// The time token of the message. @@ -50,7 +50,7 @@ public virtual string MessageText { /// It is used to identify the message in the chat. /// /// - public virtual string TimeToken { get; internal set; } + public string TimeToken { get; internal set; } /// /// The channel ID of the channel that the message belongs to. @@ -58,7 +58,7 @@ public virtual string MessageText { /// This is the ID of the channel that the message was sent to. /// /// - public virtual string ChannelId { get; internal set; } + public string ChannelId { get; internal set; } /// /// The user ID of the user that sent the message. @@ -67,7 +67,7 @@ public virtual string MessageText { /// Do not confuse this with the username of the user. /// /// - public virtual string UserId { get; internal set; } + public string UserId { get; internal set; } /// /// The metadata of the message. @@ -86,9 +86,9 @@ public virtual string MessageText { /// It means that all the deletions are soft deletions. /// /// - public virtual bool IsDeleted => MessageActions.Any(x => x.Type == PubnubMessageActionType.Deleted); + public bool IsDeleted => MessageActions.Any(x => x.Type == PubnubMessageActionType.Deleted); - public virtual List MentionedUsers { + public List MentionedUsers { get { var mentioned = new List(); @@ -114,7 +114,7 @@ public virtual List MentionedUsers { } } - public virtual List ReferencedChannels { + public List ReferencedChannels { get { var referenced = new List(); @@ -140,7 +140,7 @@ public virtual List ReferencedChannels { } } - public virtual List TextLinks { + public List TextLinks { get { var links = new List(); @@ -167,9 +167,9 @@ public virtual List TextLinks { } } - public virtual List MessageActions { get; internal set; } = new(); + public List MessageActions { get; internal set; } = new(); - public virtual List Reactions => + public List Reactions => MessageActions.Where(x => x.Type == PubnubMessageActionType.Reaction).ToList(); /// @@ -180,7 +180,7 @@ public virtual List TextLinks { /// /// /// - public virtual PubnubChatMessageType Type { get; internal set; } + public PubnubChatMessageType Type { get; internal set; } /// @@ -205,7 +205,7 @@ public virtual List TextLinks { protected override string UpdateChannelId => ChannelId; - internal Message(Chat chat, string timeToken,string originalMessageText, string channelId, string userId, PubnubChatMessageType type, Dictionary meta) : base(chat, timeToken) + 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; @@ -213,6 +213,7 @@ internal Message(Chat chat, string timeToken,string originalMessageText, string UserId = userId; Type = type; Meta = meta; + MessageActions = messageActions; } protected override SubscribeCallback CreateUpdateListener() @@ -241,12 +242,22 @@ protected override SubscribeCallback CreateUpdateListener() /// /// /// - public virtual async Task EditMessageText(string newText) + public async Task EditMessageText(string newText) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + 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 }) + .MessageTimetoken(long.Parse(TimeToken)).Channel(ChannelId).ExecuteAsync()); + return result; } - public virtual bool TryGetQuotedMessage(out Message quotedMessage) + public bool TryGetQuotedMessage(out Message quotedMessage) { throw new NotImplementedException(); } @@ -261,7 +272,7 @@ internal string GetThreadId() return $"{Chat.MESSAGE_THREAD_ID_PREFIX}_{ChannelId}_{TimeToken}"; } - public async Task> CreateThread() + public ChatOperationResult CreateThread() { var result = new ChatOperationResult(); if (ChannelId.Contains(Chat.MESSAGE_THREAD_ID_PREFIX)) @@ -332,7 +343,7 @@ public async Task Pin() throw new NotImplementedException(); } - public virtual async Task Report(string reason) + public async Task Report(string reason) { var jsonDict = new Dictionary() { @@ -346,7 +357,7 @@ public virtual async Task Report(string reason) chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)); } - public virtual async Task Forward(string channelId) + public async Task Forward(string channelId) { var result = new ChatOperationResult(); var channel = await chat.GetChannel(channelId); @@ -358,17 +369,17 @@ public virtual async Task Forward(string channelId) return result; } - public virtual bool HasUserReaction(string reactionValue) + public bool HasUserReaction(string reactionValue) { throw new NotImplementedException(); } - public virtual async Task ToggleReaction(string reactionValue) + public async Task ToggleReaction(string reactionValue) { throw new NotImplementedException(); } - public virtual async Task Restore() + public async Task Restore() { throw new NotImplementedException(); } @@ -390,12 +401,12 @@ public virtual async Task Restore() /// /// /// - public virtual async Task Delete(bool soft) + public async Task Delete(bool soft) { throw new NotImplementedException(); } - public override Task Resync() + public override Task Refresh() { throw new NotImplementedException(); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs index 4fd1c87..71ed29c 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using PubnubApi; using PubnubChatApi.Entities.Data; +using PubnubChatApi.Enums; namespace PubNubChatAPI.Entities { @@ -12,8 +13,9 @@ public class ThreadChannel : Channel public string ParentMessageTimeToken { get; } private bool initialised; - - internal ThreadChannel(Chat chat, string channelId, string parentChannelId, string parentMessageTimeToken, ChatChannelData data) : base(chat, channelId, data) + + internal ThreadChannel(Chat chat, string channelId, string parentChannelId, string parentMessageTimeToken, + ChatChannelData data) : base(chat, channelId, data) { ParentChannelId = parentChannelId; ParentMessageTimeToken = parentMessageTimeToken; @@ -27,6 +29,7 @@ private async Task InitThreadChannel() { return result; } + chat.channelWrappers.Add(Id, this); result.RegisterOperation(await chat.PubnubInstance.AddMessageAction() .Action(new PNMessageAction() { Type = "threadRootId", Value = Id }).Channel(ParentChannelId) @@ -43,14 +46,34 @@ public override async Task SendText(string message, SendTex { return result; } + initialised = true; } + return await base.SendText(message, sendTextParams); } - public async Task> GetThreadHistory(string startTimeToken, string endTimeToken, int count) + public async Task>> GetThreadHistory(string startTimeToken, + string endTimeToken, int count) { - throw new NotImplementedException(); + var result = new ChatOperationResult>() + { + Result = new List() + }; + var getHistory = await GetMessageHistory(startTimeToken, endTimeToken, count); + 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 async Task PinMessageToParentChannel(ThreadMessage message) diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs index 89b6e24..9c4e046 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using PubnubApi; +using PubnubChatApi.Entities.Data; using PubnubChatApi.Enums; using PubnubChatApi.Utilities; @@ -13,7 +14,7 @@ public class ThreadMessage : Message public string ParentChannelId { get; } - internal ThreadMessage(Chat chat, string timeToken, string originalMessageText, string channelId, string parentChannelId, string userId, PubnubChatMessageType type, Dictionary meta) : base(chat, timeToken, originalMessageText, channelId, userId, type, meta) + 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; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index 11f54e8..b1c4543 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -144,7 +144,7 @@ protected override SubscribeCallback CreateUpdateListener() public void SetListeningForMentionEvents(bool listen) { - SetListening(mentionsSubscription, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + SetListening(mentionsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: delegate(Pubnub pn, PNMessageResult m) { if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Mention, out var mentionEvent)) @@ -157,7 +157,7 @@ public void SetListeningForMentionEvents(bool listen) public void SetListeningForInviteEvents(bool listen) { - SetListening(invitesSubscription, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + SetListening(invitesSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: delegate(Pubnub pn, PNMessageResult m) { if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Invite, out var inviteEvent)) @@ -170,7 +170,7 @@ public void SetListeningForInviteEvents(bool listen) public void SetListeningForModerationEvents(bool listen) { - SetListening(moderationSubscription, listen, Chat.INTERNAL_MODERATION_PREFIX+Id, chat.ListenerFactory.ProduceListener(messageCallback: + SetListening(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)) @@ -188,9 +188,6 @@ public void SetListeningForModerationEvents(bool listen) /// /// /// The updated data for the user. - /// - /// This exception might be thrown when any error occurs while updating the user. - /// /// /// /// var user = // ...; @@ -211,7 +208,6 @@ public async Task Update(ChatUserData updatedData) internal static async Task> UpdateUserData(Chat chat, string userId, ChatUserData chatUserData) { - //TODO: Create a better way to do this var operation = chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true).Uuid(userId); if (!string.IsNullOrEmpty(chatUserData.Username)) { @@ -258,7 +254,7 @@ internal void UpdateLocalData(ChatUserData? newData) userData = newData; } - public override async Task Resync() + public override async Task Refresh() { var newData = await GetUserData(chat, Id); if (!newData.Status.Error) @@ -274,9 +270,6 @@ public override async Task Resync() /// 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 = // ...; @@ -299,9 +292,6 @@ 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 = // ...; @@ -428,9 +418,6 @@ public async Task> GetChannelsR /// /// 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 = // ...; @@ -462,9 +449,6 @@ public async Task IsPresentOn(string channelId) /// /// 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 = // ...; @@ -492,9 +476,6 @@ public async Task>> WherePresent() /// /// 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 = // ...; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs index 4dda823..79d63d7 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs @@ -20,13 +20,18 @@ internal static bool TryParseMessageResult(Chat chat, PNMessageResult me 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; //messageDict["type"].ToString(); + 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); + message = new Message(chat, messageResult.Timetoken.ToString(), text, messageResult.Channel, messageResult.Publisher, type, meta, new List()); return true; } catch (Exception e) @@ -36,6 +41,64 @@ internal static bool TryParseMessageResult(Chat chat, PNMessageResult me 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(); + + //TODO: C# FIX, Meta shouldn't be an object but a deserialized type + var meta = chat.PubnubInstance.JsonPluggableLibrary.ConvertToDictionaryObject(historyItem.Meta) ?? new Dictionary(); + + //TODO: C# FIX, Actions shouldn't be an object but a deserialized type + var actions = new List(); + if (historyItem.Actions is Dictionary actionsDict) + { + foreach (var kvp in actionsDict) + { + var actionType = kvp.Key; + var entries = kvp.Value as Dictionary; + foreach (var entryPair in entries) + { + var actionValue = entryPair.Key; + var actionEntries = entryPair.Value as List; + foreach (var entry in actionEntries) + { + var entryDict = entry as Dictionary; + actions.Add(new MessageAction() + { + TimeToken = entryDict["actionTimetoken"].ToString(), + UserId = entryDict["uuid"].ToString(), + Type = ChatEnumConverters.StringToActionType(actionType), + Value = actionValue + }); + } + } + } + } + + message = new Message(chat, historyItem.Timetoken.ToString(), text, channelId, historyItem.Uuid, type, 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) { @@ -189,7 +252,37 @@ internal static bool TryParseEvent(Chat chat, PNMessageResult messageRes } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNMessageResult into Event of type \"{eventType}\". Exception was: {e.Message}"); + 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; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ExponentialRateLimiter.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ExponentialRateLimiter.cs index 88c2810..7438f82 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ExponentialRateLimiter.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ExponentialRateLimiter.cs @@ -25,23 +25,20 @@ public ExponentialRateLimiter(float exponentialFactor) _cancellationTokenSource = new CancellationTokenSource(); } - public void RunWithinLimits(string id, int baseIntervalMs, Func> task, Action callback, Action errorCallback) + public async void RunWithinLimits(string id, int baseIntervalMs, Func> task, Action callback, Action errorCallback) { if (baseIntervalMs == 0) { // Execute immediately for zero interval - _ = Task.Run(async () => + try { - try - { - var result = await task(); - callback(result); - } - catch (Exception e) - { - errorCallback(e); - } - }); + var result = await task(); + callback(result); + } + catch (Exception e) + { + errorCallback(e); + } return; } From d2008e334ceffc9969e042cf04921ce8d70bb8da Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Tue, 2 Sep 2025 14:07:43 +0200 Subject: [PATCH 15/28] implement message forwarding and getting current user mentions --- .../PubNubChatApi.Tests/ChatTests.cs | 10 +- .../PubnubChatApi/Entities/Channel.cs | 9 +- .../PubnubChatApi/Entities/Chat.cs | 120 ++++++++++++------ .../Entities/Data/UserMentionData.cs | 4 +- .../PubnubChatApi/Entities/Message.cs | 6 +- .../PubnubChatApi/Entities/ThreadChannel.cs | 13 ++ .../PubnubChatApi/Entities/ThreadMessage.cs | 23 ++-- 7 files changed, 123 insertions(+), 62 deletions(-) diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs index ee9d700..6db7af4 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -54,7 +54,7 @@ public async Task TestGetCurrentUserMentions() 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)); @@ -137,7 +137,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"); @@ -169,7 +169,7 @@ 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.Channel.Id == channel.Id && x.Count > 0)); } [Test] @@ -179,13 +179,13 @@ public async Task TestMarkAllMessagesAsRead() await Task.Delay(10000); - Assert.True((await chat.GetUnreadMessagesCounts()).Any(x => x.Channel.Id == channel.Id && x.Count > 0)); + Assert.True(TestUtils.AssertOperation(await chat.GetUnreadMessagesCounts()).Any(x => x.Channel.Id == channel.Id && x.Count > 0)); var res = chat.MarkAllMessagesAsRead(); await Task.Delay(2000); - var counts = await chat.GetUnreadMessagesCounts(); + var counts = TestUtils.AssertOperation(await chat.GetUnreadMessagesCounts()); Assert.False(counts.Any(x => x.Channel.Id == channel.Id && x.Count > 0)); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index 6ecafa4..a2dd4d3 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -338,12 +338,15 @@ async delegate(Pubnub pn, PNPresenceEventResult p) })); } - public async Task ForwardMessage(Message message) + public async Task ForwardMessage(Message message) { - await chat.ForwardMessage(message, this); + return await SendText(message.MessageText, new SendTextParams() + { + Meta = message.Meta + }); } - public async Task EmitUserMention(string userId, string timeToken, string text) + public virtual async Task EmitUserMention(string userId, string timeToken, string text) { var jsonDict = new Dictionary() { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 5f27ac2..028e351 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -78,29 +78,6 @@ internal Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatLis RateLimiter = new ExponentialRateLimiter(chatConfig.RateLimitFactor); } - /// - /// Initializes a new instance of the class. - /// - /// Creates a new chat instance. - /// - /// - /// Config with Chat specific parameters - /// An already initialised instance of Pubnub - /// /// Optional injectable listener factory, used in Unity to allow for dispatching Chat callbacks on main thread. - /// - /// The constructor initializes the Chat object with the provided existing Pubnub instance. - /// - public static async Task CreateInstance(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? listenerFactory = null) - { - var chat = new Chat(chatConfig, pubnub, listenerFactory); - var user = await chat.GetCurrentUser(); - if (user == null) - { - await chat.CreateUser(chat.PubnubInstance.GetCurrentUserId()); - } - return chat; - } - internal Chat(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? listenerFactory = null) { Config = chatConfig; @@ -518,7 +495,9 @@ public async Task GetChannels(string filter = "", strin /// public async Task UpdateChannel(string channelId, ChatChannelData updatedData) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + result.RegisterOperation(await Channel.UpdateChannelData(this, channelId, updatedData)); + return result; } /// @@ -545,10 +524,54 @@ public async Task DeleteChannel(string channelId) #region Users - public async Task GetCurrentUserMentions(string startTimeToken, string endTimeToken, + public async Task> GetCurrentUserMentions(string startTimeToken, string endTimeToken, int count) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + var id = PubnubInstance.GetCurrentUserId(); + var getEventHistory = await GetEventsHistory(id, startTimeToken, endTimeToken, count); + 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()); + 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; } /// @@ -980,9 +1003,11 @@ public async Task UpdateUser(string userId, ChatUserData updatedData) /// chat.DeleteUser("user_id"); /// /// - public async Task DeleteUser(string userId) + public async Task DeleteUser(string userId) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + result.RegisterOperation(await PubnubInstance.RemoveUuidMetadata().Uuid(userId).ExecuteAsync()); + return result; } #endregion @@ -1205,22 +1230,32 @@ public async Task> GetMessageReportsHi /// Message object if one was found, null otherwise. public async Task> GetMessage(string channelId, string messageTimeToken) { - throw new NotImplementedException(); - /*return await Task.Run(() => + var result = new ChatOperationResult(); + var startTimeToken = (long.Parse(messageTimeToken) + 1).ToString(); + var getHistory = await GetChannelMessageHistory(channelId, startTimeToken, messageTimeToken, 1); + 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; + } + //TODO: wrappers rethink + result.Result = getHistory.Result[0]; + return result; } - public async Task MarkAllMessagesAsRead(string filter = "", string sort = "", + public async Task> MarkAllMessagesAsRead(string filter = "", string sort = "", int limit = 0, PNPageObject page = null) { throw new NotImplementedException(); } - public async Task> GetUnreadMessagesCounts(string filter = "", string sort = "", + public async Task>> GetUnreadMessagesCounts(string filter = "", string sort = "", int limit = 0, PNPageObject page = null) { @@ -1279,9 +1314,16 @@ public async Task> GetThreadChannel(Message m return result; } - public async Task ForwardMessage(Message message, Channel channel) + public async Task ForwardMessage(string messageTimeToken, string channelId) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + var getMessage = await GetMessage(channelId, messageTimeToken); + if (result.RegisterOperation(getMessage)) + { + return result; + } + result.RegisterOperation(await getMessage.Result.Forward(channelId)); + return result; } public async void AddListenerToMessagesUpdate(string channelId, List messageTimeTokens, @@ -1356,16 +1398,18 @@ public async Task>> GetChannelMessageHistory(s var getHistory = await PubnubInstance.FetchHistory().Channels(new[] { channelId }) .Start(long.Parse(startTimeToken)).End(long.Parse(endTimeToken)).MaximumPerChannel(count).IncludeMessageActions(true) .IncludeMeta(true).ExecuteAsync(); - if (result.RegisterOperation(getHistory) || !getHistory.Result.Messages.ContainsKey(channelId)) + 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)) { + //TODO: wrappers rethink result.Result.Add(message); } } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionData.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionData.cs index bccfa12..1506187 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionData.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UserMentionData.cs @@ -7,11 +7,9 @@ namespace PubnubChatApi.Entities.Data public class UserMentionData { public string ChannelId; + public string ParentChannelId; public string UserId; public ChatEvent Event; public Message Message; - - public string ParentChannelId; - public string ThreadChannelId; } } \ 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 df97321..1b82a66 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -360,12 +360,12 @@ public async Task Report(string reason) public async Task Forward(string channelId) { var result = new ChatOperationResult(); - var channel = await chat.GetChannel(channelId); - if (result.RegisterOperation(channel)) + var getChannel = await chat.GetChannel(channelId); + if (result.RegisterOperation(getChannel)) { return result; } - result.RegisterOperation(await chat.ForwardMessage(this, channel.Result)); + result.RegisterOperation(await getChannel.Result.ForwardMessage(this)); return result; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs index 71ed29c..bac1aad 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -76,6 +76,19 @@ public async Task>> GetThreadHistory(str 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)); + } + public async Task PinMessageToParentChannel(ThreadMessage message) { throw new NotImplementedException(); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs index 9c4e046..ab23551 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -11,23 +11,27 @@ namespace PubNubChatAPI.Entities 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) + + 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)) + return chat.ListenerFactory.ProduceListener( + messageActionCallback: delegate(Pubnub pn, PNMessageActionEventResult e) { - OnThreadMessageUpdated?.Invoke(this); - } - }); + if (ChatParsers.TryParseMessageUpdate(chat, this, e)) + { + OnThreadMessageUpdated?.Invoke(this); + } + }); } public async Task PinMessageToParentChannel() @@ -39,6 +43,5 @@ public async Task UnPinMessageFromParentChannel() { throw new NotImplementedException(); } - } } \ No newline at end of file From 39dbe7829e78d118e306ebacec80072922e27e09 Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Thu, 4 Sep 2025 15:14:44 +0200 Subject: [PATCH 16/28] removed wrappers storage in chat entity, added unread messages functionality --- .../PubNubChatApi.Tests/ChatTests.cs | 27 +- .../PubNubChatApi.Tests/MembershipTests.cs | 8 +- .../PubnubChatApi/Entities/Base/ChatEntity.cs | 3 +- .../PubnubChatApi/Entities/Channel.cs | 56 ++-- .../PubnubChatApi/Entities/Chat.cs | 282 +++++++++--------- .../Entities/ChatAccessManager.cs | 27 -- .../Data/MarkMessagesAsReadWrapper.cs | 2 +- .../Entities/Data/UnreadMessageWrapper.cs | 2 +- .../PubnubChatApi/Entities/Membership.cs | 61 ++-- .../PubnubChatApi/Entities/Message.cs | 2 +- .../PubnubChatApi/Entities/ThreadChannel.cs | 2 - .../PubnubChatApi/Entities/User.cs | 13 +- .../PubnubChatApi/Utilities/ChatUtils.cs | 11 +- 13 files changed, 241 insertions(+), 255 deletions(-) diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs index 6db7af4..991054e 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -16,7 +16,8 @@ public class ChatTests [SetUp] public async Task Setup() { - chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), new PNConfiguration(new UserId("chats_tests_user_10_no_calkiem_nowy_2")) + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(storeUserActivityTimestamp: true), + new PNConfiguration(new UserId("chats_tests_user_fresh_3")) { PublishKey = PubnubTestsParameters.PublishKey, SubscribeKey = PubnubTestsParameters.SubscribeKey @@ -169,25 +170,33 @@ public async Task TestGetUnreadMessagesCounts() await Task.Delay(3000); - Assert.True(TestUtils.AssertOperation(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(TestUtils.AssertOperation(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()); + 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] diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs index 12699f2..54cbe6e 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs @@ -41,14 +41,14 @@ await chat.PubnubInstance.RemoveMemberships().Channels(new List() { "mem [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) { @@ -117,7 +117,7 @@ public async Task TestLastRead() 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) { @@ -174,7 +174,7 @@ public async Task TestUnreadMessagesCount() 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) { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs index b8a214b..aed7e69 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using PubnubApi; +using PubnubChatApi.Entities.Data; namespace PubNubChatAPI.Entities { @@ -39,6 +40,6 @@ public virtual void SetListeningForUpdates(bool listen) protected abstract SubscribeCallback CreateUpdateListener(); - public abstract Task Refresh(); + public abstract Task Refresh(); } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index a2dd4d3..946e90c 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -205,13 +205,16 @@ internal static async Task> GetChannelData( .ExecuteAsync(); } - public override async Task Refresh() + public override async Task Refresh() { - var getResult = await GetChannelData(chat, Id); - if (!getResult.Status.Error) + var result = new ChatOperationResult(); + var getData = await GetChannelData(chat, Id); + if (result.RegisterOperation(getData)) { - UpdateLocalData(getResult.Result); + return result; } + UpdateLocalData(getData.Result); + return result; } public void SetListeningForCustomEvents(bool listen) @@ -458,16 +461,22 @@ public async void Leave() { Disconnect(); var currentUserId = chat.PubnubInstance.GetCurrentUserId(); - var remove = await chat.PubnubInstance.RemoveMemberships().Uuid(currentUserId).Channels(new List() { Id }) + var remove = 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(); if (remove.Status.Error) { chat.Logger.Error($"Error when trying to leave channel \"{Id}\": {remove.Status.ErrorData.Information}"); return; } - - //TODO: wrappers rethink - chat.membershipWrappers.Remove(currentUserId + Id); } /// @@ -496,27 +505,9 @@ public void Connect() { if (ChatParsers.TryParseMessageResult(chat, m, out var message)) { - //TODO: wrappers rethink - //chat.RegisterMessage(message); OnMessageReceived?.Invoke(message); } })); - /*if (subscription != null) - { - return; - } - subscription = chat.PubnubInstance.Channel(Id).Subscription(SubscriptionOptions.None); - subscription.AddListener(chat.ListenerFactory.ProduceListener(messageCallback: - delegate(Pubnub pn, PNMessageResult m) - { - if (ChatParsers.TryParseMessageResult(chat, m, out var message)) - { - //TODO: wrappers rethink - //chat.RegisterMessage(message); - OnMessageReceived?.Invoke(message); - } - })); - subscription.Subscribe();*/ } /// @@ -568,17 +559,8 @@ public async void Join(ChatMembershipData? membershipData = null) chat.Logger.Error($"Error when trying to Join() to channel \"{Id}\": {response.Status.ErrorData.Information}"); return; } - //TODO: wrappers rethink - if (chat.membershipWrappers.TryGetValue(currentUserId + Id, out var existingHostMembership)) - { - existingHostMembership.UpdateLocalData(membershipData); - } - else - { - var joinMembership = new Membership(chat, currentUserId, Id, membershipData); - await joinMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()); - chat.membershipWrappers.Add(joinMembership.Id, joinMembership); - } + var joinMembership = new Membership(chat, currentUserId, Id, membershipData); + await joinMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()); Connect(); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 028e351..47bb904 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -27,13 +27,6 @@ public class Chat internal const string INTERNAL_ADMIN_CHANNEL = "PUBNUB_INTERNAL_ADMIN_CHANNEL"; internal const string MESSAGE_THREAD_ID_PREFIX = "PUBNUB_INTERNAL_THREAD"; - //TODO: wrappers rethink - internal Dictionary channelWrappers = new(); - private Dictionary userWrappers = new(); - internal Dictionary membershipWrappers = new(); - private Dictionary messageWrappers = new(); - private bool fetchUpdates = true; - public Pubnub PubnubInstance { get; } public PubnubLogModule Logger => PubnubInstance.PNConfig.Logger; @@ -62,7 +55,7 @@ public static async Task> CreateInstance(PubnubChatCon var chat = new Chat(chatConfig, pubnubConfig, listenerFactory); var result = new ChatOperationResult(){Result = chat}; var getUser = await chat.GetCurrentUser(); - if (result.RegisterOperation(getUser)) + if (getUser.Error) { result.RegisterOperation(await chat.CreateUser(chat.PubnubInstance.GetCurrentUserId())); } @@ -170,7 +163,6 @@ public async Task> CreatePublicConversation(string } var channel = new Channel(this, channelId, additionalData); result.Result = channel; - channelWrappers.Add(channelId, channel); return result; } @@ -231,20 +223,10 @@ private async Task> CreateConversatio return result; } - if (membershipWrappers.TryGetValue(currentUserId + channelId, out var existingHostMembership)) - { - existingHostMembership.UpdateLocalData(membershipData); - result.Result.HostMembership = existingHostMembership; - } - else - { - var hostMembership = new Membership(this, currentUserId, channelId, membershipData); - membershipWrappers.Add(hostMembership.Id, hostMembership); - result.Result.HostMembership = hostMembership; - } + var hostMembership = new Membership(this, currentUserId, channelId, membershipData); + result.Result.HostMembership = hostMembership; var channel = new Channel(this, channelId, channelData); - channelWrappers.Add(channelId, channel); result.Result.CreatedChannel = channel; if (type == "direct") @@ -336,7 +318,6 @@ public async Task> InviteToChannel(string channe var newMembership = new Membership(this, userId, channelId, new ChatMembershipData()); await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()); - membershipWrappers.Add(newMembership.Id, newMembership); result.Result = newMembership; return result; @@ -370,24 +351,12 @@ public async Task>> InviteMultipleToChannel return result; } - var usersDict = users.ToDictionary(x => x.Id, y => y); foreach (var channelMember in inviteResponse.Result.ChannelMembers) { var userId = channelMember.UuidMetadata.Uuid; - if (membershipWrappers.TryGetValue(userId + channelId, - out var existingMembership)) - { - usersDict[userId].UpdateLocalData(channelMember.UuidMetadata); - existingMembership.UpdateLocalData(channelMember); - result.Result.Add(existingMembership); - } - else - { - var newMembership = new Membership(this, userId, channelId, channelMember); - await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()); - membershipWrappers.Add(newMembership.Id, newMembership); - result.Result.Add(newMembership); - } + var newMembership = new Membership(this, userId, channelId, channelMember); + await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()); + result.Result.Add(newMembership); var inviteEventPayload = $"{{\"channelType\": \"{channel.Result.Type}\", \"channelId\": {channelId}}}"; await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload); @@ -406,22 +375,13 @@ public async Task>> InviteMultipleToChannel public async Task> GetChannel(string channelId) { var result = new ChatOperationResult(); - if (channelWrappers.TryGetValue(channelId, out var existingChannel)) - { - await existingChannel.Refresh(); - result.Result = existingChannel; - } - else + var getResult = await Channel.GetChannelData(this, channelId); + if (result.RegisterOperation(getResult)) { - var getResult = await Channel.GetChannelData(this, channelId); - if (result.RegisterOperation(getResult)) - { - return result; - } - var channel = new Channel(this, channelId, getResult.Result); - channelWrappers.Add(channelId, channel); - result.Result = channel; + return result; } + var channel = new Channel(this, channelId, getResult.Result); + result.Result = channel; return result; } @@ -460,17 +420,8 @@ public async Task GetChannels(string filter = "", strin }; foreach (var resultMetadata in response.Result.Channels) { - if (channelWrappers.TryGetValue(resultMetadata.Channel, out var existingChannelWrapper)) - { - existingChannelWrapper.UpdateLocalData(resultMetadata); - wrapper.Channels.Add(existingChannelWrapper); - } - else - { - var channel = new Channel(this, resultMetadata.Channel, resultMetadata); - channelWrappers.Add(channel.Id, channel); - wrapper.Channels.Add(channel); - } + var channel = new Channel(this, resultMetadata.Channel, resultMetadata); + wrapper.Channels.Add(channel); } return wrapper; } @@ -733,7 +684,7 @@ public async Task> CreateUser(string userId, ChatUserD { var result = new ChatOperationResult(); var existingUser = await GetUser(userId); - if (!result.RegisterOperation(result)) + if (!result.RegisterOperation(existingUser)) { result.Result = existingUser.Result; return result; @@ -745,7 +696,6 @@ public async Task> CreateUser(string userId, ChatUserD return result; } var user = new User(this, userId, additionalData); - userWrappers.Add(userId, user); result.Result = user; return result; } @@ -866,19 +816,12 @@ public async Task>> WherePresent(string userId) public async Task> GetUser(string userId) { var result = new ChatOperationResult(); - if (userWrappers.TryGetValue(userId, out var existingUser)) - { - await existingUser.Refresh(); - result.Result = existingUser; - return result; - } var getData = await User.GetUserData(this, userId); if (result.RegisterOperation(getData)) { return result; } var user = new User(this, userId, getData.Result); - userWrappers.Add(userId, user); result.Result = user; return result; } @@ -945,17 +888,8 @@ public async Task GetUsers(string filter = "", string sort }; foreach (var resultMetadata in result.Result.Uuids) { - if (userWrappers.TryGetValue(resultMetadata.Uuid, out var existingUserWrapper)) - { - existingUserWrapper.UpdateLocalData(resultMetadata); - response.Users.Add(existingUserWrapper); - } - else - { - var user = new User(this, resultMetadata.Uuid, resultMetadata); - userWrappers.Add(user.Id, user); - response.Users.Add(user); - } + var user = new User(this, resultMetadata.Uuid, resultMetadata); + response.Users.Add(user); } return response; } @@ -980,14 +914,7 @@ public async Task GetUsers(string filter = "", string sort /// public async Task UpdateUser(string userId, ChatUserData updatedData) { - if (userWrappers.TryGetValue(userId, out var existingUserWrapper)) - { - await existingUserWrapper.Update(updatedData); - } - else - { - await User.UpdateUserData(this, userId, updatedData); - } + await User.UpdateUserData(this, userId, updatedData); } /// @@ -1041,12 +968,11 @@ public async Task DeleteUser(string userId) /// /// /// - public async Task GetUserMemberships(string userId, string filter = "", + public async Task> GetUserMemberships(string userId, string filter = "", string sort = "", int limit = 0, PNPageObject page = null) { - - //TODO: here also, has to be a better way to structure this arguments -> builder pattern + var result = new ChatOperationResult(); var operation = PubnubInstance.GetMemberships().Include( new[] { @@ -1073,49 +999,29 @@ public async Task GetUserMemberships(string userId, stri { operation.Page(page); } - var result = await operation.ExecuteAsync(); - if (result.Status.Error) + var getMemberships = await operation.ExecuteAsync(); + if (result.RegisterOperation(getMemberships)) { - Logger.Error($"Error when trying to get \"{userId}\" user memberships: {result.Status.ErrorData.Information}"); - return null; + return result; } var memberships = new List(); - foreach (var membershipResult in result.Result.Memberships) + foreach (var membershipResult in getMemberships.Result.Memberships) { - var membershipId = userId + membershipResult.ChannelMetadata.Channel; - if (membershipWrappers.TryGetValue(membershipId, out var existingMembershipWrapper)) + memberships.Add(new Membership(this, userId, membershipResult.ChannelMetadata.Channel, new ChatMembershipData() { - existingMembershipWrapper.MembershipData.CustomData = membershipResult.Custom; - memberships.Add(existingMembershipWrapper); - } - else - { - memberships.Add(new Membership(this, userId, membershipResult.ChannelMetadata.Channel, new ChatMembershipData() - { - CustomData = membershipResult.Custom, - Status = membershipResult.Status, - Type = membershipResult.Type - })); - } + CustomData = membershipResult.Custom, + Status = membershipResult.Status, + Type = membershipResult.Type + })); } - return new MembersResponseWrapper() + result.Result = new MembersResponseWrapper() { Memberships = memberships, - Page = result.Result.Page, - Total = result.Result.TotalCount + Page = getMemberships.Result.Page, + Total = getMemberships.Result.TotalCount }; - } - - public void AddListenerToMembershipsUpdate(List membershipIds, Action listener) - { - foreach (var membershipId in membershipIds) - { - if (membershipWrappers.TryGetValue(membershipId, out var membership)) - { - membership.OnMembershipUpdated += listener; - } - } + return result; } /// @@ -1186,21 +1092,12 @@ public async Task> GetChannelMembers var memberships = new List(); foreach (var channelMemberResult in getResult.Result.ChannelMembers) { - var membershipId = channelMemberResult.UuidMetadata.Uuid + channelId; - if (membershipWrappers.TryGetValue(membershipId, out var existingMembershipWrapper)) + memberships.Add(new Membership(this, channelMemberResult.UuidMetadata.Uuid, channelId, new ChatMembershipData() { - existingMembershipWrapper.MembershipData.CustomData = channelMemberResult.Custom; - memberships.Add(existingMembershipWrapper); - } - else - { - memberships.Add(new Membership(this, channelMemberResult.UuidMetadata.Uuid, channelId, new ChatMembershipData() - { - CustomData = channelMemberResult.Custom, - Status = channelMemberResult.Status, - Type = channelMemberResult.Type - })); - } + CustomData = channelMemberResult.Custom, + Status = channelMemberResult.Status, + Type = channelMemberResult.Type + })); } result.Result = new MembersResponseWrapper() { @@ -1243,7 +1140,6 @@ public async Task> GetMessage(string channelId, str result.Exception = new PNException($"Didn't find any message with timetoken {messageTimeToken} on channel {channelId}"); return result; } - //TODO: wrappers rethink result.Result = getHistory.Result[0]; return result; } @@ -1252,14 +1148,111 @@ public async Task> MarkAllMessage int limit = 0, PNPageObject page = null) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + 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(); + if (result.RegisterOperation(getCurrentUser)) + { + return result; + } + var getCurrentMemberships = await getCurrentUser.Result.GetMemberships(filter, sort, limit, page); + 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["lastReadMessageTimetoken"] = timeToken; + } + if (result.RegisterOperation(await Membership.UpdateMembershipsData(this, currentUserId, memberships))) + { + return result; + } + foreach (var membership in memberships) + { + await EmitEvent(PubnubChatEventType.Receipt, membership.ChannelId, + $"{{\"messageTimetoken\": \"{timeToken}\"}}"); + } + result.Result = new MarkMessagesAsReadWrapper() + { + Memberships = memberships, + Page = getCurrentMemberships.Result.Page, + Status = getCurrentMemberships.Result.Status, + Total = getCurrentMemberships.Result.Total + }; + return result; } - + public async Task>> GetUnreadMessagesCounts(string filter = "", string sort = "", int limit = 0, PNPageObject page = null) { - throw new NotImplementedException(); + var result = new ChatOperationResult>(); + 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(); + if (result.RegisterOperation(getCurrentUser)) + { + return result; + } + var getCurrentMemberships = await getCurrentUser.Result.GetMemberships(filter, sort, limit, page); + 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); + } + //TODO: ISSUE: count also includes events + var getCounts = await PubnubInstance.MessageCounts().Channels(channelIds.ToArray()).ChannelsTimetoken(timeTokens.ToArray()) + .ExecuteAsync(); + 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; } public async Task> CreateThreadChannel(string messageTimeToken, string messageChannelId) @@ -1409,7 +1402,6 @@ public async Task>> GetChannelMessageHistory(s { if (ChatParsers.TryParseMessageFromHistory(this, channelId, historyItem, out var message)) { - //TODO: wrappers rethink result.Result.Add(message); } } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs index aae87da..abf76a3 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs @@ -4,7 +4,6 @@ namespace PubNubChatAPI.Entities { - //TODO: now that core SDK is exposed do we still need SetAuthToken, ParseToken, and SetPubnubOrigin? public class ChatAccessManager { @@ -19,31 +18,5 @@ public async Task CanI(PubnubAccessPermission permission, PubnubAccessReso { throw new NotImplementedException(); } - - /// - /// Sets a new token for this Chat instance. - /// - public void SetAuthToken(string token) - { - throw new NotImplementedException(); - } - - /// - /// Decodes an existing token. - /// - /// A JSON string object containing permissions embedded in that token. - public string ParseToken(string token) - { - throw new NotImplementedException(); - } - - /// - /// Sets a new custom origin value. - /// - /// - public void SetPubnubOrigin(string origin) - { - throw new NotImplementedException(); - } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs index fe1b959..b8ca94e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs @@ -10,7 +10,7 @@ public struct MarkMessagesAsReadWrapper { public PNPageObject Page; public int Total; - public int Status; + public string Status; public List Memberships; } } \ 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 ab011de..ea1844d 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs @@ -6,7 +6,7 @@ namespace PubnubChatApi.Entities.Data { public struct UnreadMessageWrapper { - public Channel Channel; + public string ChannelId; public Membership Membership; public int Count; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs index 4540ab0..067e2d4 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using PubnubApi; using PubnubChatApi.Entities.Data; @@ -24,7 +25,7 @@ namespace PubNubChatAPI.Entities public class Membership : UniqueChatEntity { //Message counts requires a valid timetoken, so this one will be like "0", from beginning of the channel - private const long EMPTY_TIMETOKEN = 17000000000000000; + internal const long EMPTY_TIMETOKEN = 17000000000000000; /// /// The user ID of the user that this membership belongs to. @@ -96,18 +97,19 @@ protected override SubscribeCallback CreateUpdateListener() /// /// The ChatMembershipData object to update the membership with. /// - public async Task Update(ChatMembershipData membershipData) + public async Task Update(ChatMembershipData membershipData) { - var updateSuccess = await UpdateMembershipData(membershipData); - if (updateSuccess) + var result = (await UpdateMembershipData(membershipData)).ToChatOperationResult(); + if (!result.Error) { UpdateLocalData(membershipData); } + return result; } - internal async Task UpdateMembershipData(ChatMembershipData membershipData) + internal async Task> UpdateMembershipData(ChatMembershipData membershipData) { - var updateResponse = await chat.PubnubInstance.SetMemberships().Uuid(UserId).Channels(new List() + return await chat.PubnubInstance.SetMemberships().Uuid(UserId).Channels(new List() { new() { @@ -126,28 +128,46 @@ internal async Task UpdateMembershipData(ChatMembershipData membershipData PNMembershipField.CHANNEL_TYPE, PNMembershipField.CHANNEL_STATUS }).ExecuteAsync(); - - if (updateResponse.Status.Error) + } + + internal static async Task> UpdateMembershipsData(Chat chat, string userId, List memberships) + { + var pnMemberships = memberships.Select(membership => new PNMembership() { - chat.Logger.Error($"Error when trying to update membership (channel: {ChannelId}, user: {UserId}): {updateResponse.Status.ErrorData.Information}"); - return false; - } - - return true; + 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(); } - public async Task SetLastReadMessage(Message message) + public async Task SetLastReadMessage(Message message) { - await SetLastReadMessageTimeToken(message.TimeToken); + return await SetLastReadMessageTimeToken(message.TimeToken); } - public async Task SetLastReadMessageTimeToken(string timeToken) + public async Task SetLastReadMessageTimeToken(string timeToken) { + var result = new ChatOperationResult(); MembershipData.CustomData["lastReadMessageTimetoken"] = timeToken; - if (await UpdateMembershipData(MembershipData)) + var update = await UpdateMembershipData(MembershipData); + if (result.RegisterOperation(update)) { - await chat.EmitEvent(PubnubChatEventType.Receipt, ChannelId, $"{{\"messageTimetoken\": \"{timeToken}\"}}"); + return result; } + result.RegisterOperation(await chat.EmitEvent(PubnubChatEventType.Receipt, ChannelId, + $"{{\"messageTimetoken\": \"{timeToken}\"}}")); + return result; } public async Task GetUnreadMessagesCount() @@ -168,10 +188,9 @@ public async Task GetUnreadMessagesCount() return countsResponse.Result.Channels[ChannelId]; } - public override async Task Refresh() + public override async Task Refresh() { - //TODO: wrappers rethink - await chat.GetChannelMemberships(ChannelId, filter:$"uuid.id == \"{UserId}\""); + return await chat.GetChannelMemberships(ChannelId, filter:$"uuid.id == \"{UserId}\""); } } } \ 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 1b82a66..657edcb 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -406,7 +406,7 @@ public async Task Delete(bool soft) throw new NotImplementedException(); } - public override Task Refresh() + public override Task Refresh() { throw new NotImplementedException(); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs index bac1aad..791034f 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -29,8 +29,6 @@ private async Task InitThreadChannel() { return result; } - - chat.channelWrappers.Add(Id, this); result.RegisterOperation(await chat.PubnubInstance.AddMessageAction() .Action(new PNMessageAction() { Type = "threadRootId", Value = Id }).Channel(ParentChannelId) .MessageTimetoken(long.Parse(ParentMessageTimeToken)).ExecuteAsync()); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index b1c4543..961e68e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -254,13 +254,16 @@ internal void UpdateLocalData(ChatUserData? newData) userData = newData; } - public override async Task Refresh() + public override async Task Refresh() { - var newData = await GetUserData(chat, Id); - if (!newData.Status.Error) + var result = new ChatOperationResult(); + var getUserData = await GetUserData(chat, Id); + if (result.RegisterOperation(getUserData)) { - UpdateLocalData(newData.Result); + return result; } + UpdateLocalData(getUserData.Result); + return result; } /// @@ -486,7 +489,7 @@ public async Task>> WherePresent() /// /// /// - public async Task GetMemberships(string filter = "", string sort = "", int limit = 0, + public async Task> GetMemberships(string filter = "", string sort = "", int limit = 0, PNPageObject page = null) { return await chat.GetUserMemberships(Id, filter, sort, limit, page); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs index 2d921d6..4730a73 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs @@ -1,5 +1,7 @@ using System; using System.Globalization; +using PubnubApi; +using PubnubChatApi.Entities.Data; namespace PubnubChatApi.Utilities { @@ -8,8 +10,15 @@ 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); + var timeStamp = Convert.ToInt64(timeSpan.TotalSeconds * 10000000); return timeStamp.ToString(CultureInfo.InvariantCulture); } + + internal static ChatOperationResult ToChatOperationResult(this PNResult result) + { + var operationResult = new ChatOperationResult(); + operationResult.RegisterOperation(result); + return operationResult; + } } } \ No newline at end of file From dd7410001774dcc9c69f3e67fb7705f1c0922ccf Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Thu, 4 Sep 2025 15:50:18 +0200 Subject: [PATCH 17/28] implement message pinning and unpinning --- .../PubNubChatApi.Tests/ChannelTests.cs | 17 ++++--- .../PubNubChatApi.Tests/MessageTests.cs | 4 +- .../PubNubChatApi.Tests/ThreadsTests.cs | 16 ++++--- .../PubnubChatApi/Entities/Channel.cs | 46 +++++++++++++------ .../PubnubChatApi/Entities/Message.cs | 11 ++++- .../PubnubChatApi/Entities/ThreadChannel.cs | 9 ++-- .../PubnubChatApi/Entities/ThreadMessage.cs | 8 ++-- 7 files changed, 69 insertions(+), 42 deletions(-) diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs index 6cbfc6d..bc4846a 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs @@ -232,7 +232,8 @@ public async Task TestPinMessage() 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"); @@ -253,18 +254,20 @@ public async Task TestUnPinMessage() 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"); TestUtils.AssertOperation(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); } diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs index 9ff8bea..750722c 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs @@ -222,8 +222,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"); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs index 7391926..796a971 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs @@ -82,15 +82,15 @@ public async Task TestThreadChannelParentChannelPinning() 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"); @@ -145,13 +145,15 @@ public async Task TestThreadMessageParentChannelPinning() 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"); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index 946e90c..c37ec78 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -45,7 +45,7 @@ public class Channel : UniqueChatEntity /// The custom data that can be used to store additional information about the channel. /// /// - public Dictionary CustomData => channelData.CustomData; + public Dictionary CustomData => channelData.CustomData ?? new (); /// /// The information about the last update of the channel. @@ -191,7 +191,7 @@ internal static async Task> UpdateChannelDa { operation = operation.Status(data.Status); } - if (data.CustomData != null && data.CustomData.Any()) + if (data.CustomData != null) { operation = operation.Custom(data.CustomData); } @@ -373,24 +373,20 @@ public async Task StopTyping() public async Task PinMessage(Message message) { - throw new NotImplementedException(); + channelData.CustomData ??= new (); + channelData.CustomData["pinnedMessageChannelID"] = message.ChannelId; + channelData.CustomData["pinnedMessageTimetoken"] = message.TimeToken; + return (await UpdateChannelData(chat, Id, channelData)).ToChatOperationResult(); } public async Task UnpinMessage() { - throw new NotImplementedException(); + channelData.CustomData ??= new (); + channelData.CustomData.Remove("pinnedMessageChannelID"); + channelData.CustomData.Remove("pinnedMessageTimetoken"); + return (await UpdateChannelData(chat, Id, channelData)).ToChatOperationResult(); } - /// - /// 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. - /// - public bool TryGetPinnedMessage(out Message pinnedMessage) - { - throw new NotImplementedException(); - } /// /// Asynchronously tries to get the Message pinned to this Channel. @@ -398,7 +394,27 @@ public bool TryGetPinnedMessage(out Message pinnedMessage) /// The pinned Message object if there was one, null otherwise. public async Task> GetPinnedMessage() { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + if (result.RegisterOperation(await Refresh())) + { + 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()); + if (result.RegisterOperation(getMessage)) + { + return result; + } + + result.Result = getMessage.Result; + return result; } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs index 657edcb..0fb7599 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -338,9 +338,16 @@ public async Task RemoveThread() return result; } - public async Task Pin() + public async Task Pin() { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + var getChannel = await chat.GetChannel(ChannelId); + if (result.RegisterOperation(getChannel)) + { + return result; + } + result.RegisterOperation(await getChannel.Result.PinMessage(this)); + return result; } public async Task Report(string reason) diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs index 791034f..dbd151d 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Threading.Tasks; using PubnubApi; @@ -87,14 +86,14 @@ public override async Task EmitUserMention(string userId, s chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)); } - public async Task PinMessageToParentChannel(ThreadMessage message) + public async Task PinMessageToParentChannel(ThreadMessage message) { - throw new NotImplementedException(); + return await chat.PinMessageToChannel(ParentChannelId, message); } - public async Task UnPinMessageFromParentChannel() + public async Task UnPinMessageFromParentChannel() { - throw new NotImplementedException(); + return await chat.UnpinMessageFromChannel(ParentChannelId); } } } \ 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 ab23551..2635403 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -34,14 +34,14 @@ protected override SubscribeCallback CreateUpdateListener() }); } - public async Task PinMessageToParentChannel() + public async Task PinMessageToParentChannel() { - throw new NotImplementedException(); + return await chat.PinMessageToChannel(ParentChannelId, this); } - public async Task UnPinMessageFromParentChannel() + public async Task UnPinMessageFromParentChannel() { - throw new NotImplementedException(); + return await chat.UnpinMessageFromChannel(ParentChannelId); } } } \ No newline at end of file From 1b289f9e1d916e695a9975504657c3f3e670d066 Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Thu, 11 Sep 2025 14:39:24 +0200 Subject: [PATCH 18/28] finish implementing all methods --- .../PubNubChatApi.Tests/ChatTests.cs | 21 ++- .../PubNubChatApi.Tests/MembershipTests.cs | 11 +- .../PubNubChatApi.Tests/MessageTests.cs | 22 +-- .../PubNubChatApi.Tests/ThreadsTests.cs | 4 +- .../PubNubChatApi.Tests/UserTests.cs | 14 +- .../PubnubChatApi/Entities/Base/ChatEntity.cs | 6 +- .../PubnubChatApi/Entities/Channel.cs | 27 +++- .../PubnubChatApi/Entities/Chat.cs | 59 +++++++- .../Entities/ChatAccessManager.cs | 30 +++- .../PubnubChatApi/Entities/Membership.cs | 7 +- .../PubnubChatApi/Entities/Message.cs | 138 ++++++++++++++++-- .../PubnubChatApi/Entities/ThreadChannel.cs | 2 + .../PubnubChatApi/Entities/User.cs | 38 ++++- .../PubnubChatApi/PubnubChatApi.csproj | 2 +- .../PubnubChatApi/Utilities/ChatParsers.cs | 38 ++--- .../PubnubChatApi/Utilities/ChatUtils.cs | 7 + 16 files changed, 327 insertions(+), 99 deletions(-) diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs index 991054e..76295d1 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -101,12 +101,16 @@ public async Task TestGetChannels() public async Task TestCreateDirectConversation() { var convoUser = await chat.GetOrCreateUser("direct_conversation_user"); + var id = Guid.NewGuid().ToString(); var directConversation = TestUtils.AssertOperation( - await chat.CreateDirectConversation(convoUser, "direct_conversation_test")); - Assert.True(directConversation.CreatedChannel is { Id: "direct_conversation_test" }); + 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] @@ -115,13 +119,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 id = Guid.NewGuid().ToString(); var groupConversation = TestUtils.AssertOperation(await - chat.CreateGroupConversation([convoUser1, convoUser2, convoUser3], "group_conversation_test")); - Assert.True(groupConversation.CreatedChannel is { Id: "group_conversation_test" }); + 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] @@ -152,7 +160,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); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs index 54cbe6e..24146d9 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs @@ -62,24 +62,25 @@ public async Task TestUpdateMemberships() { {"key", Guid.NewGuid().ToString()} }, - Type = "some_membership", + 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.CustomData; - Assert.True(updatedData["key"].ToString() == updateData.CustomData["key"].ToString()); + 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); } diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs index 750722c..4d11088 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs @@ -47,15 +47,7 @@ public async Task TestSendAndReceive() Assert.True(message.Type == PubnubChatMessageType.Text); manualReceiveEvent.Set(); }; - await channel.SendText("Test message text", new SendTextParams() - { - //TODO: C# FIX, re-enable as soon as UserMetadata is in correct format - /*MentionedUsers = new Dictionary() { { 0, new MentionedUser() - { - Id = user.Id, - Name = user.UserName - } } },*/ - }); + await channel.SendText("Test message text"); var received = manualReceiveEvent.WaitOne(6000); Assert.IsTrue(received); } @@ -84,8 +76,8 @@ public async Task TestReceivingMessageData() 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(); } }; @@ -99,19 +91,21 @@ public async Task TestReceivingMessageData() public async Task TestGetMessage() { var manualReceiveEvent = new ManualResetEvent(false); + ChatOperationResult receivedMessage = null; channel.OnMessageReceived += async message => { + await Task.Delay(3000); if (message.ChannelId == channel.Id) { - var getMessage = await chat.GetMessage(channel.Id, message.TimeToken); - Assert.True(!getMessage.Error, $"Error when trying to GetMessage(): {getMessage.Error}"); + receivedMessage = await chat.GetMessage(channel.Id, message.TimeToken); manualReceiveEvent.Set(); } }; await channel.SendText("something"); - var received = manualReceiveEvent.WaitOne(4000); + var received = manualReceiveEvent.WaitOne(8000); Assert.IsTrue(received); + Assert.True(!receivedMessage.Error, $"Error when trying to GetMessage(): {receivedMessage.Exception?.Message}"); } [Test] diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs index 796a971..dabc42e 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs @@ -82,13 +82,13 @@ public async Task TestThreadChannelParentChannelPinning() await thread.PinMessageToParentChannel(threadMessage); await Task.Delay(7000); - + var pinned = TestUtils.AssertOperation(await channel.GetPinnedMessage()); Assert.True(pinned.MessageText == "thread init message"); await thread.UnPinMessageFromParentChannel(); await Task.Delay(7000); - + var getPinned = await channel.GetPinnedMessage(); Assert.True(getPinned.Error); historyReadReset.Set(); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs index 834a52c..200e496 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs @@ -56,10 +56,10 @@ 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 => { @@ -72,8 +72,6 @@ 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, @@ -88,7 +86,11 @@ await testUser.Update(new ChatUserData() Type = "someType" }); var updated = updatedReset.WaitOne(15000); + testUser.SetListeningForUpdates(false); Assert.True(updated); + + //Cleanup + await testUser.DeleteUser(); } [Test] diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs index aed7e69..43af9f5 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Base/ChatEntity.cs @@ -7,7 +7,7 @@ namespace PubNubChatAPI.Entities public abstract class ChatEntity { protected Chat chat; - protected Subscription? updateSubscription; + protected Subscription updateSubscription; protected abstract string UpdateChannelId { get; } internal ChatEntity(Chat chat) @@ -15,7 +15,7 @@ internal ChatEntity(Chat chat) this.chat = chat; } - protected void SetListening(Subscription subscription, SubscriptionOptions subscriptionOptions, bool listen, string channelId, SubscribeCallback listener) + protected void SetListening(ref Subscription subscription, SubscriptionOptions subscriptionOptions, bool listen, string channelId, SubscribeCallback listener) { if (listen) { @@ -35,7 +35,7 @@ protected void SetListening(Subscription subscription, SubscriptionOptions subsc public virtual void SetListeningForUpdates(bool listen) { - SetListening(updateSubscription, SubscriptionOptions.None, listen, UpdateChannelId, CreateUpdateListener()); + SetListening(ref updateSubscription, SubscriptionOptions.None, listen, UpdateChannelId, CreateUpdateListener()); } protected abstract SubscribeCallback CreateUpdateListener(); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index c37ec78..12a584e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -195,6 +195,10 @@ internal static async Task> UpdateChannelDa { operation = operation.Custom(data.CustomData); } + if (!string.IsNullOrEmpty(data.Type)) + { + operation = operation.Type(data.Type); + } return await operation.ExecuteAsync(); } @@ -219,7 +223,7 @@ public override async Task Refresh() public void SetListeningForCustomEvents(bool listen) { - SetListening(customEventsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + 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)) @@ -232,7 +236,7 @@ public void SetListeningForCustomEvents(bool listen) public void SetListeningForReportEvents(bool listen) { - SetListening(reportEventsSubscription, SubscriptionOptions.None, listen, $"{Chat.INTERNAL_MODERATION_PREFIX}_{Id}", chat.ListenerFactory.ProduceListener(messageCallback: + 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)) @@ -245,7 +249,7 @@ public void SetListeningForReportEvents(bool listen) public void SetListeningForReadReceiptsEvents(bool listen) { - SetListening(readReceiptsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + SetListening(ref readReceiptsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: async delegate(Pubnub pn, PNMessageResult m) { if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Receipt, out var readEvent)) @@ -270,7 +274,7 @@ async delegate(Pubnub pn, PNMessageResult m) public void SetListeningForTyping(bool listen) { - SetListening(typingEventsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + 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)) @@ -326,7 +330,7 @@ public void SetListeningForTyping(bool listen) public void SetListeningForPresence(bool listen) { - SetListening(presenceEventsSubscription, SubscriptionOptions.ReceivePresenceEvents, listen, Id, chat.ListenerFactory.ProduceListener(presenceCallback: + SetListening(ref presenceEventsSubscription, SubscriptionOptions.ReceivePresenceEvents, listen, Id, chat.ListenerFactory.ProduceListener(presenceCallback: async delegate(Pubnub pn, PNPresenceEventResult p) { var whoIs = await WhoIsPresent(); @@ -451,7 +455,7 @@ public MessageDraft CreateMessageDraft(UserSuggestionSource userSuggestionSource /// public void Disconnect() { - SetListening(subscription, SubscriptionOptions.None, false, Id, null); + SetListening(ref subscription, SubscriptionOptions.None, false, Id, null); } /// @@ -516,7 +520,7 @@ public async void Leave() /// public void Connect() { - SetListening(subscription, SubscriptionOptions.None, true, Id, chat.ListenerFactory.ProduceListener(messageCallback: + SetListening(ref subscription, SubscriptionOptions.None, true, Id, chat.ListenerFactory.ProduceListener(messageCallback: delegate(Pubnub pn, PNMessageResult m) { if (ChatParsers.TryParseMessageResult(chat, m, out var message)) @@ -866,7 +870,14 @@ public async Task> GetUsersRestric /// public async Task> IsUserPresent(string userId) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + var wherePresent = await chat.WherePresent(userId); + if (result.RegisterOperation(wherePresent)) + { + return result; + } + result.Result = wherePresent.Result.Contains(Id); + return result; } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 47bb904..586353e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -38,6 +38,8 @@ public class Chat public PubnubChatConfig Config { get; } internal ExponentialRateLimiter RateLimiter { get; } + private bool storeActivity = false; + /// /// Initializes a new instance of the class. /// @@ -53,6 +55,10 @@ public class Chat 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(){Result = chat}; var getUser = await chat.GetCurrentUser(); if (getUser.Error) @@ -268,7 +274,7 @@ public async Task> InviteToChannel(string channe var result = new ChatOperationResult(); //Check if already a member first var members = await GetChannelMemberships(channelId, filter:$"uuid.id == \"{userId}\""); - if (!result.RegisterOperation(members)) + if (!result.RegisterOperation(members) && members.Result.Memberships.Any()) { //Already a member, just return current membership result.Result = members.Result.Memberships[0]; @@ -354,6 +360,10 @@ public async Task>> InviteMultipleToChannel 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()); result.Result.Add(newMembership); @@ -380,8 +390,16 @@ public async Task> GetChannel(string channelId) { return result; } - var channel = new Channel(this, channelId, getResult.Result); - result.Result = channel; + 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; } @@ -475,6 +493,30 @@ public async Task DeleteChannel(string channelId) #region Users + internal async void StoreActivityTimeStamp() + { + var currentUserId = PubnubInstance.GetCurrentUserId(); + while (storeActivity) + { + var getResult = await User.GetUserData(this, currentUserId); + 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); + continue; + } + data.CustomData ??= new Dictionary(); + data.CustomData["lastActiveTimestamp"] = ChatUtils.TimeTokenNow(); + var setData = await User.UpdateUserData(this, currentUserId, data); + if (setData.Status.Error) + { + Logger.Error($"Error when trying to store user activity timestamp: {setData.Status.ErrorData}"); + } + await Task.Delay(Config.StoreUserActivityInterval); + } + } + public async Task> GetCurrentUserMentions(string startTimeToken, string endTimeToken, int count) { @@ -1178,6 +1220,7 @@ public async Task> MarkAllMessage 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))) @@ -1234,7 +1277,6 @@ public async Task>> GetUnreadMess var lastRead = string.IsNullOrEmpty(membership.LastReadMessageTimeToken) ? Membership.EMPTY_TIMETOKEN : long.Parse(membership.LastReadMessageTimeToken); timeTokens.Add(lastRead); } - //TODO: ISSUE: count also includes events var getCounts = await PubnubInstance.MessageCounts().Channels(channelIds.ToArray()).ChannelsTimetoken(timeTokens.ToArray()) .ExecuteAsync(); if (result.RegisterOperation(getCounts)) @@ -1459,8 +1501,12 @@ public async Task EmitEvent(PubnubChatEventType type, strin jsonPayload = jsonPayload.Remove(0, 1); jsonPayload = jsonPayload.Remove(jsonPayload.Length - 1); var fullPayload = $"{{{jsonPayload}, \"type\": \"{ChatEnumConverters.ChatEventTypeToString(type)}\"}}"; - result.RegisterOperation(await PubnubInstance.Publish().Channel(channelId).Message(fullPayload) - .ExecuteAsync()); + var emitOperation = PubnubInstance.Publish().Channel(channelId).Message(fullPayload); + if (type is PubnubChatEventType.Receipt or PubnubChatEventType.Typing) + { + emitOperation.ShouldStore(false); + } + result.RegisterOperation(await emitOperation.ExecuteAsync()); return result; } @@ -1468,6 +1514,7 @@ public async Task EmitEvent(PubnubChatEventType type, strin public void Destroy() { + storeActivity = false; PubnubInstance.Destroy(); RateLimiter.Dispose(); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs index abf76a3..b28068a 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ChatAccessManager.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; +using PubnubApi; using PubnubChatApi.Enums; namespace PubNubChatAPI.Entities @@ -16,7 +18,33 @@ internal ChatAccessManager(Chat chat) public async Task CanI(PubnubAccessPermission permission, PubnubAccessResourceType resourceType, string resourceName) { - throw new NotImplementedException(); + 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/Membership.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs index 067e2d4..5329627 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs @@ -40,7 +40,7 @@ public class Membership : UniqueChatEntity /// /// The string time token of last read message on the membership channel. /// - public string LastReadMessageTimeToken => MembershipData.CustomData.TryGetValue("lastReadMessageTimetoken", out var timeToken) ? timeToken.ToString() : ""; + public string LastReadMessageTimeToken => MembershipData.CustomData != null && MembershipData.CustomData.TryGetValue("lastReadMessageTimetoken", out var timeToken) ? timeToken.ToString() : ""; public ChatMembershipData MembershipData { get; private set; } @@ -109,7 +109,7 @@ public async Task Update(ChatMembershipData membershipData) internal async Task> UpdateMembershipData(ChatMembershipData membershipData) { - return await chat.PubnubInstance.SetMemberships().Uuid(UserId).Channels(new List() + return await chat.PubnubInstance.ManageMemberships().Uuid(UserId).Set(new List() { new() { @@ -126,7 +126,7 @@ internal async Task> UpdateMembershipData(ChatMemb PNMembershipField.CHANNEL, PNMembershipField.CHANNEL_CUSTOM, PNMembershipField.CHANNEL_TYPE, - PNMembershipField.CHANNEL_STATUS + PNMembershipField.CHANNEL_STATUS, }).ExecuteAsync(); } @@ -159,6 +159,7 @@ public async Task SetLastReadMessage(Message message) public async Task SetLastReadMessageTimeToken(string timeToken) { var result = new ChatOperationResult(); + MembershipData.CustomData ??= new Dictionary(); MembershipData.CustomData["lastReadMessageTimetoken"] = timeToken; var update = await UpdateMembershipData(MembershipData); if (result.RegisterOperation(update)) diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs index 0fb7599..4a07b3e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -253,13 +253,35 @@ public async Task EditMessageText(string newText) } result.RegisterOperation(await chat.PubnubInstance.AddMessageAction() .Action(new PNMessageAction() { Type = "edited", Value = newText }) + .Channel(ChannelId) .MessageTimetoken(long.Parse(TimeToken)).Channel(ChannelId).ExecuteAsync()); return result; } - public bool TryGetQuotedMessage(out Message quotedMessage) + public async Task> GetQuotedMessage() { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + 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()); + if (result.RegisterOperation(getMessage)) + { + return result; + } + result.Result = getMessage.Result; + return result; } public bool HasThread() @@ -378,17 +400,62 @@ public async Task Forward(string channelId) public bool HasUserReaction(string reactionValue) { - throw new NotImplementedException(); + return Reactions.Any(x => x.Value == reactionValue); } - public async Task ToggleReaction(string reactionValue) + public async Task ToggleReaction(string reactionValue) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + 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(); + 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(); + 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 async Task Restore() + public async Task Restore() { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + 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(); + result.RegisterOperation(restore); + MessageActions.RemoveAt(MessageActions.IndexOf(deleteAction)); + return result; } /// @@ -408,14 +475,63 @@ public async Task Restore() /// /// /// - public async Task Delete(bool soft) + public async Task Delete(bool soft) { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + if (soft) + { + var add = await chat.PubnubInstance.AddMessageAction() + .MessageTimetoken(long.Parse(TimeToken)).Action(new PNMessageAction() + { + Type = "deleted", + Value = "deleted" + }).Channel(ChannelId).ExecuteAsync(); + 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(); + if (result.RegisterOperation(getThread)) + { + return result; + } + var deleteThread = await getThread.Result.Delete(); + 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(); + result.RegisterOperation(deleteMessage); + } + return result; } - public override Task Refresh() + public override async Task Refresh() { - throw new NotImplementedException(); + var result = new ChatOperationResult(); + var get = await chat.GetMessage(ChannelId, TimeToken); + 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/ThreadChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs index dbd151d..937ecb6 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -18,6 +18,8 @@ internal ThreadChannel(Chat chat, string channelId, string parentChannelId, stri { ParentChannelId = parentChannelId; ParentMessageTimeToken = parentMessageTimeToken; + data.CustomData["parentChannelId"] = ParentChannelId; + data.CustomData["parentMessageTimetoken"] = ParentMessageTimeToken; } private async Task InitThreadChannel() diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index 961e68e..e3474a2 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -84,7 +84,14 @@ public bool Active { get { - throw new NotImplementedException(); + 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; } } @@ -92,7 +99,11 @@ public string LastActiveTimeStamp { get { - throw new NotImplementedException(); + if (CustomData == null || !CustomData.TryGetValue("lastActiveTimestamp", out var lastActiveTimestamp)) + { + return string.Empty; + } + return lastActiveTimestamp.ToString(); } } @@ -144,7 +155,7 @@ protected override SubscribeCallback CreateUpdateListener() public void SetListeningForMentionEvents(bool listen) { - SetListening(mentionsSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + 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)) @@ -157,7 +168,7 @@ public void SetListeningForMentionEvents(bool listen) public void SetListeningForInviteEvents(bool listen) { - SetListening(invitesSubscription, SubscriptionOptions.None, listen, Id, chat.ListenerFactory.ProduceListener(messageCallback: + 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)) @@ -170,7 +181,7 @@ public void SetListeningForInviteEvents(bool listen) public void SetListeningForModerationEvents(bool listen) { - SetListening(moderationSubscription, SubscriptionOptions.None, listen, Chat.INTERNAL_MODERATION_PREFIX+Id, chat.ListenerFactory.ProduceListener(messageCallback: + 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)) @@ -208,7 +219,7 @@ public async Task Update(ChatUserData updatedData) internal static async Task> UpdateUserData(Chat chat, string userId, ChatUserData chatUserData) { - var operation = chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true).Uuid(userId); + var operation = chat.PubnubInstance.SetUuidMetadata().IncludeCustom(true).IncludeStatus(true).IncludeType(true).Uuid(userId); if (!string.IsNullOrEmpty(chatUserData.Username)) { operation = operation.Name(chatUserData.Username); @@ -233,7 +244,7 @@ internal static async Task> UpdateUserData(Cha { operation = operation.Status(chatUserData.Status); } - if (chatUserData.CustomData.Any()) + if (chatUserData.CustomData != null) { operation = operation.Custom(chatUserData.CustomData); } @@ -463,7 +474,18 @@ public async Task IsPresentOn(string channelId) /// public async Task>> WherePresent() { - throw new NotImplementedException(); + var result = new ChatOperationResult>(); + var where = await chat.PubnubInstance.WhereNow().Uuid(Id).ExecuteAsync(); + if (result.RegisterOperation(where)) + { + return result; + } + result.Result = new List(); + if (where.Result != null) + { + result.Result.AddRange(where.Result.Channels); + } + return result; } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj b/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj index 5a10b29..86d783e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/PubnubChatApi.csproj @@ -14,7 +14,7 @@ - + diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs index 79d63d7..5f6a9cf 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs @@ -59,37 +59,25 @@ internal static bool TryParseMessageFromHistory(Chat chat, string channelId, PNH var type = PubnubChatMessageType.Text; var text = messageDict["text"].ToString(); - //TODO: C# FIX, Meta shouldn't be an object but a deserialized type - var meta = chat.PubnubInstance.JsonPluggableLibrary.ConvertToDictionaryObject(historyItem.Meta) ?? new Dictionary(); - - //TODO: C# FIX, Actions shouldn't be an object but a deserialized type var actions = new List(); - if (historyItem.Actions is Dictionary actionsDict) + if (historyItem.ActionItems != null) { - foreach (var kvp in actionsDict) + foreach (var kvp in historyItem.ActionItems) { - var actionType = kvp.Key; - var entries = kvp.Value as Dictionary; - foreach (var entryPair in entries) + var actionType = ChatEnumConverters.StringToActionType(kvp.Key); + foreach (var actionEntry in kvp.Value) { - var actionValue = entryPair.Key; - var actionEntries = entryPair.Value as List; - foreach (var entry in actionEntries) + actions.Add(new MessageAction() { - var entryDict = entry as Dictionary; - actions.Add(new MessageAction() - { - TimeToken = entryDict["actionTimetoken"].ToString(), - UserId = entryDict["uuid"].ToString(), - Type = ChatEnumConverters.StringToActionType(actionType), - Value = actionValue - }); - } + 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, meta, actions); + message = new Message(chat, historyItem.Timetoken.ToString(), text, channelId, historyItem.Uuid, type, historyItem.Meta, actions); return true; } catch (Exception e) @@ -104,8 +92,8 @@ internal static bool TryParseMembershipUpdate(Chat chat, Membership membership, { try { - var channel = objectEvent.ChannelMetadata.Channel; - var user = objectEvent.UuidMetadata.Uuid; + var channel = objectEvent.MembershipMetadata.Channel; + var user = objectEvent.MembershipMetadata.Uuid; var type = objectEvent.Type; if (type == "membership" && channel == membership.ChannelId && user == membership.UserId) { diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs index 4730a73..8387199 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs @@ -13,6 +13,13 @@ internal static string TimeTokenNow() 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) { From 221dee0bac89ee07accdfee6154d758064c42553 Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Thu, 11 Sep 2025 15:18:13 +0200 Subject: [PATCH 19/28] add .ConfigureAwait(false) to await statements --- .../PubnubChatApi/Entities/Channel.cs | 66 ++++----- .../PubnubChatApi/Entities/Chat.cs | 128 +++++++++--------- .../Entities/Data/ChatOperationResult.cs | 36 ++++- .../PubnubChatApi/Entities/Membership.cs | 18 +-- .../PubnubChatApi/Entities/Message.cs | 56 ++++---- .../PubnubChatApi/Entities/MessageDraft.cs | 14 +- .../PubnubChatApi/Entities/ThreadChannel.cs | 22 +-- .../PubnubChatApi/Entities/ThreadMessage.cs | 4 +- .../PubnubChatApi/Entities/User.cs | 34 ++--- .../PubnubChatApi/Utilities/ChatUtils.cs | 5 +- .../Utilities/ExponentialRateLimiter.cs | 10 +- 11 files changed, 211 insertions(+), 182 deletions(-) diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index 12a584e..b00d187 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -199,20 +199,20 @@ internal static async Task> UpdateChannelDa { operation = operation.Type(data.Type); } - return await operation.ExecuteAsync(); + 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(); + .ExecuteAsync().ConfigureAwait(false); } public override async Task Refresh() { - var result = new ChatOperationResult(); - var getData = await GetChannelData(chat, Id); + var result = new ChatOperationResult("Channel.Refresh()", chat); + var getData = await GetChannelData(chat, Id).ConfigureAwait(false); if (result.RegisterOperation(getData)) { return result; @@ -254,7 +254,7 @@ async delegate(Pubnub pn, PNMessageResult m) { if (ChatParsers.TryParseEvent(chat, m, PubnubChatEventType.Receipt, out var readEvent)) { - var getMembers = await chat.GetChannelMemberships(Id); + var getMembers = await chat.GetChannelMemberships(Id).ConfigureAwait(false); if (getMembers.Error) { return; @@ -333,7 +333,7 @@ public void SetListeningForPresence(bool listen) SetListening(ref presenceEventsSubscription, SubscriptionOptions.ReceivePresenceEvents, listen, Id, chat.ListenerFactory.ProduceListener(presenceCallback: async delegate(Pubnub pn, PNPresenceEventResult p) { - var whoIs = await WhoIsPresent(); + var whoIs = await WhoIsPresent().ConfigureAwait(false); if (whoIs.Error) { chat.Logger.Error($"Error when trying to broadcast presence update after WhoIs(): {whoIs.Exception.Message}"); @@ -350,7 +350,7 @@ public async Task ForwardMessage(Message message) return await SendText(message.MessageText, new SendTextParams() { Meta = message.Meta - }); + }).ConfigureAwait(false); } public virtual async Task EmitUserMention(string userId, string timeToken, string text) @@ -362,17 +362,17 @@ public virtual async Task EmitUserMention(string userId, st {"channel",Id} }; return await chat.EmitEvent(PubnubChatEventType.Mention, userId, - chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)); + chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)).ConfigureAwait(false); } public async Task StartTyping() { - return await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":true}}"); + return await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":true}}").ConfigureAwait(false); } public async Task StopTyping() { - return await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":false}}"); + return await chat.EmitEvent(PubnubChatEventType.Typing, Id, $"{{\"value\":false}}").ConfigureAwait(false); } public async Task PinMessage(Message message) @@ -380,7 +380,7 @@ 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)).ToChatOperationResult(); + return (await UpdateChannelData(chat, Id, channelData).ConfigureAwait(false)).ToChatOperationResult("Channel.PinMessage()", chat); } public async Task UnpinMessage() @@ -388,7 +388,7 @@ public async Task UnpinMessage() channelData.CustomData ??= new (); channelData.CustomData.Remove("pinnedMessageChannelID"); channelData.CustomData.Remove("pinnedMessageTimetoken"); - return (await UpdateChannelData(chat, Id, channelData)).ToChatOperationResult(); + return (await UpdateChannelData(chat, Id, channelData).ConfigureAwait(false)).ToChatOperationResult("Channel.UnPinMessage()", chat); } @@ -398,8 +398,8 @@ public async Task UnpinMessage() /// The pinned Message object if there was one, null otherwise. public async Task> GetPinnedMessage() { - var result = new ChatOperationResult(); - if (result.RegisterOperation(await Refresh())) + var result = new ChatOperationResult("Channel.GetPinnedMessage()", chat); + if (result.RegisterOperation(await Refresh().ConfigureAwait(false))) { return result; } @@ -411,7 +411,7 @@ public async Task> GetPinnedMessage() return result; } - var getMessage = await chat.GetMessage(pinnedChannelId.ToString(), pinnedMessageTimeToken.ToString()); + var getMessage = await chat.GetMessage(pinnedChannelId.ToString(), pinnedMessageTimeToken.ToString()).ConfigureAwait(false); if (result.RegisterOperation(getMessage)) { return result; @@ -491,7 +491,7 @@ public async void Leave() PNMembershipField.CHANNEL_TYPE, PNMembershipField.CHANNEL_STATUS }).Channels(new List() { Id }) - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); if (remove.Status.Error) { chat.Logger.Error($"Error when trying to leave channel \"{Id}\": {remove.Status.ErrorData.Information}"); @@ -573,14 +573,14 @@ public async void Join(ChatMembershipData? membershipData = null) PNMembershipField.STATUS, PNMembershipField.CHANNEL, PNMembershipField.CHANNEL_CUSTOM - }).ExecuteAsync(); + }).ExecuteAsync().ConfigureAwait(false); if (response.Status.Error) { chat.Logger.Error($"Error when trying to Join() to channel \"{Id}\": {response.Status.ErrorData.Information}"); return; } var joinMembership = new Membership(chat, currentUserId, Id, membershipData); - await joinMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()); + await joinMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()).ConfigureAwait(false); Connect(); } @@ -605,12 +605,12 @@ public async void Join(ChatMembershipData? membershipData = null) /// public async Task SetRestrictions(string userId, bool banUser, bool muteUser, string reason) { - return await chat.SetRestriction(userId, Id, banUser, muteUser, reason); + return await chat.SetRestriction(userId, Id, banUser, muteUser, reason).ConfigureAwait(false); } public async Task SetRestrictions(string userId, Restriction restriction) { - return await SetRestrictions(userId, restriction.Ban, restriction.Mute, restriction.Reason); + return await SetRestrictions(userId, restriction.Ban, restriction.Mute, restriction.Reason).ConfigureAwait(false); } /// @@ -630,12 +630,12 @@ public async Task SetRestrictions(string userId, Restrictio /// public async Task SendText(string message) { - return await SendText(message, new SendTextParams()); + return await SendText(message, new SendTextParams()).ConfigureAwait(false); } public virtual async Task SendText(string message, SendTextParams sendTextParams) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Channel.SendText()", chat); var baseInterval = Type switch { @@ -677,7 +677,7 @@ public virtual async Task SendText(string message, SendText .UsePOST(sendTextParams.SendByPost) .Message(chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(messageDict)) .Meta(meta) - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(publishResult)) { return result; @@ -685,7 +685,7 @@ public virtual async Task SendText(string message, SendText foreach (var mention in sendTextParams.MentionedUsers) { result.RegisterOperation(await EmitUserMention(mention.Value.Id, - publishResult.Result.Timetoken.ToString(), message)); + publishResult.Result.Timetoken.ToString(), message).ConfigureAwait(false)); } return result; }, response => @@ -701,7 +701,7 @@ public virtual async Task SendText(string message, SendText completionSource.SetResult(true); }); - await completionSource.Task; + await completionSource.Task.ConfigureAwait(false); return result; } @@ -729,7 +729,7 @@ public virtual async Task SendText(string message, SendText /// public async Task Update(ChatChannelData updatedData) { - return await chat.UpdateChannel(Id, updatedData); + return await chat.UpdateChannel(Id, updatedData).ConfigureAwait(false); } /// @@ -774,11 +774,11 @@ public async Task Delete() /// public async Task> GetUserRestrictions(User user) { - var result = new ChatOperationResult(); + 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(); + }).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; @@ -805,7 +805,7 @@ public async Task> GetUserRestrictions(User use public async Task> GetUsersRestrictions(string sort = "", int limit = 0, PNPageObject page = null) { - var result = new ChatOperationResult(){Result = new UsersRestrictionsWrapper()}; + var result = new ChatOperationResult("Channel.GetUsersRestrictions()", chat){Result = new UsersRestrictionsWrapper()}; var operation = chat.PubnubInstance.GetChannelMembers().Channel($"{Chat.INTERNAL_MODERATION_PREFIX}_{Id}") .Include(new[] { @@ -824,7 +824,7 @@ public async Task> GetUsersRestric { operation = operation.Page(page); } - var membersResult = await operation.ExecuteAsync(); + var membersResult = await operation.ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(membersResult)) { return result; @@ -870,7 +870,7 @@ public async Task> GetUsersRestric /// public async Task> IsUserPresent(string userId) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Channel.IsUserPresent()", chat); var wherePresent = await chat.WherePresent(userId); if (result.RegisterOperation(wherePresent)) { @@ -899,9 +899,9 @@ public async Task> IsUserPresent(string userId) /// public async Task>> WhoIsPresent() { - var result = new ChatOperationResult>() { Result = new List() }; + var result = new ChatOperationResult>("Channel.WhoIsPresent()", chat) { Result = new List() }; var response = await chat.PubnubInstance.HereNow().Channels(new[] { Id }).IncludeState(true) - .IncludeUUIDs(true).ExecuteAsync(); + .IncludeUUIDs(true).ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(response)) { return result; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 586353e..6d780f2 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -59,11 +59,11 @@ public static async Task> CreateInstance(PubnubChatCon { chat.StoreActivityTimeStamp(); } - var result = new ChatOperationResult(){Result = chat}; - var getUser = await chat.GetCurrentUser(); + 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())); + result.RegisterOperation(await chat.CreateUser(chat.PubnubInstance.GetCurrentUserId()).ConfigureAwait(false)); } return result; } @@ -92,7 +92,7 @@ public async Task AddListenerToChannelsUpdate(List channelIds, Action> CreatePublicConversation(string channelId = Guid.NewGuid().ToString(); } - return await CreatePublicConversation(channelId, new ChatChannelData()); + return await CreatePublicConversation(channelId, new ChatChannelData()).ConfigureAwait(false); } /// @@ -152,9 +152,9 @@ public async Task> CreatePublicConversation(string /// public async Task> CreatePublicConversation(string channelId, ChatChannelData additionalData) { - var result = new ChatOperationResult(); - var existingChannel = await GetChannel(channelId); - if (!result.RegisterOperation(existingChannel)) + var result = new ChatOperationResult("Chat.CreatePublicConversation()", this); + var existingChannel = await GetChannel(channelId).ConfigureAwait(false); + if (!result.RegisterOperation(existingChannel, false)) { Logger.Debug("Trying to create a channel with ID that already exists! Returning existing one."); result.Result = existingChannel.Result; @@ -162,7 +162,7 @@ public async Task> CreatePublicConversation(string } additionalData.Type = "public"; - var updated = await Channel.UpdateChannelData(this, channelId, additionalData); + var updated = await Channel.UpdateChannelData(this, channelId, additionalData).ConfigureAwait(false); if (result.RegisterOperation(updated)) { return result; @@ -179,15 +179,15 @@ private async Task> CreateConversatio ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) { - var result = new ChatOperationResult(){Result = new CreatedChannelWrapper()}; + var result = new ChatOperationResult($"Chat.CreateConversation-{type}", this){Result = new CreatedChannelWrapper()}; if (string.IsNullOrEmpty(channelId)) { channelId = Guid.NewGuid().ToString(); } - var existingChannel = await GetChannel(channelId); - if (!result.RegisterOperation(existingChannel)) + var existingChannel = await GetChannel(channelId).ConfigureAwait(false); + if (!result.RegisterOperation(existingChannel, false)) { Logger.Debug("Trying to create a channel with ID that already exists! Returning existing one."); result.Result.CreatedChannel = existingChannel.Result; @@ -196,7 +196,7 @@ private async Task> CreateConversatio channelData ??= new ChatChannelData(); channelData.Type = type; - var updated = await Channel.UpdateChannelData(this, channelId, channelData); + var updated = await Channel.UpdateChannelData(this, channelId, channelData).ConfigureAwait(false); if (result.RegisterOperation(updated)) { return result; @@ -222,7 +222,7 @@ private async Task> CreateConversatio Status = membershipData.Status, Type = membershipData.Type }}) - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(setMembershipResult)) { @@ -237,7 +237,7 @@ private async Task> CreateConversatio if (type == "direct") { - var inviteMembership = await InviteToChannel(channelId, users[0].Id); + var inviteMembership = await InviteToChannel(channelId, users[0].Id).ConfigureAwait(false); if (result.RegisterOperation(inviteMembership)) { return result; @@ -245,7 +245,7 @@ private async Task> CreateConversatio result.Result.InviteesMemberships = new List() { inviteMembership.Result }; }else if (type == "group") { - var inviteMembership = await InviteMultipleToChannel(channelId, users); + var inviteMembership = await InviteMultipleToChannel(channelId, users).ConfigureAwait(false); if (result.RegisterOperation(inviteMembership)) { return result; @@ -259,29 +259,29 @@ public async Task> CreateDirectConver ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) { return await CreateConversation("direct", new List() { user }, channelId, channelData, - membershipData); + membershipData).ConfigureAwait(false); } public async Task> CreateGroupConversation(List users, string channelId = "", ChatChannelData? channelData = null, ChatMembershipData? membershipData = null) { return await CreateConversation("group", users, channelId, channelData, - membershipData); + membershipData).ConfigureAwait(false); } public async Task> InviteToChannel(string channelId, string userId) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.InviteToChannel()", this); //Check if already a member first - var members = await GetChannelMemberships(channelId, filter:$"uuid.id == \"{userId}\""); - if (!result.RegisterOperation(members) && members.Result.Memberships.Any()) + var members = await GetChannelMemberships(channelId, filter:$"uuid.id == \"{userId}\"").ConfigureAwait(false); + if (!result.RegisterOperation(members, false) && members.Result.Memberships.Any()) { //Already a member, just return current membership result.Result = members.Result.Memberships[0]; return result; } - var channel = await GetChannel(channelId); + var channel = await GetChannel(channelId).ConfigureAwait(false); if (result.RegisterOperation(channel)) { return result; @@ -305,7 +305,7 @@ public async Task> InviteToChannel(string channe Status = , Type = */ } - }).ExecuteAsync(); + }).ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(setMemberships)) { @@ -331,7 +331,7 @@ public async Task> InviteToChannel(string channe public async Task>> InviteMultipleToChannel(string channelId, List users) { - var result = new ChatOperationResult>() { Result = new List() }; + var result = new ChatOperationResult>("Chat.InviteMultipleToChannel()", this) { Result = new List() }; var channel = await GetChannel(channelId); if (result.RegisterOperation(channel)) { @@ -350,7 +350,7 @@ public async Task>> InviteMultipleToChannel }) //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(); + .ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(inviteResponse)) { @@ -384,7 +384,7 @@ public async Task>> InviteMultipleToChannel /// Channel object if it exists, null otherwise. public async Task> GetChannel(string channelId) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.GetChannel()", this); var getResult = await Channel.GetChannelData(this, channelId); if (result.RegisterOperation(getResult)) { @@ -423,7 +423,7 @@ public async Task GetChannels(string filter = "", strin { operation = operation.Page(page); } - var response = await operation.ExecuteAsync(); + var response = await operation.ExecuteAsync().ConfigureAwait(false); if (response.Status.Error) { @@ -464,7 +464,7 @@ public async Task GetChannels(string filter = "", strin /// public async Task UpdateChannel(string channelId, ChatChannelData updatedData) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.UpdateChannel()", this); result.RegisterOperation(await Channel.UpdateChannelData(this, channelId, updatedData)); return result; } @@ -484,8 +484,8 @@ public async Task UpdateChannel(string channelId, ChatChann /// public async Task DeleteChannel(string channelId) { - var result = new ChatOperationResult(); - result.RegisterOperation(await PubnubInstance.RemoveChannelMetadata().Channel(channelId).ExecuteAsync()); + var result = new ChatOperationResult("Chat.DeleteChannel()", this); + result.RegisterOperation(await PubnubInstance.RemoveChannelMetadata().Channel(channelId).ExecuteAsync().ConfigureAwait(false)); return result; } @@ -520,7 +520,7 @@ internal async void StoreActivityTimeStamp() public async Task> GetCurrentUserMentions(string startTimeToken, string endTimeToken, int count) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.GetCurrentUserMentions()", this); var id = PubnubInstance.GetCurrentUserId(); var getEventHistory = await GetEventsHistory(id, startTimeToken, endTimeToken, count); if (result.RegisterOperation(getEventHistory)) @@ -573,7 +573,7 @@ public async Task> GetCurrentUserMentio /// User object if there is a current user, null otherwise. public async Task> GetCurrentUser() { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.GetCurrentUser()", this); var userId = PubnubInstance.GetCurrentUserId(); var getUser = await GetUser(userId); if (result.RegisterOperation(getUser)) @@ -602,7 +602,7 @@ public async Task> GetCurrentUser() /// public async Task SetRestriction(string userId, string channelId, bool banUser, bool muteUser, string reason) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.SetRestriction()", this); var restrictionsChannelId = $"{INTERNAL_MODERATION_PREFIX}_{channelId}"; var getResult = await Channel.GetChannelData(this, restrictionsChannelId); if (result.RegisterOperation(getResult)) @@ -618,7 +618,7 @@ public async Task SetRestriction(string userId, string chan if (!banUser && !muteUser) { if (result.RegisterOperation(await PubnubInstance.RemoveChannelMembers().Channel(restrictionsChannelId) - .Uuids(new List() { userId }).ExecuteAsync())) + .Uuids(new List() { userId }).ExecuteAsync().ConfigureAwait(false))) { return result; } @@ -643,7 +643,7 @@ public async Task SetRestriction(string userId, string chan { PNChannelMemberField.UUID, PNChannelMemberField.CUSTOM - }).ExecuteAsync())) + }).ExecuteAsync().ConfigureAwait(false))) { return result; } @@ -724,9 +724,9 @@ public async Task> CreateUser(string userId) /// public async Task> CreateUser(string userId, ChatUserData additionalData) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.CreateUser()", this); var existingUser = await GetUser(userId); - if (!result.RegisterOperation(existingUser)) + if (!result.RegisterOperation(existingUser, false)) { result.Result = existingUser.Result; return result; @@ -763,7 +763,7 @@ public async Task> CreateUser(string userId, ChatUserD /// public async Task> IsPresent(string userId, string channelId) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.IsPresent()", this); var getChannel = await GetChannel(channelId); if (result.RegisterOperation(getChannel)) { @@ -799,7 +799,7 @@ public async Task> IsPresent(string userId, string cha /// public async Task>> WhoIsPresent(string channelId) { - var result = new ChatOperationResult>() { Result = new List() }; + var result = new ChatOperationResult>("Chat.WhoIsPresent()", this) { Result = new List() }; var getChannel = await GetChannel(channelId); if (result.RegisterOperation(getChannel)) { @@ -835,7 +835,7 @@ public async Task>> WhoIsPresent(string channel /// public async Task>> WherePresent(string userId) { - var result = new ChatOperationResult>() { Result = new List() }; + var result = new ChatOperationResult>("Chat.WherePresent()", this) { Result = new List() }; var getUser = await GetUser(userId); if (result.RegisterOperation(getUser)) { @@ -857,7 +857,7 @@ public async Task>> WherePresent(string userId) /// User object if one with given ID is found, null otherwise. public async Task> GetUser(string userId) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.GetUser()", this); var getData = await User.GetUserData(this, userId); if (result.RegisterOperation(getData)) { @@ -914,7 +914,7 @@ public async Task GetUsers(string filter = "", string sort { operation = operation.Page(page); } - var result = await operation.ExecuteAsync(); + var result = await operation.ExecuteAsync().ConfigureAwait(false); if (result.Status.Error) { @@ -974,8 +974,8 @@ public async Task UpdateUser(string userId, ChatUserData updatedData) /// public async Task DeleteUser(string userId) { - var result = new ChatOperationResult(); - result.RegisterOperation(await PubnubInstance.RemoveUuidMetadata().Uuid(userId).ExecuteAsync()); + var result = new ChatOperationResult("Chat.DeleteUser()", this); + result.RegisterOperation(await PubnubInstance.RemoveUuidMetadata().Uuid(userId).ExecuteAsync().ConfigureAwait(false)); return result; } @@ -1014,7 +1014,7 @@ public async Task> GetUserMembership string sort = "", int limit = 0, PNPageObject page = null) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.GetUserMemberships()", this); var operation = PubnubInstance.GetMemberships().Include( new[] { @@ -1041,7 +1041,7 @@ public async Task> GetUserMembership { operation.Page(page); } - var getMemberships = await operation.ExecuteAsync(); + var getMemberships = await operation.ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(getMemberships)) { return result; @@ -1097,7 +1097,7 @@ public async Task> GetChannelMembers string sort = "", int limit = 0, PNPageObject page = null) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.GetChannelMemberships()", this); var operation = PubnubInstance.GetChannelMembers().Include( new[] { @@ -1125,7 +1125,7 @@ public async Task> GetChannelMembers operation = operation.Page(page); } - var getResult = await operation.ExecuteAsync(); + var getResult = await operation.ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(getResult)) { return result; @@ -1169,7 +1169,7 @@ public async Task> GetMessageReportsHi /// Message object if one was found, null otherwise. public async Task> GetMessage(string channelId, string messageTimeToken) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.GetMessage()", this); var startTimeToken = (long.Parse(messageTimeToken) + 1).ToString(); var getHistory = await GetChannelMessageHistory(channelId, startTimeToken, messageTimeToken, 1); if (result.RegisterOperation(getHistory)) @@ -1190,7 +1190,7 @@ public async Task> MarkAllMessage int limit = 0, PNPageObject page = null) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.MarkAllMessagesAsRead()", this); if (limit < 0 || limit > 100) { result.Error = true; @@ -1246,7 +1246,7 @@ public async Task>> GetUnreadMess int limit = 0, PNPageObject page = null) { - var result = new ChatOperationResult>(); + var result = new ChatOperationResult>("Chat.GetUnreadMessagesCounts()", this); if (limit < 0 || limit > 100) { result.Error = true; @@ -1278,7 +1278,7 @@ public async Task>> GetUnreadMess timeTokens.Add(lastRead); } var getCounts = await PubnubInstance.MessageCounts().Channels(channelIds.ToArray()).ChannelsTimetoken(timeTokens.ToArray()) - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(getCounts)) { return result; @@ -1299,7 +1299,7 @@ public async Task>> GetUnreadMess public async Task> CreateThreadChannel(string messageTimeToken, string messageChannelId) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.CreateThreadChannel()", this); var getMessage = await GetMessage(messageChannelId, messageTimeToken); if (result.RegisterOperation(getMessage)) { @@ -1316,7 +1316,7 @@ public async Task> CreateThreadChannel(string public async Task RemoveThreadChannel(string messageTimeToken, string messageChannelId) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.RemoveThreadChannel()", this); var getMessage = await GetMessage(messageChannelId, messageTimeToken); if (result.RegisterOperation(getMessage)) { @@ -1333,7 +1333,7 @@ public async Task RemoveThreadChannel(string messageTimeTok /// The ThreadChannel object if one was found, null otherwise. public async Task> GetThreadChannel(Message message) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.GetThreadChannel()", this); var getChannel = await GetChannel(message.GetThreadId()); if (result.RegisterOperation(getChannel)) { @@ -1351,7 +1351,7 @@ public async Task> GetThreadChannel(Message m public async Task ForwardMessage(string messageTimeToken, string channelId) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.ForwardMessage()", this); var getMessage = await GetMessage(channelId, messageTimeToken); if (result.RegisterOperation(getMessage)) { @@ -1376,7 +1376,7 @@ public async void AddListenerToMessagesUpdate(string channelId, List mes public async Task PinMessageToChannel(string channelId, Message message) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.PinMessageToChannel()", this); var getChannel = await GetChannel(channelId); if (result.RegisterOperation(getChannel)) { @@ -1389,7 +1389,7 @@ public async Task PinMessageToChannel(string channelId, Mes public async Task UnpinMessageFromChannel(string channelId) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Chat.UnPinMessageFromChannel()", this); var getChannel = await GetChannel(channelId); if (result.RegisterOperation(getChannel)) { @@ -1426,13 +1426,13 @@ public async Task>> GetChannelMessageHistory(s string endTimeToken, int count) { - var result = new ChatOperationResult>() + 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(); + .IncludeMeta(true).ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(getHistory) || getHistory.Result.Messages == null || !getHistory.Result.Messages.ContainsKey(channelId)) { return result; @@ -1463,7 +1463,7 @@ public async Task> GetEventsHistory(st string endTimeToken, int count) { - var result = new ChatOperationResult() + var result = new ChatOperationResult("Chat.GetEventsHistory()", this) { Result = new EventsHistoryWrapper() { @@ -1472,7 +1472,7 @@ public async Task> GetEventsHistory(st }; var getHistory = await PubnubInstance.FetchHistory().Channels(new[] { channelId }) .Start(long.Parse(startTimeToken)).End(long.Parse(endTimeToken)).MaximumPerChannel(count) - .ExecuteAsync(); + .ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(getHistory) || !getHistory.Result.Messages.ContainsKey(channelId)) { return result; @@ -1497,7 +1497,7 @@ public async Task> GetEventsHistory(st public async Task EmitEvent(PubnubChatEventType type, string channelId, string jsonPayload) { - var result = new ChatOperationResult(); + 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)}\"}}"; @@ -1506,7 +1506,7 @@ public async Task EmitEvent(PubnubChatEventType type, strin { emitOperation.ShouldStore(false); } - result.RegisterOperation(await emitOperation.ExecuteAsync()); + result.RegisterOperation(await emitOperation.ExecuteAsync().ConfigureAwait(false)); return result; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatOperationResult.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatOperationResult.cs index a1fc93c..9d3e226 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatOperationResult.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatOperationResult.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using PubnubApi; +using PubNubChatAPI.Entities; namespace PubnubChatApi.Entities.Data { @@ -9,29 +10,52 @@ 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) + internal bool RegisterOperation(PNResult pubnubResult, bool logIfError = true) { 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) { + if (logIfError) + { + chat.Logger.Error($"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) + internal bool RegisterOperation(ChatOperationResult otherChatResult, bool logIfError = true) { - InternalStatuses.AddRange(otherChatResult.InternalStatuses); + 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 && logIfError) + { + chat.Logger.Error($"Chat operation \"{OperationName}\" registered PN Status from operation \"{otherChatResult.OperationName}\" with error: {otherChatResult.Exception.Message}"); + } Exception = otherChatResult.Exception; Error = otherChatResult.Error; return Error; @@ -40,6 +64,10 @@ internal bool RegisterOperation(ChatOperationResult otherChatResult) 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/Membership.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs index 5329627..169aacb 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs @@ -99,7 +99,7 @@ protected override SubscribeCallback CreateUpdateListener() /// public async Task Update(ChatMembershipData membershipData) { - var result = (await UpdateMembershipData(membershipData)).ToChatOperationResult(); + var result = (await UpdateMembershipData(membershipData).ConfigureAwait(false)).ToChatOperationResult("Membership.Update()", chat); if (!result.Error) { UpdateLocalData(membershipData); @@ -127,7 +127,7 @@ internal async Task> UpdateMembershipData(ChatMemb PNMembershipField.CHANNEL_CUSTOM, PNMembershipField.CHANNEL_TYPE, PNMembershipField.CHANNEL_STATUS, - }).ExecuteAsync(); + }).ExecuteAsync().ConfigureAwait(false); } internal static async Task> UpdateMembershipsData(Chat chat, string userId, List memberships) @@ -148,26 +148,26 @@ internal static async Task> UpdateMembershipsData( PNMembershipField.CHANNEL_CUSTOM, PNMembershipField.CHANNEL_TYPE, PNMembershipField.CHANNEL_STATUS - }).ExecuteAsync(); + }).ExecuteAsync().ConfigureAwait(false); } public async Task SetLastReadMessage(Message message) { - return await SetLastReadMessageTimeToken(message.TimeToken); + return await SetLastReadMessageTimeToken(message.TimeToken).ConfigureAwait(false); } public async Task SetLastReadMessageTimeToken(string timeToken) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Membership.SetLastReadMessageTimeToken()", chat); MembershipData.CustomData ??= new Dictionary(); MembershipData.CustomData["lastReadMessageTimetoken"] = timeToken; - var update = await UpdateMembershipData(MembershipData); + var update = await UpdateMembershipData(MembershipData).ConfigureAwait(false); if (result.RegisterOperation(update)) { return result; } result.RegisterOperation(await chat.EmitEvent(PubnubChatEventType.Receipt, ChannelId, - $"{{\"messageTimetoken\": \"{timeToken}\"}}")); + $"{{\"messageTimetoken\": \"{timeToken}\"}}").ConfigureAwait(false)); return result; } @@ -180,7 +180,7 @@ public async Task GetUnreadMessagesCount() } lastRead = lastRead == 0 ? EMPTY_TIMETOKEN : lastRead; var countsResponse = await chat.PubnubInstance.MessageCounts().Channels(new[] { ChannelId }) - .ChannelsTimetoken(new[] { lastRead }).ExecuteAsync(); + .ChannelsTimetoken(new[] { lastRead }).ExecuteAsync().ConfigureAwait(false); if (countsResponse.Status.Error) { chat.Logger.Error($"Error when trying to get message counts on channel \"{ChannelId}\": {countsResponse.Status.ErrorData}"); @@ -191,7 +191,7 @@ public async Task GetUnreadMessagesCount() public override async Task Refresh() { - return await chat.GetChannelMemberships(ChannelId, filter:$"uuid.id == \"{UserId}\""); + 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 4a07b3e..f9399c3 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -244,7 +244,7 @@ protected override SubscribeCallback CreateUpdateListener() /// public async Task EditMessageText(string newText) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Message.EditMessageText()", chat); if (string.IsNullOrEmpty(newText)) { result.Error = true; @@ -254,13 +254,13 @@ public async Task EditMessageText(string newText) result.RegisterOperation(await chat.PubnubInstance.AddMessageAction() .Action(new PNMessageAction() { Type = "edited", Value = newText }) .Channel(ChannelId) - .MessageTimetoken(long.Parse(TimeToken)).Channel(ChannelId).ExecuteAsync()); + .MessageTimetoken(long.Parse(TimeToken)).Channel(ChannelId).ExecuteAsync().ConfigureAwait(false)); return result; } public async Task> GetQuotedMessage() { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Message.GetQuotedMessage()", chat); if (!Meta.TryGetValue("quotedMessage", out var quotedMessage)) { result.Error = true; @@ -275,7 +275,7 @@ public async Task> GetQuotedMessage() result.Exception = new PNException("Quoted message data has incorrect format."); return result; } - var getMessage = await chat.GetMessage(channelId.ToString(), timetoken.ToString()); + var getMessage = await chat.GetMessage(channelId.ToString(), timetoken.ToString()).ConfigureAwait(false); if (result.RegisterOperation(getMessage)) { return result; @@ -296,7 +296,7 @@ internal string GetThreadId() public ChatOperationResult CreateThread() { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Message.CreateThread()", chat); if (ChannelId.Contains(Chat.MESSAGE_THREAD_ID_PREFIX)) { result.Error = true; @@ -331,12 +331,12 @@ public ChatOperationResult CreateThread() /// The retrieved ThreadChannel object, null if one wasn't found. public async Task> GetThread() { - return await chat.GetThreadChannel(this); + return await chat.GetThreadChannel(this).ConfigureAwait(false); } public async Task RemoveThread() { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Message.RemoveThread()", chat); if (!HasThread()) { result.Error = true; @@ -351,24 +351,24 @@ public async Task RemoveThread() } if (result.RegisterOperation(await chat.PubnubInstance.RemoveMessageAction().Channel(ChannelId) .MessageTimetoken(long.Parse(TimeToken)).ActionTimetoken(long.Parse(threadMessageAction.TimeToken)) - .ExecuteAsync())) + .ExecuteAsync().ConfigureAwait(false))) { return result; } MessageActions = MessageActions.Where(x => x.Type != PubnubMessageActionType.ThreadRootId).ToList(); - result.RegisterOperation(await getThread.Result.Delete()); + result.RegisterOperation(await getThread.Result.Delete().ConfigureAwait(false)); return result; } public async Task Pin() { - var result = new ChatOperationResult(); - var getChannel = await chat.GetChannel(ChannelId); + 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)); + result.RegisterOperation(await getChannel.Result.PinMessage(this).ConfigureAwait(false)); return result; } @@ -383,18 +383,18 @@ public async Task Report(string reason) {"reportedUserId",UserId} }; return await chat.EmitEvent(PubnubChatEventType.Report, $"{Chat.INTERNAL_MODERATION_PREFIX}_{ChannelId}", - chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)); + chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)).ConfigureAwait(false); } public async Task Forward(string channelId) { - var result = new ChatOperationResult(); - var getChannel = await chat.GetChannel(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)); + result.RegisterOperation(await getChannel.Result.ForwardMessage(this).ConfigureAwait(false)); return result; } @@ -405,7 +405,7 @@ public bool HasUserReaction(string reactionValue) public async Task ToggleReaction(string reactionValue) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Message.ToggleReaction()", chat); var currentUserId = chat.PubnubInstance.GetCurrentUserId(); for (var i = 0; i < MessageActions.Count; i++) { @@ -414,7 +414,7 @@ public async Task ToggleReaction(string reactionValue) { //Removing old one var remove = await chat.PubnubInstance.RemoveMessageAction().MessageTimetoken(long.Parse(TimeToken)) - .ActionTimetoken(long.Parse(reaction.TimeToken)).ExecuteAsync(); + .ActionTimetoken(long.Parse(reaction.TimeToken)).ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(remove)) { return result; @@ -426,7 +426,7 @@ public async Task ToggleReaction(string reactionValue) var add = await chat.PubnubInstance.AddMessageAction().Action(new PNMessageAction() { Type = "reaction", Value = reactionValue - }).MessageTimetoken(long.Parse(TimeToken)).Channel(ChannelId).ExecuteAsync(); + }).MessageTimetoken(long.Parse(TimeToken)).Channel(ChannelId).ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(add)) { return result; @@ -443,7 +443,7 @@ public async Task ToggleReaction(string reactionValue) public async Task Restore() { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Message.Restore()", chat); if (!IsDeleted) { result.Error = true; @@ -452,7 +452,7 @@ public async Task Restore() } 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(); + .ActionTimetoken(long.Parse(deleteAction.TimeToken)).Channel(ChannelId).ExecuteAsync().ConfigureAwait(false); result.RegisterOperation(restore); MessageActions.RemoveAt(MessageActions.IndexOf(deleteAction)); return result; @@ -477,7 +477,7 @@ public async Task Restore() /// public async Task Delete(bool soft) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("Message.Delete()", chat); if (soft) { var add = await chat.PubnubInstance.AddMessageAction() @@ -485,7 +485,7 @@ public async Task Delete(bool soft) { Type = "deleted", Value = "deleted" - }).Channel(ChannelId).ExecuteAsync(); + }).Channel(ChannelId).ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(add)) { return result; @@ -502,12 +502,12 @@ public async Task Delete(bool soft) { if (HasThread()) { - var getThread = await GetThread(); + var getThread = await GetThread().ConfigureAwait(false); if (result.RegisterOperation(getThread)) { return result; } - var deleteThread = await getThread.Result.Delete(); + var deleteThread = await getThread.Result.Delete().ConfigureAwait(false); if (result.RegisterOperation(deleteThread)) { return result; @@ -515,7 +515,7 @@ public async Task Delete(bool soft) } var startTimeToken = long.Parse(TimeToken) + 1; var deleteMessage = await chat.PubnubInstance.DeleteMessages().Start(startTimeToken) - .End(long.Parse(TimeToken)).ExecuteAsync(); + .End(long.Parse(TimeToken)).ExecuteAsync().ConfigureAwait(false); result.RegisterOperation(deleteMessage); } return result; @@ -523,8 +523,8 @@ public async Task Delete(bool soft) public override async Task Refresh() { - var result = new ChatOperationResult(); - var get = await chat.GetMessage(ChannelId, TimeToken); + var result = new ChatOperationResult("Message.Refresh()", chat); + var get = await chat.GetMessage(ChannelId, TimeToken).ConfigureAwait(false); if (result.RegisterOperation(get)) { return result; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs index 7336b2e..60f6698 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs @@ -132,7 +132,7 @@ private async void BroadcastDraftUpdate() try { var messageElements = GetMessageElements(); - var suggestedMentions = ShouldSearchForSuggestions ? await GenerateSuggestedMentions() : new List(); + var suggestedMentions = ShouldSearchForSuggestions ? await GenerateSuggestedMentions().ConfigureAwait(false) : new List(); OnDraftUpdated?.Invoke(messageElements, suggestedMentions); } catch (Exception e) @@ -160,14 +160,14 @@ private async Task> GenerateSuggestedMentions() { case MentionType.User: var usersWrapper = - await chat.GetUsers(filter: $"name LIKE \"{rawMention.Target.Target}*\"", limit:userLimit); + await chat.GetUsers(filter: $"name LIKE \"{rawMention.Target.Target}*\"", limit:userLimit).ConfigureAwait(false); if (usersWrapper.Users != null && usersWrapper.Users.Any()) { var user = usersWrapper.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)) + !await user.IsPresentOn(channel.Id).ConfigureAwait(false)) { continue; } @@ -179,7 +179,7 @@ private async Task> GenerateSuggestedMentions() break; case MentionType.Channel: var channelsWrapper = await chat.GetChannels(filter: $"name LIKE \"{rawMention.Target.Target}*\"", - limit: channelLimit); + limit: channelLimit).ConfigureAwait(false); if (channelsWrapper.Channels != null && channelsWrapper.Channels.Any()) { var mentionedChannel = channelsWrapper.Channels[0]; @@ -502,7 +502,7 @@ private void ApplyRemoveTextInternal(int offset, int length) /// public async Task Send() { - return await Send(new SendTextParams()); + return await Send(new SendTextParams()).ConfigureAwait(false); } /// @@ -556,7 +556,7 @@ public async Task Send(SendTextParams sendTextParams) } } sendTextParams.MentionedUsers = mentions; - return await channel.SendText(Render(), sendTextParams); + return await channel.SendText(Render(), sendTextParams).ConfigureAwait(false); } /// @@ -668,7 +668,7 @@ private async void TriggerTypingIndicator() { if (isTypingIndicatorTriggered && channel.Type == "public") { - await channel.StartTyping(); + await channel.StartTyping().ConfigureAwait(false); } } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs index 937ecb6..f4c0b67 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -24,24 +24,24 @@ internal ThreadChannel(Chat chat, string channelId, string parentChannelId, stri private async Task InitThreadChannel() { - var result = new ChatOperationResult(); - var channelUpdate = await UpdateChannelData(chat, Id, channelData); + 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()); + .MessageTimetoken(long.Parse(ParentMessageTimeToken)).ExecuteAsync().ConfigureAwait(false)); return result; } public override async Task SendText(string message, SendTextParams sendTextParams) { - var result = new ChatOperationResult(); + var result = new ChatOperationResult("ThreadChannel.SendText()", chat); if (!initialised) { - if (result.RegisterOperation(await InitThreadChannel())) + if (result.RegisterOperation(await InitThreadChannel().ConfigureAwait(false))) { return result; } @@ -49,17 +49,17 @@ public override async Task SendText(string message, SendTex initialised = true; } - return await base.SendText(message, sendTextParams); + return await base.SendText(message, sendTextParams).ConfigureAwait(false); } public async Task>> GetThreadHistory(string startTimeToken, string endTimeToken, int count) { - var result = new ChatOperationResult>() + var result = new ChatOperationResult>("ThreadChannel.GetThreadHistory()", chat) { Result = new List() }; - var getHistory = await GetMessageHistory(startTimeToken, endTimeToken, count); + var getHistory = await GetMessageHistory(startTimeToken, endTimeToken, count).ConfigureAwait(false); if (result.RegisterOperation(getHistory)) { return result; @@ -85,17 +85,17 @@ public override async Task EmitUserMention(string userId, s {"parentChannel", ParentChannelId} }; return await chat.EmitEvent(PubnubChatEventType.Mention, userId, - chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)); + chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(jsonDict)).ConfigureAwait(false); } public async Task PinMessageToParentChannel(ThreadMessage message) { - return await chat.PinMessageToChannel(ParentChannelId, message); + return await chat.PinMessageToChannel(ParentChannelId, message).ConfigureAwait(false); } public async Task UnPinMessageFromParentChannel() { - return await chat.UnpinMessageFromChannel(ParentChannelId); + 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 2635403..6aafd23 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -36,12 +36,12 @@ protected override SubscribeCallback CreateUpdateListener() public async Task PinMessageToParentChannel() { - return await chat.PinMessageToChannel(ParentChannelId, this); + return await chat.PinMessageToChannel(ParentChannelId, this).ConfigureAwait(false); } public async Task UnPinMessageFromParentChannel() { - return await chat.UnpinMessageFromChannel(ParentChannelId); + 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 e3474a2..be5ab51 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -212,8 +212,8 @@ public void SetListeningForModerationEvents(bool listen) public async Task Update(ChatUserData updatedData) { UpdateLocalData(updatedData); - var result = new ChatOperationResult(); - result.RegisterOperation(await UpdateUserData(chat, Id, updatedData)); + var result = new ChatOperationResult("User.Update()", chat); + result.RegisterOperation(await UpdateUserData(chat, Id, updatedData).ConfigureAwait(false)); return result; } @@ -248,12 +248,12 @@ internal static async Task> UpdateUserData(Cha { operation = operation.Custom(chatUserData.CustomData); } - return await operation.ExecuteAsync(); + 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(); + return await chat.PubnubInstance.GetUuidMetadata().Uuid(userId).IncludeCustom(true).ExecuteAsync().ConfigureAwait(false); } internal void UpdateLocalData(ChatUserData? newData) @@ -267,8 +267,8 @@ internal void UpdateLocalData(ChatUserData? newData) public override async Task Refresh() { - var result = new ChatOperationResult(); - var getUserData = await GetUserData(chat, Id); + var result = new ChatOperationResult("User.Refresh()", chat); + var getUserData = await GetUserData(chat, Id).ConfigureAwait(false); if (result.RegisterOperation(getUserData)) { return result; @@ -292,7 +292,7 @@ public override async Task Refresh() /// public async Task DeleteUser() { - await chat.DeleteUser(Id); + await chat.DeleteUser(Id).ConfigureAwait(false); } /// @@ -314,12 +314,12 @@ public async Task DeleteUser() /// public async Task SetRestriction(string channelId, bool banUser, bool muteUser, string reason) { - return 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) { - return await chat.SetRestriction(Id, channelId, restriction); + return await chat.SetRestriction(Id, channelId, restriction).ConfigureAwait(false); } /// @@ -338,11 +338,11 @@ public async Task SetRestriction(string channelId, Restrict /// public async Task> GetChannelRestrictions(Channel channel) { - var result = new ChatOperationResult(); + 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(); + }).Filter($"uuid.id == \"{Id}\"").IncludeCount(true).ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(membersResult) || membersResult.Result.ChannelMembers == null || !membersResult.Result.ChannelMembers.Any()) { result.Error = true; @@ -369,7 +369,7 @@ public async Task> GetChannelRestrictions(Chann public async Task> GetChannelsRestrictions(string sort = "", int limit = 0, PNPageObject page = null) { - var result = new ChatOperationResult(){Result = new ChannelsRestrictionsWrapper()}; + var result = new ChatOperationResult("User.GetChannelsRestrictions()", chat){Result = new ChannelsRestrictionsWrapper()}; var operation = chat.PubnubInstance.GetMemberships().Uuid(Id) .Include(new[] { @@ -388,7 +388,7 @@ public async Task> GetChannelsR { operation = operation.Page(page); } - var membershipsResult = await operation.ExecuteAsync(); + var membershipsResult = await operation.ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(membershipsResult)) { return result; @@ -442,7 +442,7 @@ public async Task> GetChannelsR /// public async Task IsPresentOn(string channelId) { - var response = await chat.PubnubInstance.WhereNow().Uuid(Id).ExecuteAsync(); + var response = await chat.PubnubInstance.WhereNow().Uuid(Id).ExecuteAsync().ConfigureAwait(false); if (response.Status.Error) { chat.Logger.Error($"Error when trying to perform IsPresentOn(): {response.Status.ErrorData.Information}"); @@ -474,8 +474,8 @@ public async Task IsPresentOn(string channelId) /// public async Task>> WherePresent() { - var result = new ChatOperationResult>(); - var where = await chat.PubnubInstance.WhereNow().Uuid(Id).ExecuteAsync(); + var result = new ChatOperationResult>("User.WherePresent()", chat); + var where = await chat.PubnubInstance.WhereNow().Uuid(Id).ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(where)) { return result; @@ -514,7 +514,7 @@ public async Task>> WherePresent() public async Task> GetMemberships(string filter = "", string sort = "", int limit = 0, PNPageObject page = null) { - return await chat.GetUserMemberships(Id, filter, sort, limit, page); + 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/Utilities/ChatUtils.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs index 8387199..83bdf4a 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatUtils.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using PubnubApi; +using PubNubChatAPI.Entities; using PubnubChatApi.Entities.Data; namespace PubnubChatApi.Utilities @@ -21,9 +22,9 @@ internal static long TimeTokenNowLong() return timeStamp; } - internal static ChatOperationResult ToChatOperationResult(this PNResult result) + internal static ChatOperationResult ToChatOperationResult(this PNResult result, string operationName, Chat chat) { - var operationResult = new ChatOperationResult(); + var operationResult = new ChatOperationResult(operationName, chat); operationResult.RegisterOperation(result); return operationResult; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ExponentialRateLimiter.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ExponentialRateLimiter.cs index 7438f82..b48ddc1 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ExponentialRateLimiter.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ExponentialRateLimiter.cs @@ -32,7 +32,7 @@ public async void RunWithinLimits(string id, int baseIntervalMs, Func= toSleep) { - toSleep = await ProcessQueue(slept); + toSleep = await ProcessQueue(slept).ConfigureAwait(false); } else { @@ -103,7 +103,7 @@ private async Task ProcessorLoop() if (slept > 0) { - await Task.Delay(slept, _cancellationTokenSource.Token); + await Task.Delay(slept, _cancellationTokenSource.Token).ConfigureAwait(false); } } } @@ -151,7 +151,7 @@ private async Task ProcessQueue(int sleptMs) // This ensures we don't overwhelm the system with concurrent tasks if (processingTasks.Count > 0) { - await Task.WhenAll(processingTasks); + await Task.WhenAll(processingTasks).ConfigureAwait(false); } // Remove finished limiters @@ -186,7 +186,7 @@ private async Task ProcessLimiterAsync(string id, RateLimiterRoot limiterRoot) try { - var result = await element.Task(); + var result = await element.Task().ConfigureAwait(false); element.Callback(result); } catch (Exception e) From dadee4b9c326ece80cea87828309019ad961c20b Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Thu, 11 Sep 2025 17:26:06 +0200 Subject: [PATCH 20/28] XML comments updates, some missing ChatOperationResult and ConfigureAwait(false) --- .../PubNubChatApi.Tests/MembershipTests.cs | 4 +- .../PubNubChatApi.Tests/UserTests.cs | 2 +- .../PubnubChatApi/Entities/Channel.cs | 191 ++++++--- .../PubnubChatApi/Entities/Chat.cs | 369 ++++++++++++------ .../PubnubChatApi/Entities/Membership.cs | 56 ++- .../PubnubChatApi/Entities/Message.cs | 240 +++++++++++- .../PubnubChatApi/Entities/MessageDraft.cs | 14 +- .../PubnubChatApi/Entities/ThreadChannel.cs | 66 ++++ .../PubnubChatApi/Entities/ThreadMessage.cs | 42 ++ .../PubnubChatApi/Entities/User.cs | 138 +++++-- 10 files changed, 911 insertions(+), 211 deletions(-) diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs index 24146d9..2abb844 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MembershipTests.cs @@ -165,7 +165,7 @@ public async Task TestUnreadMessagesCount() 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}"); } @@ -184,7 +184,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/UserTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs index 200e496..31f690f 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs @@ -132,7 +132,7 @@ public async Task TestUserIsPresentOn() 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/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index b00d187..afed93e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -221,6 +221,10 @@ public override async Task Refresh() 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: @@ -234,6 +238,10 @@ public void SetListeningForCustomEvents(bool listen) })); } + /// + /// 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: @@ -247,6 +255,10 @@ public void SetListeningForReportEvents(bool listen) })); } + /// + /// 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: @@ -272,6 +284,10 @@ async delegate(Pubnub pn, PNMessageResult m) })); } + /// + /// 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: @@ -328,6 +344,10 @@ public void SetListeningForTyping(bool listen) })); } + /// + /// 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: @@ -345,6 +365,11 @@ async delegate(Pubnub pn, PNPresenceEventResult p) })); } + /// + /// 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() @@ -353,6 +378,13 @@ public async Task ForwardMessage(Message message) }).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() @@ -365,16 +397,29 @@ public virtual async Task EmitUserMention(string userId, st 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 (); @@ -383,6 +428,10 @@ public async Task PinMessage(Message message) 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 (); @@ -395,7 +444,7 @@ public async Task UnpinMessage() /// /// Asynchronously tries to get the Message pinned to this Channel. /// - /// The pinned Message object if there was one, null otherwise. + /// A ChatOperationResult containing the pinned Message object if there was one, null otherwise. public async Task> GetPinnedMessage() { var result = new ChatOperationResult("Channel.GetPinnedMessage()", chat); @@ -429,7 +478,7 @@ public async Task> GetPinnedMessage() /// 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) { @@ -474,14 +523,15 @@ public void Disconnect() /// channel.Leave(); /// /// + /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// - public async void Leave() + public async Task Leave() { Disconnect(); var currentUserId = chat.PubnubInstance.GetCurrentUserId(); - var remove = await chat.PubnubInstance.RemoveMemberships().Uuid(currentUserId).Include(new [] + return (await chat.PubnubInstance.RemoveMemberships().Uuid(currentUserId).Include(new[] { PNMembershipField.TYPE, PNMembershipField.CUSTOM, @@ -491,12 +541,7 @@ public async void Leave() PNMembershipField.CHANNEL_TYPE, PNMembershipField.CHANNEL_STATUS }).Channels(new List() { Id }) - .ExecuteAsync().ConfigureAwait(false); - if (remove.Status.Error) - { - chat.Logger.Error($"Error when trying to leave channel \"{Id}\": {remove.Status.ErrorData.Information}"); - return; - } + .ExecuteAsync().ConfigureAwait(false)).ToChatOperationResult("Channel.Leave()", chat); } /// @@ -548,14 +593,16 @@ public void Connect() /// channel.Join(); /// /// + /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// - public async void Join(ChatMembershipData? membershipData = null) + public async Task Join(ChatMembershipData? membershipData = null) { + var result = new ChatOperationResult("Channel.Join()", chat); membershipData ??= new ChatMembershipData(); var currentUserId = chat.PubnubInstance.GetCurrentUserId(); - var response = await chat.PubnubInstance.SetMemberships().Uuid(currentUserId) + var setMembership = await chat.PubnubInstance.SetMemberships().Uuid(currentUserId) .Channels(new List() { new PNMembership() @@ -574,15 +621,18 @@ public async void Join(ChatMembershipData? membershipData = null) PNMembershipField.CHANNEL, PNMembershipField.CHANNEL_CUSTOM }).ExecuteAsync().ConfigureAwait(false); - if (response.Status.Error) + if (result.RegisterOperation(setMembership)) { - chat.Logger.Error($"Error when trying to Join() to channel \"{Id}\": {response.Status.ErrorData.Information}"); - return; + return result; } var joinMembership = new Membership(chat, currentUserId, Id, membershipData); - await joinMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()).ConfigureAwait(false); - + var setLast = await joinMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()).ConfigureAwait(false); + if (result.RegisterOperation(setLast)) + { + return result; + } Connect(); + return result; } /// @@ -596,10 +646,11 @@ public async void Join(ChatMembershipData? membershipData = null) /// 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"); /// /// /// @@ -608,6 +659,12 @@ public async Task SetRestrictions(string userId, bool banUs 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); @@ -621,10 +678,11 @@ public async Task SetRestrictions(string userId, Restrictio /// /// /// 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!"); /// /// /// @@ -633,6 +691,15 @@ 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); @@ -714,10 +781,11 @@ public virtual async Task SendText(string message, SendText /// /// /// 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\"}", @@ -738,37 +806,32 @@ 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(); /// /// public async Task Delete() { - return 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; /// /// /// @@ -802,6 +865,13 @@ public async Task> GetUserRestrictions(User use 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) { @@ -859,11 +929,12 @@ public async Task> GetUsersRestric /// /// /// 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}"); /// /// @@ -871,7 +942,7 @@ public async Task> GetUsersRestric public async Task> IsUserPresent(string userId) { var result = new ChatOperationResult("Channel.IsUserPresent()", chat); - var wherePresent = await chat.WherePresent(userId); + var wherePresent = await chat.WherePresent(userId).ConfigureAwait(false); if (result.RegisterOperation(wherePresent)) { return result; @@ -886,11 +957,12 @@ 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}"); /// } @@ -921,14 +993,16 @@ 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}"); /// } @@ -938,33 +1012,50 @@ public async Task>> WhoIsPresent() public async Task> GetMemberships(string filter = "", string sort = "", int limit = 0, PNPageObject page = null) { - return await chat.GetChannelMemberships(Id, filter, sort, limit, page); + 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. + /// A ChatOperationResult containing the Message object if one was found, null otherwise. public async Task> GetMessage(string timeToken) { - return await chat.GetMessage(Id, 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); + 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); + 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); + 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 6d780f2..3b3057f 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -49,6 +49,7 @@ public class Chat /// 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. /// @@ -88,6 +89,11 @@ internal Chat(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? l #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) @@ -108,14 +114,15 @@ public async Task 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; /// /// /// @@ -138,14 +145,15 @@ public async Task> CreatePublicConversation(string /// /// 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; /// /// /// @@ -255,6 +263,14 @@ private async Task> CreateConversatio 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) { @@ -262,6 +278,14 @@ public async Task> CreateDirectConver 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) { @@ -269,6 +293,12 @@ public async Task> CreateGroupConvers 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); @@ -320,19 +350,25 @@ public async Task> InviteToChannel(string channe } var inviteEventPayload = $"{{\"channelType\": \"{channel.Result.Type}\", \"channelId\": {channelId}}}"; - await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload); + await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload).ConfigureAwait(false); var newMembership = new Membership(this, userId, channelId, new ChatMembershipData()); - await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()); + 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); + var channel = await GetChannel(channelId).ConfigureAwait(false); if (result.RegisterOperation(channel)) { return result; @@ -365,14 +401,14 @@ public async Task>> InviteMultipleToChannel continue; } var newMembership = new Membership(this, userId, channelId, channelMember); - await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()); + 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); + await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload).ConfigureAwait(false); } - await channel.Result.Refresh(); + await channel.Result.Refresh().ConfigureAwait(false); return result; } @@ -381,11 +417,11 @@ public async Task>> InviteMultipleToChannel /// Performs an async retrieval of a Channel object with a given ID. /// /// ID of the channel. - /// Channel object if it exists, null otherwise. + /// 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); + var getResult = await Channel.GetChannelData(this, channelId).ConfigureAwait(false); if (result.RegisterOperation(getResult)) { return result; @@ -403,6 +439,14 @@ public async Task> GetChannel(string channelId) 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) { @@ -452,10 +496,11 @@ public async Task GetChannels(string filter = "", strin /// /// The channel ID. /// The updated data for the channel. + /// 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" /// // ... /// }); @@ -465,7 +510,7 @@ public async Task GetChannels(string filter = "", strin public async Task UpdateChannel(string channelId, ChatChannelData updatedData) { var result = new ChatOperationResult("Chat.UpdateChannel()", this); - result.RegisterOperation(await Channel.UpdateChannelData(this, channelId, updatedData)); + result.RegisterOperation(await Channel.UpdateChannelData(this, channelId, updatedData).ConfigureAwait(false)); return result; } @@ -476,10 +521,11 @@ public async Task UpdateChannel(string channelId, ChatChann /// /// /// The channel ID. + /// 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) @@ -498,31 +544,38 @@ internal async void StoreActivityTimeStamp() var currentUserId = PubnubInstance.GetCurrentUserId(); while (storeActivity) { - var getResult = await User.GetUserData(this, currentUserId); + 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); + 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); + 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); + 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); + var getEventHistory = await GetEventsHistory(id, startTimeToken, endTimeToken, count).ConfigureAwait(false); if (result.RegisterOperation(getEventHistory)) { return result; @@ -543,7 +596,7 @@ public async Task> GetCurrentUserMentio { continue; } - var getMessage = await GetMessage(mentionChannel.ToString(), messageTimeToken.ToString()); + 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}"); @@ -570,12 +623,12 @@ public async Task> GetCurrentUserMentio /// /// Asynchronously tries to retrieve the current User object for this chat. /// - /// User object if there is a current user, null otherwise. + /// 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); + var getUser = await GetUser(userId).ConfigureAwait(false); if (result.RegisterOperation(getUser)) { return result; @@ -595,20 +648,21 @@ public async Task> GetCurrentUser() /// The ban user flag. /// The mute user flag. /// The reason for the restrictions. + /// A ChatOperationResult indicating the success or failure of the operation. /// /// - /// await chat.SetRestriction("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) { var result = new ChatOperationResult("Chat.SetRestriction()", this); var restrictionsChannelId = $"{INTERNAL_MODERATION_PREFIX}_{channelId}"; - var getResult = await Channel.GetChannelData(this, restrictionsChannelId); + var getResult = await Channel.GetChannelData(this, restrictionsChannelId).ConfigureAwait(false); if (result.RegisterOperation(getResult)) { if (result.RegisterOperation(await Channel.UpdateChannelData(this, restrictionsChannelId, - new ChatChannelData()))) + new ChatChannelData()).ConfigureAwait(false))) { return result; } @@ -623,7 +677,7 @@ public async Task SetRestriction(string userId, string chan return result; } result.RegisterOperation(await EmitEvent(PubnubChatEventType.Moderation, moderationEventsChannelId, - $"{{\"channelId\": \"{channelId}\", \"restriction\": \"lifted\", \"reason\": \"{reason}\"}}")); + $"{{\"channelId\": \"{channelId}\", \"restriction\": \"lifted\", \"reason\": \"{reason}\"}}").ConfigureAwait(false)); return result; } //Ban or mute @@ -648,7 +702,7 @@ public async Task SetRestriction(string userId, string chan return result; } result.RegisterOperation(await EmitEvent(PubnubChatEventType.Moderation, moderationEventsChannelId, - $"{{\"channelId\": \"{channelId}\", \"restriction\": \"{(banUser ? "banned" : "muted")}\", \"reason\": \"{reason}\"}}")); + $"{{\"channelId\": \"{channelId}\", \"restriction\": \"{(banUser ? "banned" : "muted")}\", \"reason\": \"{reason}\"}}").ConfigureAwait(false)); return result; } @@ -661,21 +715,27 @@ public async Task SetRestriction(string userId, string chan /// The user ID. /// The channel ID. /// The Restriction object to be applied. + /// A ChatOperationResult indicating the success or failure of the operation. /// /// - /// await chat.SetRestriction("user_id", "channel_id", new Restriction(){Ban = true, Mute = true, Reason = "Spamming"}); + /// 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); + 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); + var getUser = await GetUser(userId).ConfigureAwait(false); if (!getUser.Error) { getUser.Result.OnUserUpdated += listener; @@ -690,20 +750,21 @@ public async void AddListenerToUsersUpdate(List userIds, Action li /// /// /// The user ID. - /// The created user. + /// A ChatOperationResult containing the created User object. /// /// The data for user is empty. /// /// /// /// 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) { - return await CreateUser(userId, new ChatUserData()); + return await CreateUser(userId, new ChatUserData()).ConfigureAwait(false); } /// @@ -714,25 +775,26 @@ public async Task> CreateUser(string userId) /// /// The user ID. /// The additional data for the user. - /// The created user. + /// 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) { var result = new ChatOperationResult("Chat.CreateUser()", this); - var existingUser = await GetUser(userId); + var existingUser = await GetUser(userId).ConfigureAwait(false); if (!result.RegisterOperation(existingUser, false)) { result.Result = existingUser.Result; return result; } - var update = await User.UpdateUserData(this, userId, additionalData); + var update = await User.UpdateUserData(this, userId, additionalData).ConfigureAwait(false); if (result.RegisterOperation(update)) { return result; @@ -750,11 +812,12 @@ public async Task> CreateUser(string userId, ChatUserD /// /// The user ID. /// The channel ID. - /// True if the user is present, false otherwise. + /// 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 /// } /// @@ -764,12 +827,12 @@ public async Task> CreateUser(string userId, ChatUserD public async Task> IsPresent(string userId, string channelId) { var result = new ChatOperationResult("Chat.IsPresent()", this); - var getChannel = await GetChannel(channelId); + var getChannel = await GetChannel(channelId).ConfigureAwait(false); if (result.RegisterOperation(getChannel)) { return result; } - var isPresent = await getChannel.Result.IsUserPresent(userId); + var isPresent = await getChannel.Result.IsUserPresent(userId).ConfigureAwait(false); if (result.RegisterOperation(isPresent)) { return result; @@ -785,12 +848,12 @@ public async Task> IsPresent(string userId, string cha /// /// /// The channel ID. - /// The list of the users present in the channel. + /// 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 /// } /// @@ -800,12 +863,12 @@ public async Task> IsPresent(string userId, string cha public async Task>> WhoIsPresent(string channelId) { var result = new ChatOperationResult>("Chat.WhoIsPresent()", this) { Result = new List() }; - var getChannel = await GetChannel(channelId); + var getChannel = await GetChannel(channelId).ConfigureAwait(false); if (result.RegisterOperation(getChannel)) { return result; } - var whoIs = await getChannel.Result.WhoIsPresent(); + var whoIs = await getChannel.Result.WhoIsPresent().ConfigureAwait(false); if (result.RegisterOperation(whoIs)) { return result; @@ -821,13 +884,13 @@ public async Task>> WhoIsPresent(string channel /// /// /// The user ID. - /// The list of the channels where the user is present. + /// 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 /// }; /// /// @@ -836,12 +899,12 @@ public async Task>> WhoIsPresent(string channel public async Task>> WherePresent(string userId) { var result = new ChatOperationResult>("Chat.WherePresent()", this) { Result = new List() }; - var getUser = await GetUser(userId); + var getUser = await GetUser(userId).ConfigureAwait(false); if (result.RegisterOperation(getUser)) { return result; } - var wherePresent = await getUser.Result.WherePresent(); + var wherePresent = await getUser.Result.WherePresent().ConfigureAwait(false); if (result.RegisterOperation(wherePresent)) { return result; @@ -854,11 +917,11 @@ public async Task>> WherePresent(string userId) /// 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. + /// 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); + var getData = await User.GetUserData(this, userId).ConfigureAwait(false); if (result.RegisterOperation(getData)) { return result; @@ -874,21 +937,19 @@ public async Task> GetUser(string userId) /// 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. /// /// /// 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 /// }; /// @@ -944,19 +1005,20 @@ public async Task GetUsers(string filter = "", string sort /// /// The user ID. /// The updated data for the user. + /// 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) { - await User.UpdateUserData(this, userId, updatedData); + return (await User.UpdateUserData(this, userId, updatedData).ConfigureAwait(false)).ToChatOperationResult("Chat.UpdateUser()", this); } /// @@ -966,10 +1028,11 @@ public async Task UpdateUser(string userId, ChatUserData updatedData) /// /// /// The user ID. + /// 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) @@ -987,24 +1050,23 @@ 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. + /// 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 /// }; /// @@ -1070,24 +1132,23 @@ public async Task> GetUserMembership /// 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. + /// 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 /// }; /// @@ -1154,11 +1215,19 @@ public async Task> GetChannelMembers #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); + count).ConfigureAwait(false); } /// @@ -1166,12 +1235,12 @@ public async Task> GetMessageReportsHi /// /// ID of the channel on which the message was sent. /// TimeToken of the searched-for message. - /// Message object if one was found, null otherwise. + /// 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); + var getHistory = await GetChannelMessageHistory(channelId, startTimeToken, messageTimeToken, 1).ConfigureAwait(false); if (result.RegisterOperation(getHistory)) { return result; @@ -1186,6 +1255,14 @@ public async Task> GetMessage(string channelId, str 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) @@ -1198,12 +1275,12 @@ public async Task> MarkAllMessage return result; } var currentUserId = PubnubInstance.GetCurrentUserId(); - var getCurrentUser = await GetCurrentUser(); + var getCurrentUser = await GetCurrentUser().ConfigureAwait(false); if (result.RegisterOperation(getCurrentUser)) { return result; } - var getCurrentMemberships = await getCurrentUser.Result.GetMemberships(filter, sort, limit, page); + var getCurrentMemberships = await getCurrentUser.Result.GetMemberships(filter, sort, limit, page).ConfigureAwait(false); if (result.RegisterOperation(getCurrentMemberships)) { return result; @@ -1223,14 +1300,14 @@ public async Task> MarkAllMessage membership.MembershipData.CustomData ??= new(); membership.MembershipData.CustomData["lastReadMessageTimetoken"] = timeToken; } - if (result.RegisterOperation(await Membership.UpdateMembershipsData(this, currentUserId, memberships))) + 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}\"}}"); + $"{{\"messageTimetoken\": \"{timeToken}\"}}").ConfigureAwait(false); } result.Result = new MarkMessagesAsReadWrapper() { @@ -1242,6 +1319,14 @@ await EmitEvent(PubnubChatEventType.Receipt, membership.ChannelId, 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) @@ -1253,12 +1338,12 @@ public async Task>> GetUnreadMess result.Exception = new PNException("For getting message counts limit has to be between 0 and 100"); return result; } - var getCurrentUser = await GetCurrentUser(); + var getCurrentUser = await GetCurrentUser().ConfigureAwait(false); if (result.RegisterOperation(getCurrentUser)) { return result; } - var getCurrentMemberships = await getCurrentUser.Result.GetMemberships(filter, sort, limit, page); + var getCurrentMemberships = await getCurrentUser.Result.GetMemberships(filter, sort, limit, page).ConfigureAwait(false); if (result.RegisterOperation(getCurrentMemberships)) { return result; @@ -1297,10 +1382,16 @@ public async Task>> GetUnreadMess 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); + var getMessage = await GetMessage(messageChannelId, messageTimeToken).ConfigureAwait(false); if (result.RegisterOperation(getMessage)) { return result; @@ -1314,15 +1405,21 @@ public async Task> CreateThreadChannel(string 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); + var getMessage = await GetMessage(messageChannelId, messageTimeToken).ConfigureAwait(false); if (result.RegisterOperation(getMessage)) { return result; } - result.RegisterOperation(await getMessage.Result.RemoveThread()); + result.RegisterOperation(await getMessage.Result.RemoveThread().ConfigureAwait(false)); return result; } @@ -1330,11 +1427,11 @@ public async Task RemoveThreadChannel(string messageTimeTok /// 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. + /// 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()); + var getChannel = await GetChannel(message.GetThreadId()).ConfigureAwait(false); if (result.RegisterOperation(getChannel)) { return result; @@ -1349,24 +1446,36 @@ public async Task> GetThreadChannel(Message m 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); + var getMessage = await GetMessage(channelId, messageTimeToken).ConfigureAwait(false); if (result.RegisterOperation(getMessage)) { return result; } - result.RegisterOperation(await getMessage.Result.Forward(channelId)); + 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); + var getMessage = await GetMessage(channelId, messageTimeToken).ConfigureAwait(false); if (!getMessage.Error) { getMessage.Result.OnMessageUpdated += listener; @@ -1374,28 +1483,39 @@ public async void AddListenerToMessagesUpdate(string channelId, List mes } } + /// + /// 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); + var getChannel = await GetChannel(channelId).ConfigureAwait(false); if (result.RegisterOperation(getChannel)) { return result; } - var pin = await getChannel.Result.PinMessage(message); + 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); + var getChannel = await GetChannel(channelId).ConfigureAwait(false); if (result.RegisterOperation(getChannel)) { return result; } - var unpin = await getChannel.Result.UnpinMessage(); + var unpin = await getChannel.Result.UnpinMessage().ConfigureAwait(false); result.RegisterOperation(unpin); return result; } @@ -1411,12 +1531,12 @@ 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. + /// 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 /// }; /// @@ -1459,6 +1579,14 @@ 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) @@ -1495,6 +1623,13 @@ public async Task> GetEventsHistory(st 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); @@ -1512,6 +1647,12 @@ public async Task EmitEvent(PubnubChatEventType type, strin #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; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs index 169aacb..2c3b672 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Membership.cs @@ -96,6 +96,7 @@ protected override SubscribeCallback CreateUpdateListener() /// /// /// The ChatMembershipData object to update the membership with. + /// A ChatOperationResult indicating the success or failure of the operation. /// public async Task Update(ChatMembershipData membershipData) { @@ -151,11 +152,33 @@ internal static async Task> UpdateMembershipsData( }).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); @@ -171,24 +194,43 @@ public async Task SetLastReadMessageTimeToken(string timeTo return result; } - public async Task GetUnreadMessagesCount() + /// + /// 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)) { - chat.Logger.Error("LastReadMessageTimeToken is not a valid time token!"); - return -1; + 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 (countsResponse.Status.Error) + if (result.RegisterOperation(countsResponse)) { - chat.Logger.Error($"Error when trying to get message counts on channel \"{ChannelId}\": {countsResponse.Status.ErrorData}"); - return -1; + return result; } - return countsResponse.Result.Channels[ChannelId]; + 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); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs index f9399c3..1dd7dc1 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -88,6 +88,13 @@ public string MessageText { /// 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 { @@ -114,6 +121,13 @@ public List MentionedUsers { } } + /// + /// 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 { @@ -140,6 +154,14 @@ public List ReferencedChannels { } } + /// + /// 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 { @@ -167,8 +189,22 @@ public List TextLinks { } } + /// + /// 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(); @@ -235,10 +271,11 @@ protected override SubscribeCallback CreateUpdateListener() /// /// /// 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"); /// /// /// @@ -258,6 +295,10 @@ public async Task EditMessageText(string newText) 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); @@ -284,6 +325,15 @@ public async Task> GetQuotedMessage() 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); @@ -294,6 +344,27 @@ 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); @@ -328,12 +399,34 @@ public ChatOperationResult CreateThread() /// /// Asynchronously tries to get the ThreadChannel started on this Message. /// - /// The retrieved ThreadChannel object, null if one wasn't found. + /// 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); @@ -344,7 +437,7 @@ public async Task RemoveThread() return result; } var threadMessageAction = MessageActions.First(x => x.Type == PubnubMessageActionType.ThreadRootId); - var getThread = await GetThread(); + var getThread = await GetThread().ConfigureAwait(false); if (result.RegisterOperation(getThread)) { return result; @@ -360,6 +453,25 @@ public async Task RemoveThread() 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); @@ -372,6 +484,24 @@ public async Task Pin() 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() @@ -386,6 +516,24 @@ public async Task Report(string reason) 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); @@ -398,11 +546,49 @@ public async Task Forward(string channelId) 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); @@ -441,6 +627,27 @@ public async Task ToggleReaction(string 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); @@ -461,16 +668,15 @@ public async Task Restore() /// /// 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); /// /// /// @@ -521,6 +727,24 @@ public async Task Delete(bool soft) 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); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs index 60f6698..855c2ef 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs @@ -167,7 +167,7 @@ private async Task> GenerateSuggestedMentions() 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)) + !(await user.IsPresentOn(channel.Id).ConfigureAwait(false)).Result) { continue; } @@ -236,8 +236,11 @@ internal List SuggestRawMentions() } /// + /// 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. @@ -573,8 +576,9 @@ private bool ValidateSuggestedMention(SuggestedMention suggestedMention) } /// - /// Validates that mentions don't overlap + /// 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++) @@ -628,8 +632,12 @@ public List GetMessageElements() } /// - /// Renders the message with markdown-style links + /// 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(); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs index f4c0b67..c8de60b 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -52,6 +52,31 @@ public override async Task SendText(string message, SendTex 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) { @@ -88,11 +113,52 @@ public override async Task EmitUserMention(string userId, s 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); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs index 6aafd23..e2e5dbc 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -34,11 +34,53 @@ protected override SubscribeCallback CreateUpdateListener() }); } + /// + /// 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); diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index be5ab51..de24c5b 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -153,6 +153,14 @@ protected override SubscribeCallback CreateUpdateListener() }); } + /// + /// 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: @@ -166,6 +174,14 @@ public void SetListeningForMentionEvents(bool listen) })); } + /// + /// 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: @@ -179,6 +195,14 @@ public void SetListeningForInviteEvents(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) { SetListening(ref moderationSubscription, SubscriptionOptions.None, listen, Chat.INTERNAL_MODERATION_PREFIX+Id, chat.ListenerFactory.ProduceListener(messageCallback: @@ -199,10 +223,11 @@ public void SetListeningForModerationEvents(bool listen) /// /// /// The updated data for the user. + /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// var user = // ...; - /// user.UpdateUser(new ChatUserData + /// var result = await user.Update(new ChatUserData /// { /// UserName = "New User Name", /// }); @@ -265,6 +290,24 @@ internal void UpdateLocalData(ChatUserData? newData) 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); @@ -284,15 +327,16 @@ public override async Task Refresh() /// 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 = // ...; - /// user.DeleteUser(); + /// await user.DeleteUser(); /// /// - public async Task DeleteUser() + public async Task DeleteUser() { - await chat.DeleteUser(Id).ConfigureAwait(false); + return await chat.DeleteUser(Id).ConfigureAwait(false); } /// @@ -317,25 +361,40 @@ public async Task SetRestriction(string channelId, bool ban 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 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. - /// + /// 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); @@ -366,6 +425,29 @@ public async Task> GetChannelRestrictions(Chann 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) { @@ -430,7 +512,7 @@ public async Task> GetChannelsR /// /// 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. /// /// /// @@ -440,15 +522,16 @@ public async Task> GetChannelsR /// } /// /// - public async Task IsPresentOn(string channelId) + 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 (response.Status.Error) + if (result.RegisterOperation(response)) { - chat.Logger.Error($"Error when trying to perform IsPresentOn(): {response.Status.ErrorData.Information}"); - return false; + return result; } - return response.Result.Channels.Contains(channelId); + result.Result = response.Result.Channels.Contains(channelId); + return result; } /// @@ -458,7 +541,7 @@ 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. @@ -466,7 +549,8 @@ public async Task IsPresentOn(string channelId) /// /// /// var user = // ...; - /// var channels = user.WherePresent(); + /// var result = await user.WherePresent(); + /// var channels = result.Result; /// foreach (var channel in channels) { /// Console.WriteLine(channel); /// } @@ -495,16 +579,18 @@ 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. /// /// /// /// 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); /// } From 68eada0046d08d4b50b3543cfeb8dce74de55d6a Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Fri, 12 Sep 2025 11:07:38 +0200 Subject: [PATCH 21/28] add more leeway in GetMessage test --- .../PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs index 4d11088..f22a2f0 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs @@ -94,7 +94,7 @@ public async Task TestGetMessage() ChatOperationResult receivedMessage = null; channel.OnMessageReceived += async message => { - await Task.Delay(3000); + await Task.Delay(5000); if (message.ChannelId == channel.Id) { receivedMessage = await chat.GetMessage(channel.Id, message.TimeToken); @@ -103,7 +103,7 @@ public async Task TestGetMessage() }; await channel.SendText("something"); - var received = manualReceiveEvent.WaitOne(8000); + var received = manualReceiveEvent.WaitOne(12000); Assert.IsTrue(received); Assert.True(!receivedMessage.Error, $"Error when trying to GetMessage(): {receivedMessage.Exception?.Message}"); } From 03d1fdbea51a5f396556cc6b40562ad0ee680b91 Mon Sep 17 00:00:00 2001 From: "PUBNUB\\jakub.grzesiowski" Date: Mon, 15 Sep 2025 14:27:25 +0200 Subject: [PATCH 22/28] Update Unity project with API source code instead of CPP DLL --- c-sharp-chat/PubnubChatApi/PubnubChatApi.sln | 4 +- .../PubnubChatApi/Entities/Chat.cs | 29 + .../PubnubChat/Runtime/PubnubChatApi.dll | Bin 157184 -> 0 bytes .../PubnubChat/Runtime/PubnubChatApi.dll.meta | 33 - ...nubChatApi.xml.meta => PubnubChatApi.meta} | 5 +- .../PubnubChat/Runtime/PubnubChatApi.xml | 1574 ----------- .../Runtime/PubnubChatApi/Entities.meta | 8 + .../Runtime/PubnubChatApi/Entities/Base.meta | 8 + .../PubnubChatApi/Entities/Base/ChatEntity.cs | 45 + .../Entities/Base/ChatEntity.cs.meta | 11 + .../Entities/Base/UniqueChatEntity.cs | 14 + .../Entities/Base/UniqueChatEntity.cs.meta | 11 + .../Runtime/PubnubChatApi/Entities/Channel.cs | 1061 ++++++++ .../PubnubChatApi/Entities/Channel.cs.meta | 11 + .../Runtime/PubnubChatApi/Entities/Chat.cs | 1697 ++++++++++++ .../PubnubChatApi/Entities/Chat.cs.meta | 11 + .../Entities/ChatAccessManager.cs | 50 + .../Entities/ChatAccessManager.cs.meta | 11 + .../Runtime/PubnubChatApi/Entities/Data.meta | 8 + .../Entities/Data/ChannelsResponseWrapper.cs | 15 + .../Data/ChannelsResponseWrapper.cs.meta | 11 + .../Data/ChannelsRestrictionsWrapper.cs | 12 + .../Data/ChannelsRestrictionsWrapper.cs.meta | 11 + .../Entities/Data/ChatChannelData.cs | 37 + .../Entities/Data/ChatChannelData.cs.meta | 11 + .../Entities/Data/ChatMembershipData.cs | 31 + .../Entities/Data/ChatMembershipData.cs.meta | 11 + .../Entities/Data/ChatOperationResult.cs | 73 + .../Entities/Data/ChatOperationResult.cs.meta | 11 + .../Entities/Data/ChatUserData.cs | 40 + .../Entities/Data/ChatUserData.cs.meta | 11 + .../Entities/Data/CreatedChannelWrapper.cs | 12 + .../Data/CreatedChannelWrapper.cs.meta | 11 + .../Entities/Data/EventsHistoryWrapper.cs | 11 + .../Data/EventsHistoryWrapper.cs.meta | 11 + .../Data/MarkMessagesAsReadWrapper.cs | 16 + .../Data/MarkMessagesAsReadWrapper.cs.meta | 11 + .../Entities/Data/MembersResponseWrapper.cs | 16 + .../Data/MembersResponseWrapper.cs.meta | 11 + .../Entities/Data/MentionedUser.cs | 12 + .../Entities/Data/MentionedUser.cs.meta | 11 + .../Entities/Data/MessageAction.cs | 12 + .../Entities/Data/MessageAction.cs.meta | 11 + .../Entities/Data/PubnubChatConfig.cs | 35 + .../Entities/Data/PubnubChatConfig.cs.meta | 11 + .../Entities/Data/ReferencedChannel.cs | 12 + .../Entities/Data/ReferencedChannel.cs.meta | 11 + .../Entities/Data/Restriction.cs | 22 + .../Entities/Data/Restriction.cs.meta | 11 + .../Entities/Data/SendTextParams.cs | 14 + .../Entities/Data/SendTextParams.cs.meta | 11 + .../PubnubChatApi/Entities/Data/TextLink.cs | 14 + .../Entities/Data/TextLink.cs.meta | 11 + .../Entities/Data/UnreadMessageWrapper.cs | 13 + .../Data/UnreadMessageWrapper.cs.meta | 11 + .../Entities/Data/UserMentionData.cs | 15 + .../Entities/Data/UserMentionData.cs.meta | 11 + .../Entities/Data/UserMentionsWrapper.cs | 11 + .../Entities/Data/UserMentionsWrapper.cs.meta | 11 + .../Entities/Data/UsersResponseWrapper.cs | 15 + .../Data/UsersResponseWrapper.cs.meta | 11 + .../Entities/Data/UsersRestrictionsWrapper.cs | 12 + .../Data/UsersRestrictionsWrapper.cs.meta | 11 + .../PubnubChatApi/Entities/Events.meta | 8 + .../Entities/Events/ChatEvent.cs | 13 + .../Entities/Events/ChatEvent.cs.meta | 11 + .../PubnubChatApi/Entities/Membership.cs | 239 ++ .../PubnubChatApi/Entities/Membership.cs.meta | 11 + .../Runtime/PubnubChatApi/Entities/Message.cs | 761 ++++++ .../PubnubChatApi/Entities/Message.cs.meta | 11 + .../PubnubChatApi/Entities/MessageDraft.cs | 699 +++++ .../Entities/MessageDraft.cs.meta | 11 + .../PubnubChatApi/Entities/ThreadChannel.cs | 167 ++ .../Entities/ThreadChannel.cs.meta | 11 + .../PubnubChatApi/Entities/ThreadMessage.cs | 89 + .../Entities/ThreadMessage.cs.meta | 11 + .../Runtime/PubnubChatApi/Entities/User.cs | 606 +++++ .../PubnubChatApi/Entities/User.cs.meta | 11 + .../Runtime/PubnubChatApi/Enums.meta | 8 + .../Enums/PubnubAccessPermission.cs | 13 + .../Enums/PubnubAccessPermission.cs.meta | 11 + .../Enums/PubnubAccessResourceType.cs | 8 + .../Enums/PubnubAccessResourceType.cs.meta | 11 + .../PubnubChatApi/Enums/PubnubChannelType.cs | 9 + .../Enums/PubnubChannelType.cs.meta | 11 + .../Enums/PubnubChatEventType.cs | 13 + .../Enums/PubnubChatEventType.cs.meta | 11 + .../Enums/PubnubChatMessageType.cs | 13 + .../Enums/PubnubChatMessageType.cs.meta | 11 + .../Enums/PubnubMessageActionType.cs | 12 + .../Enums/PubnubMessageActionType.cs.meta | 11 + .../Runtime/PubnubChatApi/Utilities.meta | 8 + .../Utilities/ChatEnumConverters.cs | 76 + .../Utilities/ChatEnumConverters.cs.meta | 11 + .../Utilities/ChatListenerFactory.cs | 16 + .../Utilities/ChatListenerFactory.cs.meta | 11 + .../PubnubChatApi/Utilities/ChatParsers.cs | 279 ++ .../Utilities/ChatParsers.cs.meta | 11 + .../PubnubChatApi/Utilities/ChatUtils.cs | 32 + .../PubnubChatApi/Utilities/ChatUtils.cs.meta | 11 + .../PubnubChatApi/Utilities/DiffMatchPatch.cs | 2296 +++++++++++++++++ .../Utilities/DiffMatchPatch.cs.meta | 11 + .../Utilities/DotNetListenerFactory.cs | 20 + .../Utilities/DotNetListenerFactory.cs.meta | 11 + .../Utilities/ExponentialRateLimiter.cs | 243 ++ .../Utilities/ExponentialRateLimiter.cs.meta | 11 + .../Utilities/PubnubChatDotNetPNSDKSource.cs | 20 + .../PubnubChatDotNetPNSDKSource.cs.meta | 11 + .../Assets/PubnubChat/Runtime/UnityChat.cs | 67 + .../PubnubChat/Runtime/UnityChat.cs.meta | 3 + .../Runtime/UnityListenerFactory.cs | 18 + .../Runtime/UnityListenerFactory.cs.meta | 11 + .../PubnubChat/Runtime/libpubnub-chat.dylib | Bin 3754688 -> 0 bytes .../Runtime/libpubnub-chat.dylib.meta | 33 - .../Assets/PubnubChat/Runtime/pubnub-chat.dll | Bin 1172992 -> 0 bytes .../PubnubChat/Runtime/pubnub-chat.dll.meta | 27 - .../PubnubChatConfigAsset.cs | 22 +- .../PubnubChatConfigAsset/PubnubChatSample.cs | 73 +- .../PubnubChatUnity/Packages/manifest.json | 1 + .../Packages/packages-lock.json | 16 + 120 files changed, 9725 insertions(+), 1708 deletions(-) delete mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.dll delete mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.dll.meta rename unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/{PubnubChatApi.xml.meta => PubnubChatApi.meta} (57%) delete mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi.xml create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/ChatEntity.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/ChatEntity.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/UniqueChatEntity.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/UniqueChatEntity.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ChatAccessManager.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ChatAccessManager.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsRestrictionsWrapper.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatChannelData.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatChannelData.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatMembershipData.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatMembershipData.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatOperationResult.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatOperationResult.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatUserData.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatUserData.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/EventsHistoryWrapper.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/EventsHistoryWrapper.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MentionedUser.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MentionedUser.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MessageAction.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MessageAction.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/PubnubChatConfig.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/PubnubChatConfig.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ReferencedChannel.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ReferencedChannel.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/Restriction.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/Restriction.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/SendTextParams.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/SendTextParams.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/TextLink.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/TextLink.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionData.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionData.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersRestrictionsWrapper.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersRestrictionsWrapper.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Events.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Events/ChatEvent.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Events/ChatEvent.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Membership.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Membership.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MessageDraft.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MessageDraft.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadChannel.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadChannel.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadMessage.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadMessage.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/User.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/User.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessPermission.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessPermission.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessResourceType.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessResourceType.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChannelType.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChannelType.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatEventType.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatEventType.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatMessageType.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatMessageType.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubMessageActionType.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubMessageActionType.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatEnumConverters.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatEnumConverters.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatListenerFactory.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatListenerFactory.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatUtils.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatUtils.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DiffMatchPatch.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DiffMatchPatch.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DotNetListenerFactory.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DotNetListenerFactory.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ExponentialRateLimiter.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ExponentialRateLimiter.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/PubnubChatDotNetPNSDKSource.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/PubnubChatDotNetPNSDKSource.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChat.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChat.cs.meta create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityListenerFactory.cs create mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityListenerFactory.cs.meta delete mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/libpubnub-chat.dylib delete mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/libpubnub-chat.dylib.meta delete mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/pubnub-chat.dll delete mode 100644 unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/pubnub-chat.dll.meta diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi.sln b/c-sharp-chat/PubnubChatApi/PubnubChatApi.sln index 5c36ac5..e64a13e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi.sln +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi.sln @@ -14,8 +14,8 @@ Global GlobalSection(ProjectConfigurationPlatforms) = postSolution {8D3851D4-6FD7-4DE5-9960-DA386442603E}.Release|Any CPU.ActiveCfg = Release|Any CPU {8D3851D4-6FD7-4DE5-9960-DA386442603E}.Release|Any CPU.Build.0 = Release|Any CPU - {8D3851D4-6FD7-4DE5-9960-DA386442603E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8D3851D4-6FD7-4DE5-9960-DA386442603E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D3851D4-6FD7-4DE5-9960-DA386442603E}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {8D3851D4-6FD7-4DE5-9960-DA386442603E}.Debug|Any CPU.Build.0 = Release|Any CPU {54ACBC4B-510A-499F-9494-24F9F90F7B67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54ACBC4B-510A-499F-9494-24F9F90F7B67}.Debug|Any CPU.Build.0 = Debug|Any CPU {54ACBC4B-510A-499F-9494-24F9F90F7B67}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 3b3057f..4e2c077 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -69,6 +69,35 @@ public static async Task> CreateInstance(PubnubChatCon 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); 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 05e215b23fa88c230b546271afa203a72f40eac5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157184 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 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..43af9f5 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/ChatEntity.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi.Entities.Data; + +namespace PubNubChatAPI.Entities +{ + 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..3b06a01 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Base/UniqueChatEntity.cs @@ -0,0 +1,14 @@ +using System; + +namespace PubNubChatAPI.Entities +{ + 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..afed93e --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs @@ -0,0 +1,1061 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Timers; +using PubnubApi; +using PubnubChatApi.Entities.Data; +using PubnubChatApi.Entities.Events; +using PubnubChatApi.Enums; +using PubnubChatApi.Utilities; + +namespace PubNubChatAPI.Entities +{ + /// + /// 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 pn, 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(Pubnub pn, PNPresenceEventResult p) + { + 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..4e2c077 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs @@ -0,0 +1,1697 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi.Entities.Data; +using PubnubChatApi.Entities.Events; +using PubnubChatApi.Enums; +using PubnubChatApi.Utilities; + +namespace PubNubChatAPI.Entities +{ + //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 INTERNAL_ADMIN_CHANNEL = "PUBNUB_INTERNAL_ADMIN_CHANNEL"; + 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, false)) + { + 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, false)) + { + 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, false) && 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, false)) + { + 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 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 result = await operation.ExecuteAsync().ConfigureAwait(false); + + if (result.Status.Error) + { + Logger.Error($"Error when trying to GetUsers(): {result.Status.ErrorData.Information}"); + return default; + } + + var response = new UsersResponseWrapper() + { + Users = new List(), + Total = result.Result.TotalCount, + Page = result.Result.Page + }; + foreach (var resultMetadata in result.Result.Uuids) + { + var user = new User(this, resultMetadata.Uuid, resultMetadata); + response.Users.Add(user); + } + return response; + } + + /// + /// 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..b28068a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ChatAccessManager.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi.Enums; + +namespace PubNubChatAPI.Entities +{ + 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..d5b0e29 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChannelsResponseWrapper.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using PubnubApi; +using PubnubChatApi.Utilities; +using Channel = PubNubChatAPI.Entities.Channel; + +namespace PubnubChatApi.Entities.Data +{ + 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..b7b9f55 --- /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.Entities.Data +{ + 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..84a2209 --- /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.Entities.Data +{ + /// + /// 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..0e76957 --- /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.Entities.Data +{ + /// + /// 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..9d3e226 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatOperationResult.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using PubnubApi; +using PubNubChatAPI.Entities; + +namespace PubnubChatApi.Entities.Data +{ + 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, bool logIfError = true) + { + 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) + { + if (logIfError) + { + chat.Logger.Error($"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, bool logIfError = true) + { + 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 && logIfError) + { + chat.Logger.Error($"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..098901f --- /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.Entities.Data +{ + /// + /// 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..0e4aee5 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/CreatedChannelWrapper.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using PubNubChatAPI.Entities; + +namespace PubnubChatApi.Entities.Data +{ + 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..9bbfe85 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/EventsHistoryWrapper.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using PubnubChatApi.Entities.Events; + +namespace PubnubChatApi.Entities.Data +{ + 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..b8ca94e --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MarkMessagesAsReadWrapper.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using PubnubApi; +using PubNubChatAPI.Entities; +using PubnubChatApi.Utilities; + +namespace PubnubChatApi.Entities.Data +{ + 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..47b41fe --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MembersResponseWrapper.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using PubnubApi; +using PubNubChatAPI.Entities; +using PubnubChatApi.Utilities; + +namespace PubnubChatApi.Entities.Data +{ + 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..207a195 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MentionedUser.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace PubnubChatApi.Entities.Data +{ + 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..edb88c3 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/MessageAction.cs @@ -0,0 +1,12 @@ +using PubnubChatApi.Enums; + +namespace PubnubChatApi.Entities.Data +{ + 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..9f53116 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/PubnubChatConfig.cs @@ -0,0 +1,35 @@ +using PubnubApi; + +namespace PubnubChatApi.Entities.Data +{ + 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..85a4625 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ReferencedChannel.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace PubnubChatApi.Entities.Data +{ + 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..3f78b9b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/Restriction.cs @@ -0,0 +1,22 @@ +namespace PubnubChatApi.Entities.Data +{ + /// + /// 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..83ed1fd --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/SendTextParams.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using PubNubChatAPI.Entities; + +namespace PubnubChatApi.Entities.Data +{ + 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..fae4f6a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/TextLink.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace PubnubChatApi.Entities.Data +{ + 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..ea1844d --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UnreadMessageWrapper.cs @@ -0,0 +1,13 @@ +using System; +using PubNubChatAPI.Entities; +using PubnubChatApi.Utilities; + +namespace PubnubChatApi.Entities.Data +{ + 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..1506187 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionData.cs @@ -0,0 +1,15 @@ +using System; +using PubNubChatAPI.Entities; +using PubnubChatApi.Entities.Events; + +namespace PubnubChatApi.Entities.Data +{ + 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..9d8833b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UserMentionsWrapper.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using PubNubChatAPI.Entities; + +namespace PubnubChatApi.Entities.Data +{ + 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..9916b77 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/UsersResponseWrapper.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using PubnubApi; +using PubNubChatAPI.Entities; +using PubnubChatApi.Utilities; + +namespace PubnubChatApi.Entities.Data +{ + 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..ddf46b1 --- /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.Entities.Data +{ + 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..e214a05 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Events/ChatEvent.cs @@ -0,0 +1,13 @@ +using PubnubChatApi.Enums; + +namespace PubnubChatApi.Entities.Events +{ + 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..2c3b672 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Membership.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi.Entities.Data; +using PubnubChatApi.Enums; +using PubnubChatApi.Utilities; + +namespace PubNubChatAPI.Entities +{ + /// + /// 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..1dd7dc1 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs @@ -0,0 +1,761 @@ +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 PubnubApi; +using PubnubChatApi.Entities.Data; +using PubnubChatApi.Enums; +using PubnubChatApi.Utilities; + +namespace PubNubChatAPI.Entities +{ + /// + /// 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..855c2ef --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MessageDraft.cs @@ -0,0 +1,699 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PubnubChatApi.Entities.Data; +using DiffMatchPatch; + +namespace PubNubChatAPI.Entities +{ + 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.Users != null && usersWrapper.Users.Any()) + { + var user = usersWrapper.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; + } + 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) + { + if (text == null) 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; + } + } + + BroadcastDraftUpdate(); + } + + /// + /// Internal method to apply text insertion without triggering broadcasts + /// + private void ApplyInsertTextInternal(int offset, string text) + { + if (text == null) 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; + } + } + } + + 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..c8de60b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadChannel.cs @@ -0,0 +1,167 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi.Entities.Data; +using PubnubChatApi.Enums; + +namespace PubNubChatAPI.Entities +{ + 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..e2e5dbc --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadMessage.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi.Entities.Data; +using PubnubChatApi.Enums; +using PubnubChatApi.Utilities; + +namespace PubNubChatAPI.Entities +{ + 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..de24c5b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/User.cs @@ -0,0 +1,606 @@ +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 PubnubApi; +using PubnubChatApi.Entities.Data; +using PubnubChatApi.Entities.Events; +using PubnubChatApi.Enums; +using PubnubChatApi.Utilities; + +namespace PubNubChatAPI.Entities +{ + /// + /// 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..4b556e5 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessPermission.cs @@ -0,0 +1,13 @@ +namespace PubnubChatApi.Enums +{ + 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..94b0399 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubAccessResourceType.cs @@ -0,0 +1,8 @@ +namespace PubnubChatApi.Enums +{ + 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..3bb22fa --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChannelType.cs @@ -0,0 +1,9 @@ +namespace PubnubChatApi.Enums +{ + 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..dfd9e91 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatEventType.cs @@ -0,0 +1,13 @@ +namespace PubnubChatApi.Enums +{ + 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..68c3959 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubChatMessageType.cs @@ -0,0 +1,13 @@ +namespace PubnubChatApi.Enums +{ + /// + /// 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..8c7690f --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Enums/PubnubMessageActionType.cs @@ -0,0 +1,12 @@ +namespace PubnubChatApi.Enums +{ + 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..7df0fe1 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatEnumConverters.cs @@ -0,0 +1,76 @@ +using System; +using PubnubChatApi.Enums; + +namespace PubnubChatApi.Utilities +{ + 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..71152ce --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatListenerFactory.cs @@ -0,0 +1,16 @@ +using System; +using PubnubApi; + +namespace PubnubChatApi.Utilities +{ + 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..5f6a9cf --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PubnubApi; +using PubNubChatAPI.Entities; +using PubnubChatApi.Entities.Data; +using PubnubChatApi.Entities.Events; +using PubnubChatApi.Enums; +using Channel = PubNubChatAPI.Entities.Channel; + +namespace PubnubChatApi.Utilities +{ + 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..83bdf4a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatUtils.cs @@ -0,0 +1,32 @@ +using System; +using System.Globalization; +using PubnubApi; +using PubNubChatAPI.Entities; +using PubnubChatApi.Entities.Data; + +namespace PubnubChatApi.Utilities +{ + 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..58b08b8 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DiffMatchPatch.cs @@ -0,0 +1,2296 @@ +/* + * 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; + } + 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..6d2239f --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/DotNetListenerFactory.cs @@ -0,0 +1,20 @@ +using System; +using PubnubApi; + +namespace PubnubChatApi.Utilities +{ + 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..b48ddc1 --- /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.Utilities +{ + 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(id, 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(string id, 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..96cd096 --- /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.Utilities +{ + 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/UnityChat.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChat.cs new file mode 100644 index 0000000..71f5a42 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChat.cs @@ -0,0 +1,67 @@ +using System.Threading.Tasks; +using PubnubApi; +using PubnubApi.Unity; +using PubNubChatAPI.Entities; +using PubnubChatApi.Entities.Data; + +namespace PubnubChat.Runtime +{ + 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); + 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); + 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/UnityListenerFactory.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityListenerFactory.cs new file mode 100644 index 0000000..356046e --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityListenerFactory.cs @@ -0,0 +1,18 @@ +using System; +using PubnubApi; +using PubnubApi.Unity; +using PubnubChatApi.Utilities; + +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 1cac054aca55a9c27a75daac989f0bbba1ae08a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3754688 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
(PT<(`B`KWcYlfzhoUW~uM#4TNA%=O?5a#m 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

>@FWJ|ik{5vdo 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%0Dt3MYg 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�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®Ku^48wYLrtb@Pl;5!_=UCxewsDn>;@Kp|e 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

c2DDx|Q7a&`gw}I2W!xGR;p6m{Ha$6(pu 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