diff --git a/Realtime/Conversation/LCIMConversation.cs b/Realtime/Conversation/LCIMConversation.cs index bb02d54..1675acb 100644 --- a/Realtime/Conversation/LCIMConversation.cs +++ b/Realtime/Conversation/LCIMConversation.cs @@ -38,6 +38,14 @@ namespace LeanCloud.Realtime { get; internal set; } + public int Unread { + get; internal set; + } + + public LCIMMessage LastMessage { + get; internal set; + } + public DateTime CreatedAt { get; internal set; } @@ -86,6 +94,28 @@ namespace LeanCloud.Realtime { return response.ConvMessage.Count; } + /// + /// 将该会话标记为已读 + /// + /// + /// + public async Task Read() { + if (LastMessage == null) { + return this; + } + ReadCommand read = new ReadCommand(); + ReadTuple tuple = new ReadTuple { + Cid = Id, + Mid = LastMessage.Id, + Timestamp = LastMessage.SentTimestamp + }; + read.Convs.Add(tuple); + GenericCommand request = client.NewCommand(CommandType.Read, OpType.Open); + request.ReadMessage = read; + await client.connection.SendRequest(request); + return this; + } + public async Task Save() { ConvCommand conv = new ConvCommand { Cid = Id, @@ -479,6 +509,38 @@ namespace LeanCloud.Realtime { }; } + public async Task> QueryMessage(LCIMMessageQueryEndpoint start = null, + LCIMMessageQueryEndpoint end = null, + LCIMMessageQueryDirection direction = LCIMMessageQueryDirection.NewToOld, + int limit = 20, + int messageType = 0) { + LogsCommand logs = new LogsCommand { + Cid = Id + }; + if (start != null) { + logs.T = start.SentTimestamp; + logs.Mid = start.MessageId; + logs.TIncluded = start.IsClosed; + } + if (end != null) { + logs.Tt = end.SentTimestamp; + logs.Tmid = end.MessageId; + logs.TtIncluded = end.IsClosed; + } + logs.Direction = direction == LCIMMessageQueryDirection.NewToOld ? + LogsCommand.Types.QueryDirection.Old : LogsCommand.Types.QueryDirection.New; + logs.Limit = limit; + if (messageType != 0) { + logs.Lctype = messageType; + } + GenericCommand request = client.NewCommand(CommandType.Logs, OpType.Open); + request.LogsMessage = logs; + GenericCommand response = await client.connection.SendRequest(request); + // TODO 反序列化聊天记录 + + return null; + } + private LCIMPartiallySuccessResult NewPartiallySuccessResult(IEnumerable succesfulIds, IEnumerable errors) { LCIMPartiallySuccessResult result = new LCIMPartiallySuccessResult { SuccessfulClientIdList = succesfulIds.ToList() diff --git a/Realtime/Conversation/LCIMConversationQuery.cs b/Realtime/Conversation/LCIMConversationQuery.cs index 072c3b8..5d0b3cd 100644 --- a/Realtime/Conversation/LCIMConversationQuery.cs +++ b/Realtime/Conversation/LCIMConversationQuery.cs @@ -256,7 +256,10 @@ namespace LeanCloud.Realtime { foreach (object c in convs) { Dictionary cd = c as Dictionary; string convId = cd["objectId"] as string; - LCIMConversation conversation = client.GetOrCreateConversation(convId); + if (!client.conversationDict.TryGetValue(convId, out LCIMConversation conversation)) { + conversation = new LCIMConversation(client); + client.conversationDict[convId] = conversation; + } conversation.MergeFrom(cd); convList.Add(conversation); } diff --git a/Realtime/Conversation/LCIMMessageQueryOptions.cs b/Realtime/Conversation/LCIMMessageQueryOptions.cs new file mode 100644 index 0000000..c936feb --- /dev/null +++ b/Realtime/Conversation/LCIMMessageQueryOptions.cs @@ -0,0 +1,26 @@ +using System; + +namespace LeanCloud.Realtime { + public class LCIMMessageQueryEndpoint { + public string MessageId { + get; set; + } + + public long SentTimestamp { + get; set; + } + + public bool IsClosed { + get; set; + } + + public LCIMMessageQueryEndpoint() { + + } + } + + public enum LCIMMessageQueryDirection { + NewToOld, + OldToNew + } +} diff --git a/Realtime/Conversation/LCIMServiceConversation.cs b/Realtime/Conversation/LCIMServiceConversation.cs index 403d02b..b6f52c9 100644 --- a/Realtime/Conversation/LCIMServiceConversation.cs +++ b/Realtime/Conversation/LCIMServiceConversation.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace LeanCloud.Realtime { public class LCIMServiceConversation : LCIMConversation { diff --git a/Realtime/Internal/WebSocket/LCWebSocketConnection.cs b/Realtime/Internal/WebSocket/LCWebSocketConnection.cs index 30996f7..936ecb5 100644 --- a/Realtime/Internal/WebSocket/LCWebSocketConnection.cs +++ b/Realtime/Internal/WebSocket/LCWebSocketConnection.cs @@ -23,7 +23,7 @@ namespace LeanCloud.Realtime.Internal.WebSocket { private string id; - internal Action OnNotification { + internal Func OnNotification { get; set; } @@ -77,14 +77,14 @@ namespace LeanCloud.Realtime.Internal.WebSocket { } // 拼合 WebSocket Frame byte[] oldData = data; - data = new byte[data.Length + result.Count]; + data = new byte[oldData.Length + result.Count]; Array.Copy(oldData, data, oldData.Length); Array.Copy(buffer, 0, data, oldData.Length, result.Count); } while (!result.EndOfMessage); try { GenericCommand command = GenericCommand.Parser.ParseFrom(data); LCLogger.Debug($"{id} <= {command.Cmd}/{command.Op}: {command.ToString()}"); - HandleCommand(command); + _ = HandleCommand(command); } catch (Exception e) { // 解析消息错误 LCLogger.Error(e.Message); @@ -96,25 +96,29 @@ namespace LeanCloud.Realtime.Internal.WebSocket { } } - private void HandleCommand(GenericCommand command) { - if (command.HasI) { - // 应答 - if (responses.TryGetValue(command.I, out TaskCompletionSource tcs)) { - if (command.HasErrorMessage) { - // 错误 - ErrorCommand error = command.ErrorMessage; - int code = error.Code; - string detail = error.Detail; - // TODO 包装成异常抛出 - LCException exception = new LCException(code, detail); - tcs.SetException(exception); - } else { - tcs.SetResult(command); + private async Task HandleCommand(GenericCommand command) { + try { + if (command.HasI) { + // 应答 + if (responses.TryGetValue(command.I, out TaskCompletionSource tcs)) { + if (command.HasErrorMessage) { + // 错误 + ErrorCommand error = command.ErrorMessage; + int code = error.Code; + string detail = error.Detail; + // TODO 包装成异常抛出 + LCException exception = new LCException(code, detail); + tcs.SetException(exception); + } else { + tcs.SetResult(command); + } } + } else { + // 通知 + await OnNotification?.Invoke(command); } - } else { - // 通知 - OnNotification?.Invoke(command); + } catch (Exception e) { + LCLogger.Error(e.Message); } } diff --git a/Realtime/LCIMClient.cs b/Realtime/LCIMClient.cs index 196ba35..76eb563 100644 --- a/Realtime/LCIMClient.cs +++ b/Realtime/LCIMClient.cs @@ -5,13 +5,14 @@ using System.Linq; using LeanCloud.Realtime.Internal.WebSocket; using LeanCloud.Realtime.Protocol; using LeanCloud.Storage.Internal.Codec; +using LeanCloud.Storage.Internal; using Newtonsoft.Json; namespace LeanCloud.Realtime { public class LCIMClient { internal LCWebSocketConnection connection; - private Dictionary conversationDict; + internal Dictionary conversationDict; public string ClientId { get; private set; @@ -29,6 +30,11 @@ namespace LeanCloud.Realtime { get; set; } + /// + /// 当前客户端在某个对话中被禁言 + /// + public Action OnMuted; + /// /// 客户端连接断开 /// @@ -50,6 +56,25 @@ namespace LeanCloud.Realtime { get; set; } + /// + /// 客户端连接断开 + /// + public Action OnDisconnect { + get; set; + } + + /// + /// 用户在其他客户端登录,当前客户端被服务端强行下线 + /// + public Action OnConflict { + get; set; + } + + /// + /// 该对话信息被更新 + /// + public Action, string> OnConversationInfoUpdated; + /// /// 当前用户被添加至某个对话 /// @@ -71,14 +96,74 @@ namespace LeanCloud.Realtime { get; set; } + /// + /// 有成员被从某个对话中移除 + /// public Action, string> OnMembersLeft { get; set; } + /// + /// 有成员被加入某个对话的黑名单 + /// + public Action, string> OnMembersBlocked { + get; set; + } + + /// + /// 有成员被移出某个对话的黑名单 + /// + public Action, string> OnMembersUnblocked { + get; set; + } + + /// + /// 有成员在某个对话中被禁言 + /// + public Action, string> OnMembersMuted { + get; set; + } + + /// + /// 有成员被移出某个对话的黑名单 + /// + public Action, string> OnMembersUnmuted { + get; set; + } + + /// + /// 有成员的对话信息被更新 + /// + public Action, string> OnMemberInfoUpdated; + + /// + /// 当前用户收到消息 + /// public Action OnMessageReceived { get; set; } + /// + /// 消息被撤回 + /// + public Action OnMessageRecall { + get; set; + } + + /// + /// 消息被修改 + /// + public Action OnMessageUpdate { + get; set; + } + + /// + /// 未读消息数目更新 + /// + public Action> OnUnreadMessagesCountUpdated { + get; set; + } + internal ILCIMSignatureFactory SignatureFactory { get; private set; } @@ -117,6 +202,8 @@ namespace LeanCloud.Realtime { /// /// public async Task Close() { + GenericCommand request = NewCommand(CommandType.Session, OpType.Close); + await connection.SendRequest(request); await connection.Close(); } @@ -209,8 +296,8 @@ namespace LeanCloud.Realtime { if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException(nameof(id)); } - LCIMConversationQuery query = GetQuery(); - query.WhereEqualTo("objectId", id) + LCIMConversationQuery query = GetQuery() + .WhereEqualTo("objectId", id) .Limit(1); List results = await query.Find(); if (results == null || results.Count < 1) { @@ -244,19 +331,44 @@ namespace LeanCloud.Realtime { return new LCIMConversationQuery(this); } - private void OnNotification(GenericCommand notification) { + private async Task OnNotification(GenericCommand notification) { switch (notification.Cmd) { + case CommandType.Session: + await OnSessionNotification(notification); + break; case CommandType.Conv: OnConversationNotification(notification); break; case CommandType.Direct: - OnDirectNotification(notification.DirectMessage); + await OnDirectNotification(notification.DirectMessage); + break; + case CommandType.Unread: + await OnUnreadNotification(notification.UnreadMessage); break; default: break; } } + private async Task OnSessionNotification(GenericCommand notification) { + switch (notification.Op) { + case OpType.Closed: + await OnSessionClosed(notification.SessionMessage); + break; + default: + break; + } + } + + private async Task OnSessionClosed(SessionCommand session) { + int code = session.Code; + string reason = session.Reason; + string detail = session.Detail; + await connection.Close(); + // TODO 关闭连接后回调给开发者 + + } + private void OnConversationNotification(GenericCommand notification) { ConvCommand conv = notification.ConvMessage; switch (notification.Op) { @@ -273,21 +385,24 @@ namespace LeanCloud.Realtime { OnConversationMemberLeft(conv); break; case OpType.Updated: - OnPropertiesUpdated(conv); + OnConversationPropertiesUpdated(conv); + break; + case OpType.MemberInfoChanged: + OnConversationMemberInfoChanged(conv); break; default: break; } } - private void OnConversationJoined(ConvCommand conv) { - LCIMConversation conversation = GetOrCreateConversation(conv.Cid); + private async void OnConversationJoined(ConvCommand conv) { + LCIMConversation conversation = await GetOrQueryConversation(conv.Cid); conversation.MergeFrom(conv); OnInvited?.Invoke(conversation, conv.InitBy); } - private void OnConversationMembersJoined(ConvCommand conv) { - LCIMConversation conversation = GetOrCreateConversation(conv.Cid); + private async void OnConversationMembersJoined(ConvCommand conv) { + LCIMConversation conversation = await GetOrQueryConversation(conv.Cid); conversation.MergeFrom(conv); OnMembersJoined?.Invoke(conversation, conv.M.ToList(), conv.InitBy); } @@ -305,14 +420,19 @@ namespace LeanCloud.Realtime { } } - private void OnPropertiesUpdated(ConvCommand conv) { + private void OnConversationPropertiesUpdated(ConvCommand conv) { if (conversationDict.TryGetValue(conv.Cid, out LCIMConversation conversation)) { - // TODO + // TODO 修改对话属性,并回调给开发者 + OnConversationInfoUpdated?.Invoke(conversation, null, conv.InitBy); } } - private void OnDirectNotification(DirectCommand direct) { + private void OnConversationMemberInfoChanged(ConvCommand conv) { + + } + + private async Task OnDirectNotification(DirectCommand direct) { LCIMMessage message = null; if (direct.HasBinaryMsg) { // 二进制消息 @@ -321,7 +441,8 @@ namespace LeanCloud.Realtime { } else { // 文本消息 string messageData = direct.Msg; - Dictionary msg = JsonConvert.DeserializeObject>(messageData); + Dictionary msg = JsonConvert.DeserializeObject>(messageData, + new LCJsonConverter()); int msgType = (int)(long)msg["_lctype"]; switch (msgType) { case -1: @@ -347,16 +468,30 @@ namespace LeanCloud.Realtime { } message.Decode(direct); } - // TODO 获取对话 - - OnMessageReceived?.Invoke(null, message); + // 获取对话 + LCIMConversation conversation = await GetOrQueryConversation(direct.Cid); + OnMessageReceived?.Invoke(conversation, message); } - internal LCIMConversation GetOrCreateConversation(string convId) { - if (!conversationDict.TryGetValue(convId, out LCIMConversation conversation)) { - conversation = new LCIMConversation(this); - conversationDict.Add(convId, conversation); + private async Task OnUnreadNotification(UnreadCommand unread) { + List conversationList = new List(); + foreach (UnreadTuple conv in unread.Convs) { + // 查询对话 + LCIMConversation conversation = await GetOrQueryConversation(conv.Cid); + conversation.Unread = conv.Unread; + // TODO 反序列化对话 + // 最后一条消息 + JsonConvert.DeserializeObject>(conv.Data); + conversationList.Add(conversation); } + OnUnreadMessagesCountUpdated?.Invoke(conversationList); + } + + internal async Task GetOrQueryConversation(string convId) { + if (conversationDict.TryGetValue(convId, out LCIMConversation conversation)) { + return conversation; + } + conversation = await GetConversation(convId); return conversation; } diff --git a/Realtime/Message/LCIMAudioMessage.cs b/Realtime/Message/LCIMAudioMessage.cs index 3dcece3..fbc28e5 100644 --- a/Realtime/Message/LCIMAudioMessage.cs +++ b/Realtime/Message/LCIMAudioMessage.cs @@ -23,7 +23,9 @@ namespace LeanCloud.Realtime { Dictionary data = base.Encode(); Dictionary fileData = data["_lcfile"] as Dictionary; Dictionary metaData = fileData["metaData"] as Dictionary; - metaData["duration"] = File.MetaData["duration"]; + if (File.MetaData.TryGetValue("duration", out object duration)) { + metaData["duration"] = duration; + } return data; } diff --git a/Realtime/Message/LCIMFileMessage.cs b/Realtime/Message/LCIMFileMessage.cs index 1861400..7a4375e 100644 --- a/Realtime/Message/LCIMFileMessage.cs +++ b/Realtime/Message/LCIMFileMessage.cs @@ -46,10 +46,13 @@ namespace LeanCloud.Realtime { { "url", File.Url }, { "metaData", new Dictionary { { "name", File.Name }, - { "format", File.MimeType }, - { "size", File.MetaData["size"] } + { "format", File.MimeType } } } }; + if (File.MetaData.TryGetValue("size", out object size)) { + Dictionary metaData = fileData["metaData"] as Dictionary; + metaData["size"] = size; + } Dictionary data = base.Encode(); data["_lcfile"] = fileData; return data; diff --git a/Realtime/Message/LCIMImageMessage.cs b/Realtime/Message/LCIMImageMessage.cs index 5e13028..fcf59ef 100644 --- a/Realtime/Message/LCIMImageMessage.cs +++ b/Realtime/Message/LCIMImageMessage.cs @@ -32,8 +32,12 @@ namespace LeanCloud.Realtime { Dictionary data = base.Encode(); Dictionary fileData = data["_lcfile"] as Dictionary; Dictionary metaData = fileData["metaData"] as Dictionary; - metaData["width"] = File.MetaData["width"]; - metaData["height"] = File.MetaData["height"]; + if (File.MetaData.TryGetValue("width", out object width)) { + metaData["width"] = width; + } + if (File.MetaData.TryGetValue("height", out object height)) { + metaData["height"] = height; + } return data; } diff --git a/Realtime/Message/LCIMTypedMessage.cs b/Realtime/Message/LCIMTypedMessage.cs index 566c5e6..1e598a4 100644 --- a/Realtime/Message/LCIMTypedMessage.cs +++ b/Realtime/Message/LCIMTypedMessage.cs @@ -52,5 +52,34 @@ namespace LeanCloud.Realtime { customProperties = LCDecoder.Decode(attrObj) as Dictionary; } } + + internal static LCIMTypedMessage Deserialize(Dictionary messageData) { + LCIMTypedMessage message = null; + int msgType = (int)(long)messageData["_lctype"]; + switch (msgType) { + case -1: + message = new LCIMTextMessage(); + break; + case -2: + message = new LCIMImageMessage(); + break; + case -3: + message = new LCIMAudioMessage(); + break; + case -4: + message = new LCIMVideoMessage(); + break; + case -5: + message = new LCIMLocationMessage(); + break; + case -6: + message = new LCIMFileMessage(); + break; + default: + break; + } + //message.Decode(direct); + return message; + } } } diff --git a/Realtime/Message/LCIMVideoMessage.cs b/Realtime/Message/LCIMVideoMessage.cs index 95c17a2..9754dd6 100644 --- a/Realtime/Message/LCIMVideoMessage.cs +++ b/Realtime/Message/LCIMVideoMessage.cs @@ -23,9 +23,15 @@ namespace LeanCloud.Realtime { Dictionary data = base.Encode(); Dictionary fileData = data["_lcfile"] as Dictionary; Dictionary metaData = fileData["metaData"] as Dictionary; - metaData["width"] = File.MetaData["width"]; - metaData["height"] = File.MetaData["height"]; - metaData["duration"] = File.MetaData["duration"]; + if (File.MetaData.TryGetValue("width", out object width)) { + metaData["width"] = width; + } + if (File.MetaData.TryGetValue("height", out object height)) { + metaData["height"] = height; + } + if (File.MetaData.TryGetValue("duration", out object duration)) { + metaData["duration"] = duration; + } return data; } diff --git a/Test/RealtimeConsole/Program.cs b/Test/RealtimeConsole/Program.cs index 805b427..b449b4e 100644 --- a/Test/RealtimeConsole/Program.cs +++ b/Test/RealtimeConsole/Program.cs @@ -15,13 +15,13 @@ namespace RealtimeConsole { LCLogger.LogDelegate += (level, info) => { switch (level) { case LCLogLevel.Debug: - Console.WriteLine($"[DEBUG] {info}"); + Console.WriteLine($"[DEBUG]\n{info}"); break; case LCLogLevel.Warn: - Console.WriteLine($"[WARNING] {info}"); + Console.WriteLine($"[WARNING]\n{info}"); break; case LCLogLevel.Error: - Console.WriteLine($"[ERROR] {info}"); + Console.WriteLine($"[ERROR]\n{info}"); break; default: Console.WriteLine(info); @@ -30,22 +30,131 @@ namespace RealtimeConsole { }; LCApplication.Initialize("ikGGdRE2YcVOemAaRbgp1xGJ-gzGzoHsz", "NUKmuRbdAhg1vrb2wexYo1jo", "https://ikggdre2.lc-cn-n1-shared.com"); - //_ = Start(); + //Conversation().Wait(); //_ = ChatRoom(); - _ = TemporaryConversation(); + //_ = TemporaryConversation(); + + //_ = Signature(); + + //_ = Block(); + + //_ = Mute(); + + //QueryConversation().Wait(); + + //_ = OpenAndClose(); + + //SendMessage().Wait(); + + _ = Unread(); Console.ReadKey(true); } + static async Task Run(int s) { + for (int i = 0; i < s; i++) { + Console.WriteLine($"run {i}"); + await Task.Delay(1000); + Console.WriteLine($"run {i} done"); + } + } + + static async Task Unread() { + LCIMClient u2 = new LCIMClient("u2"); + await u2.Open(); + u2.OnUnreadMessagesCountUpdated = conversationList => { + foreach (LCIMConversation conv in conversationList) { + Console.WriteLine($"unread: {conv.Unread}"); + } + }; + } + + static async Task SendMessage() { + try { + LCIMClient u1 = new LCIMClient("u1"); + await u1.Open(); + LCIMConversation conversation = await u1.CreateConversation(new string[] { "u2" }); + + LCIMTextMessage textMessage = new LCIMTextMessage("hello, text message"); + await conversation.Send(textMessage); + + //LCFile file = new LCFile("avatar", "../../../Storage.Test/assets/hello.png"); + //await file.Save(); + //LCIMImageMessage imageMessage = new LCIMImageMessage(file); + //await conversation.Send(imageMessage); + } catch (Exception e) { + Console.WriteLine(e.ToString()); + } + } + + static async Task OpenAndClose() { + LCIMClient o1 = new LCIMClient("o1"); + await o1.Open(); + await o1.Close(); + } + + static async Task QueryConversation() { + LCIMClient m2 = new LCIMClient("m2"); + await m2.Open(); + + LCIMConversation conv = (await m2.GetQuery() + .WhereEqualTo("objectId", "5e7863bf90aef5aa849be75a") + .Find())[0]; + LCIMTextMessage textMessage = new LCIMTextMessage("hello, world"); + await conv.Send(textMessage); + } + + static async Task Mute() { + LCIMClient m1 = new LCIMClient("m0"); + await m1.Open(); + + LCIMClient m2 = new LCIMClient("m2"); + await m2.Open(); + + LCIMConversation conversation = await m1.CreateConversation(new string[] { "m2", "m3" }); + await conversation.MuteMembers(new string[] { "m2" }); + + LCIMConversation conv = (await m2.GetQuery() + .WhereEqualTo("objectId", conversation.Id) + .Find())[0]; + LCIMTextMessage textMessage = new LCIMTextMessage("hello, world"); + await conv.Send(textMessage); + } + + static async Task Block() { + LocalSignatureFactory signatureFactory = new LocalSignatureFactory(); + LCIMClient c1 = new LCIMClient("c0"); + await c1.Open(); + LCIMConversation conversation = await c1.CreateConversation(new string[] { "c2", "c3", "c4", "c5" }); + LCIMTextMessage textMessage = new LCIMTextMessage("hello"); + await conversation.Send(textMessage); + await conversation.BlockMembers(new string[] { "c5" }); + + LCIMClient c5 = new LCIMClient("c5"); + await c5.Open(); + await conversation.Add(new string[] { "c5" }); + } + + static async Task Signature() { + LocalSignatureFactory signatureFactory = new LocalSignatureFactory(); + LCIMClient hello = new LCIMClient("hello111", signatureFactory); + await hello.Open(); + } + static async Task ChatRoom() { - LCIMClient hello = new LCIMClient("hello"); + LocalSignatureFactory signatureFactory = new LocalSignatureFactory(); + LCIMClient hello = new LCIMClient("hello", signatureFactory); await hello.Open(); string name = Guid.NewGuid().ToString(); LCIMChatRoom chatRoom = await hello.CreateChatRoom(name); Console.WriteLine(chatRoom.Name); + + await chatRoom.Add(new string[] { "world" }); + + await chatRoom.Remove(new string[] { "world" }); } static async Task TemporaryConversation() { @@ -61,7 +170,7 @@ namespace RealtimeConsole { Console.WriteLine(temporaryConversation.Id); } - static async Task Start() { + static async Task Conversation() { LCIMClient hello = new LCIMClient("hello"); await hello.Open();